[gjs/esm-local-imports: 4/6] esm: Enable ESM static imports



commit 1dd2ff6fd188e389b4c8f7277aebd80c41f5c028
Author: Evan Welsh <noreply evanwelsh com>
Date:   Sat Jun 6 12:10:22 2020 -0500

    esm: Enable ESM static imports

 gi/repo.cpp                                  |  95 ++++--
 gjs/.eslintrc.yml                            |  22 ++
 gjs/context-private.h                        |   7 +-
 gjs/context.cpp                              | 207 ++++++++++--
 gjs/global.cpp                               | 144 ++++++++-
 gjs/global.h                                 |   9 +-
 gjs/importer.cpp                             |  17 +-
 gjs/internal.cpp                             | 450 +++++++++++++++++++++++++++
 gjs/internal.h                               |  66 ++++
 gjs/jsapi-util.cpp                           |   4 +
 gjs/jsapi-util.h                             |   3 +
 gjs/module.cpp                               | 151 ++++++++-
 gjs/module.h                                 |  76 +++++
 gjs/module.js                                | 218 +++++++++++++
 installed-tests/esm-test.sh                  |  25 ++
 installed-tests/esm.test.in                  |   4 +
 installed-tests/esm/default-import.js        |   3 +
 installed-tests/esm/default-import.js.output |   1 +
 installed-tests/esm/exports.js               |   3 +
 installed-tests/esm/gi-import.js             |   3 +
 installed-tests/esm/gi-import.js.output      |   1 +
 installed-tests/esm/named-import.js          |   3 +
 installed-tests/esm/named-import.js.output   |   1 +
 installed-tests/esm/system-import.js         |   3 +
 installed-tests/esm/system-import.js.output  |   3 +
 installed-tests/meson.build                  |  39 +++
 js.gresource.xml                             |   7 +
 meson.build                                  |   1 +
 modules/esm/.eslintrc.yml                    |   4 +
 modules/esm/gi.js                            |  14 +
 modules/esm/system.js                        |   1 +
 31 files changed, 1540 insertions(+), 45 deletions(-)
---
diff --git a/gi/repo.cpp b/gi/repo.cpp
index 93ba9447..ca306366 100644
--- a/gi/repo.cpp
+++ b/gi/repo.cpp
@@ -58,6 +58,7 @@
 #include "gjs/jsapi-class.h"
 #include "gjs/jsapi-util.h"
 #include "gjs/mem-private.h"
+#include "gjs/module.h"
 #include "util/log.h"
 
 struct JSFunctionSpec;
@@ -150,8 +151,13 @@ static bool resolve_namespace_object(JSContext* context,
         return false;
 
     JS::RootedValue override(context);
-    if (!lookup_override_function(context, ns_id, &override))
-        return false;
+
+    // We do not load overrides on the internal global.
+    if (gjs_global_is_type(context, GjsGlobalType::DEFAULT)) {
+        if (!lookup_override_function(context, ns_id, &override)) {
+            return false;
+        }
+    }
 
     JS::RootedValue result(context);
     if (!override.isUndefined() &&
@@ -637,37 +643,90 @@ lookup_override_function(JSContext             *cx,
     }
     return true;
 
- fail:
+fail:
     saved_exc.drop();
     return false;
 }
 
-JSObject*
-gjs_lookup_namespace_object_by_name(JSContext      *context,
-                                    JS::HandleId    ns_name)
-{
-    auto global = gjs_get_import_global(context);
-    JS::RootedValue importer(
-        context, gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS));
-    g_assert(importer.isObject());
-
-    JS::RootedObject repo(context), importer_obj(context, &importer.toObject());
-    const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
-    if (!gjs_object_require_property(context, importer_obj, "importer",
-                                     atoms.gi(), &repo)) {
+GJS_JSAPI_RETURN_CONVENTION
+static JSObject* lookup_namespace(JSContext* context, JS::HandleId ns_name) {
+    auto native_registry = gjs_get_native_module_registry(context);
+    auto it = native_registry->lookup("gi");
+    if (!it.found()) {
         gjs_log_exception(context);
-        gjs_throw(context, "No gi property in importer");
+        gjs_throw(context, "No gi property in native registry");
         return NULL;
     }
 
+    JS::RootedObject gi(context, it->value());
     JS::RootedObject retval(context);
-    if (!gjs_object_require_property(context, repo, "GI repository object",
+    if (!gjs_object_require_property(context, gi, "GI repository object",
                                      ns_name, &retval))
         return NULL;
 
     return retval;
 }
 
+GJS_JSAPI_RETURN_CONVENTION
+static JSObject* lookup_internal_namespace(JSContext* cx,
+                                           JS::HandleId ns_name) {
+    auto gcx = GjsContextPrivate::from_cx(cx);
+    JS::RootedObject global(cx, gcx->internal_global());
+    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+
+    // The internal global only supports GObject, Gio, GLib, and private
+    // namespaces.
+    if (ns_name == atoms.gobject() || ns_name == atoms.gio() ||
+        ns_name == atoms.glib() || ns_name == atoms.private_ns_marker()) {
+        JS::RootedObject retval(cx);
+
+        if (!gjs_object_require_property(
+                cx, global, "internal namespace import", ns_name, &retval))
+            return nullptr;
+
+        return retval;
+    } else if (JSID_IS_STRING(ns_name)) {
+        JS::RootedString str(cx, JSID_TO_STRING(ns_name));
+
+        JS::UniqueChars name(gjs_string_to_utf8(cx, JS::StringValue(str)));
+
+        gjs_throw(
+            cx,
+            "Attempted to load unknown GI namespace (%s) on internal global.",
+            name.get());
+    } else {
+        gjs_throw(cx,
+                  "Attempted to load invalid GI namespace on internal global.");
+    }
+
+    return nullptr;
+}
+
+JSObject* gjs_lookup_namespace_object_by_name(JSContext* context,
+                                              JS::HandleId ns_name) {
+    JSObject* global = JS::CurrentGlobalOrNull(context);
+    JSObject* ns = nullptr;
+
+    switch (gjs_global_get_type(global)) {
+        case GjsGlobalType::DEFAULT:
+            ns = lookup_namespace(context, ns_name);
+            break;
+        case GjsGlobalType::INTERNAL:
+            ns = lookup_internal_namespace(context, ns_name);
+            break;
+        case GjsGlobalType::DEBUGGER:
+            ns = nullptr;
+            break;
+    }
+
+    if (!ns) {
+        return nullptr;
+    }
+
+    JS::RootedObject retval(context, ns);
+    return retval;
+}
+
 const char*
 gjs_info_type_name(GIInfoType type)
 {
diff --git a/gjs/.eslintrc.yml b/gjs/.eslintrc.yml
new file mode 100644
index 00000000..748035e5
--- /dev/null
+++ b/gjs/.eslintrc.yml
@@ -0,0 +1,22 @@
+---
+extends: ../.eslintrc.yml
+globals:
+  ARGV: off
+  Debugger: readonly
+  GIRepositoryGType: off
+  globalThis: readonly
+  imports: off
+  Intl: readonly
+  log: readonly
+  logError: readonly
+  print: readonly
+  printerr: readonly
+  GLib: readonly
+  Gio: readonly
+  ByteUtils: readonly
+  lookupModule: readonly
+  registerModule: readonly
+  lookupInternalModule: readonly
+  registerInternalModule: readonly
+  setModuleResolveHook: readonly
+  getModuleUri: readonly
\ No newline at end of file
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 537ae2de..bd711ba5 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -95,6 +95,7 @@ class GjsContextPrivate : public JS::JobQueue {
     GjsContext* m_public_context;
     JSContext* m_cx;
     JS::Heap<JSObject*> m_global;
+    JS::Heap<JSObject*> m_internal_global;
     GThread* m_owner_thread;
 
     char* m_program_name;
@@ -187,6 +188,9 @@ class GjsContextPrivate : public JS::JobQueue {
     GJS_USE GjsContext* public_context(void) const { return m_public_context; }
     GJS_USE JSContext* context(void) const { return m_cx; }
     GJS_USE JSObject* global(void) const { return m_global.get(); }
+    GJS_USE JSObject* internal_global(void) const {
+        return m_internal_global.get();
+    }
     GJS_USE GjsProfiler* profiler(void) const { return m_profiler; }
     GJS_USE const GjsAtoms& atoms(void) const { return *m_atoms; }
     GJS_USE bool destroying(void) const { return m_destroying; }
@@ -259,7 +263,8 @@ class GjsContextPrivate : public JS::JobQueue {
     void unregister_unhandled_promise_rejection(uint64_t id);
 
     bool register_module(const char* identifier, const char* filename,
-                         const char* mod_text, size_t mod_len, GError** error);
+                         const char* module, ssize_t module_len,
+                         GError** error);
 
     void set_sweeping(bool value);
 
diff --git a/gjs/context.cpp b/gjs/context.cpp
index e705a793..a4d14d71 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -82,9 +82,11 @@
 #include "gjs/error-types.h"
 #include "gjs/global.h"
 #include "gjs/importer.h"
+#include "gjs/internal.h"
 #include "gjs/jsapi-util-args.h"
 #include "gjs/jsapi-util.h"
 #include "gjs/mem.h"
+#include "gjs/module.h"
 #include "gjs/native.h"
 #include "gjs/profiler-private.h"
 #include "gjs/profiler.h"
@@ -333,6 +335,8 @@ gjs_context_class_init(GjsContextClass *klass)
 void GjsContextPrivate::trace(JSTracer* trc, void* data) {
     auto* gjs = static_cast<GjsContextPrivate*>(data);
     JS::TraceEdge<JSObject*>(trc, &gjs->m_global, "GJS global object");
+    JS::TraceEdge<JSObject*>(trc, &gjs->m_internal_global,
+                             "GJS internal global object");
     gjs->m_atoms->trace(trc);
     gjs->m_job_queue.trace(trc);
     gjs->m_object_init_list.trace(trc);
@@ -419,6 +423,7 @@ void GjsContextPrivate::dispose(void) {
         gjs_debug(GJS_DEBUG_CONTEXT, "Ending trace on global object");
         JS_RemoveExtraGCRootsTracer(m_cx, &GjsContextPrivate::trace, this);
         m_global = nullptr;
+        m_internal_global = nullptr;
 
         gjs_debug(GJS_DEBUG_CONTEXT, "Freeing allocated resources");
         delete m_fundamental_table;
@@ -504,23 +509,50 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
 
     m_atoms = new GjsAtoms();
 
-    JS::RootedObject global(
-        m_cx, gjs_create_global_object(cx, GjsGlobalType::DEFAULT));
+    JS::RootedObject internal_global(
+        m_cx, gjs_create_global_object(cx, GjsGlobalType::INTERNAL));
 
-    if (!global) {
+    if (!internal_global) {
         gjs_log_exception(m_cx);
-        g_error("Failed to initialize global object");
+        g_error("Failed to initialize internal global object");
     }
 
-    JSAutoRealm ar(m_cx, global);
+    JSAutoRealm ar(m_cx, internal_global);
 
-    m_global = global;
+    m_internal_global = internal_global;
     JS_AddExtraGCRootsTracer(m_cx, &GjsContextPrivate::trace, this);
 
     if (!m_atoms->init_atoms(m_cx)) {
         gjs_log_exception(m_cx);
         g_error("Failed to initialize global strings");
     }
+
+    if (!gjs_define_global_properties(m_cx, internal_global,
+                                      GjsGlobalType::INTERNAL,
+                                      "GJS internal global", "nullptr")) {
+        gjs_log_exception(m_cx);
+        g_warning("Failed to define properties on internal global object.");
+    }
+
+    JS::RootedObject global(
+        m_cx,
+        gjs_create_global_object(cx, GjsGlobalType::DEFAULT, internal_global));
+
+    if (!global) {
+        gjs_log_exception(m_cx);
+        g_error("Failed to initialize global object");
+    }
+
+    m_global = global;
+
+    // Load internal script *must* be called from the internal realm.
+    if (!gjs_load_internal_script(cx, "module")) {
+        gjs_log_exception(cx);
+        g_warning("Failed to load internal dynamic module hooks.");
+    }
+
+    auto realm = JS::EnterRealm(m_cx, global);
+
     std::vector<std::string> paths;
     if (m_search_path)
         paths = {m_search_path, m_search_path + g_strv_length(m_search_path)};
@@ -542,6 +574,8 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
         gjs_log_exception(m_cx);
         g_error("Failed to define properties on global object");
     }
+
+    JS::LeaveRealm(m_cx, realm);
 }
 
 void GjsContextPrivate::set_args(std::vector<std::string> args) {
@@ -1053,24 +1087,161 @@ bool GjsContextPrivate::eval(const char* script, ssize_t script_len,
 
 bool GjsContextPrivate::eval_module(const char* identifier,
                                     uint8_t* exit_status_p, GError** error) {
-    // TODO Implement eval_module
-    g_error(
-        "GjsContextPrivate::eval_module is not implemented. Exiting with "
-        "error.");
+    bool auto_profile = m_should_profile;
 
-    return false;
+    if (auto_profile &&
+        (_gjs_profiler_is_running(m_profiler) || m_should_listen_sigusr2))
+        auto_profile = false;
+
+    if (auto_profile)
+        gjs_profiler_start(m_profiler);
+
+    JSAutoRealm ac(m_cx, m_global);
+
+    auto it = gjs_get_esm_registry(m_cx)->lookup(identifier);
+
+    if (!it) {
+        g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+                    "Cannot find module with identifier %s.", identifier);
+        return false;
+    }
+
+    bool ok = true;
+
+    JS::RootedObject obj(m_cx, it->value());
+
+    if (!JS::ModuleInstantiate(m_cx, obj)) {
+        gjs_log_exception(m_cx);
+        g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+                    "Failed to instantiate module %s.", identifier);
+
+        return false;
+    }
+
+    if (!JS::ModuleEvaluate(m_cx, obj)) {
+        ok = false;
+    }
+
+    schedule_gc_if_needed();
+
+    if (JS_IsExceptionPending(m_cx)) {
+        gjs_log_exception(m_cx);
+        g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+                    "Uncaught exception in %s.", identifier);
+        return false;
+    }
+
+    gjs_debug(GJS_DEBUG_CONTEXT, "Module evaluation succeeded for module %s.",
+              identifier);
+
+    /* The promise job queue should be drained even on error, to finish
+     * outstanding async tasks before the context is torn down. Drain after
+     * uncaught exceptions have been reported since draining runs callbacks.
+     */
+    {
+        JS::AutoSaveExceptionState saved_exc(m_cx);
+        ok = run_jobs_fallible() && ok;
+    }
+
+    if (auto_profile)
+        gjs_profiler_stop(m_profiler);
+
+    if (!ok) {
+        uint8_t code;
+
+        if (should_exit(&code)) {
+            *exit_status_p = code;
+            g_set_error(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT,
+                        "Exit with code %d", code);
+            return false;
+        }
+
+        if (!JS_IsExceptionPending(m_cx)) {
+            g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+                        "Module %s terminated with an uncatchable exception",
+                        identifier);
+        } else {
+            g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+                        "Module %s threw an exception", identifier);
+        }
+
+        gjs_log_exception(m_cx);
+        /* No exit code from script, but we don't want to exit(0) */
+        *exit_status_p = 1;
+        return false;
+    }
+
+    if (exit_status_p) {
+        /* Assume success if no integer was returned */
+        *exit_status_p = 0;
+    }
+
+    return true;
 }
 
 bool GjsContextPrivate::register_module(const char* identifier,
                                         const char* filename,
-                                        const char* module, size_t module_len,
+                                        const char* script, ssize_t script_len,
                                         GError** error) {
-    // TODO Implement register_module
-    g_warning(
-        "GjsContextPrivate::register_module is not yet implemented. Printing "
-        "module text...");
-    g_warning("%s", module);
-    return true;
+    JSAutoRealm ac(m_cx, m_global);
+
+    // Module registration uses exceptions to report errors
+    // so we'll store the exception state, clear it, attempt to load the
+    // module, then restore the original exception state.
+    JS::AutoSaveExceptionState exp_state(m_cx);
+
+    auto module = new GjsESModule(identifier, filename);
+
+    auto esm_registry = gjs_get_esm_registry(m_cx);
+
+    auto it = esm_registry->lookupForAdd(filename);
+
+    if (it.found()) {
+        gjs_throw(m_cx, "Module '%s' already registered", filename);
+        return false;
+    }
+
+    JS::RootedObject module_record(m_cx,
+                                   module->compile(m_cx, script, script_len));
+
+    if (module_record && !esm_registry->add(it, identifier, module_record)) {
+        JS_ReportOutOfMemory(m_cx);
+        return false;
+    }
+
+    if (module_record) {
+        return true;
+    }
+
+    // Our message could come from memory owned by us or by the runtime.
+    const char* msg = nullptr;
+
+    JS::RootedValue exc(m_cx);
+    if (JS_GetPendingException(m_cx, &exc)) {
+        JS::RootedObject exc_obj(m_cx, &exc.toObject());
+        JSErrorReport* report = JS_ErrorFromException(m_cx, exc_obj);
+        if (report) {
+            msg = report->message().c_str();
+        } else {
+            JS::RootedString js_message(m_cx, JS::ToString(m_cx, exc));
+
+            if (js_message) {
+                JS::UniqueChars cstr(JS_EncodeStringToUTF8(m_cx, js_message));
+                msg = cstr.get();
+            }
+        }
+    }
+
+    g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+                "Error registering module '%s': %s", identifier,
+                msg ? msg : "unknown");
+
+    // We've successfully handled the exception so we can clear it.
+    // This is necessary because AutoSaveExceptionState doesn't erase
+    // exceptions when it restores the previous exception state.
+    JS_ClearPendingException(m_cx);
+
+    return false;
 }
 
 bool
diff --git a/gjs/global.cpp b/gjs/global.cpp
index e05644a4..88cb420c 100644
--- a/gjs/global.cpp
+++ b/gjs/global.cpp
@@ -58,6 +58,7 @@
 #include "gjs/context-private.h"
 #include "gjs/engine.h"
 #include "gjs/global.h"
+#include "gjs/internal.h"
 #include "gjs/jsapi-util-args.h"
 #include "gjs/jsapi-util.h"
 #include "gjs/module.h"
@@ -311,11 +312,24 @@ class GjsGlobal {
         JS_FN("printerr", gjs_printerr, 0, GJS_MODULE_PROP_FLAGS),
         JS_FS_END};
 
+    static void setup_global(JSContext* cx, JSObject* global) {
+        JSAutoRealm ac(cx, global);
+
+        JSRuntime* rt = JS_GetRuntime(cx);
+
+        JS::SetModuleResolveHook(rt, gjs_module_resolve);
+        JS::SetModuleMetadataHook(rt, gjs_populate_module_meta);
+    }
+
  public:
     GJS_USE
     static JSObject* create(JSContext* cx) {
         auto global = global_create_new(cx, &klass);
 
+        if (global) {
+            setup_global(cx, global);
+        }
+
         return global;
     }
 
@@ -324,6 +338,10 @@ class GjsGlobal {
                                              JSObject* cmp_global) {
         auto global = global_create_with_existing(cx, cmp_global, &klass);
 
+        if (global) {
+            setup_global(cx, global);
+        }
+
         return global;
     }
 
@@ -331,6 +349,11 @@ class GjsGlobal {
     static bool define_properties(JSContext* cx, JS::HandleObject global,
                                   const char* realm_name,
                                   const char* bootstrap_script) {
+        gjs_set_global_slot(global, GjsGlobalSlot::ES_MODULE_REGISTRY,
+                            JS::PrivateValue(new GjsModuleRegistry()));
+        gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_MODULE_REGISTRY,
+                            JS::PrivateValue(new GjsModuleRegistry()));
+
         const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
         if (!JS_DefinePropertyById(cx, global, atoms.window(), global,
                                    JSPROP_READONLY | JSPROP_PERMANENT) ||
@@ -423,6 +446,102 @@ class GjsDebuggerGlobal {
     }
 };
 
+class GjsInternalGlobal {
+    static constexpr JSFunctionSpec static_funcs[] = {
+        JS_FN("getModuleUri", GetModuleUri, 1, 0),
+        JS_FN("compileAndEvalModule", CompileAndEvalModule, 1, 0),
+        JS_FN("debug", Debug, 1, 0),
+        JS_FN("lookupInternalModule", LookupInternalModule, 1, 0),
+        JS_FN("lookupModule", LookupModule, 1, 0),
+        JS_FN("registerModule", RegisterModule, 5, 0),
+        JS_FN("registerInternalModule", RegisterInternalModule, 5, 0),
+        JS_FN("setModuleResolveHook", SetModuleResolveHook, 1, 0),
+        JS_FS_END};
+
+    static constexpr JSClass klass = {
+        "GjsInternalGlobal",
+        JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(
+            static_cast<uint32_t>(GjsInternalGlobalSlot::LAST)),
+        &JS::DefaultGlobalClassOps,
+    };
+
+ public:
+    GJS_USE
+    static JSObject* create(JSContext* cx) {
+        return global_create_new(cx, &klass);
+    }
+
+    GJS_USE
+    static JSObject* create_with_compartment(JSContext* cx,
+                                             JSObject* cmp_global) {
+        return global_create_with_existing(cx, cmp_global, &klass);
+    }
+
+    static bool define_properties(JSContext* cx, JS::HandleObject global,
+                                  const char* realm_name,
+                                  const char* bootstrap_script G_GNUC_UNUSED) {
+        gjs_set_global_slot(global, GjsInternalGlobalSlot::SCRIPT_REGISTRY,
+                            JS::PrivateValue(new GjsModuleRegistry()));
+        gjs_set_global_slot(global, GjsInternalGlobalSlot::MODULE_REGISTRY,
+                            JS::PrivateValue(new GjsModuleRegistry()));
+
+        const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+
+        JS::Realm* realm = JS::GetObjectRealmOrNull(global);
+        g_assert(realm && "Global object must be associated with a realm");
+        // const_cast is allowed here if we never free the realm data
+        JS::SetRealmPrivate(realm, const_cast<char*>(realm_name));
+
+        if (!JS_DefineFunctions(cx, global, static_funcs)) {
+            return false;
+        }
+
+        // GI Modules
+
+        GError* error = nullptr;
+
+        if (!g_irepository_require(nullptr, "GObject", "2.0",
+                                   GIRepositoryLoadFlags(0), &error) ||
+            !g_irepository_require(nullptr, "GLib", "2.0",
+                                   GIRepositoryLoadFlags(0), &error) ||
+            !g_irepository_require(nullptr, "Gio", "2.0",
+                                   GIRepositoryLoadFlags(0), &error)) {
+            gjs_throw_gerror_message(cx, error);
+            g_error_free(error);
+            return false;
+        }
+
+        JS::RootedObject gobject(cx, gjs_create_ns(cx, "GObject"));
+        JS::RootedObject glib(cx, gjs_create_ns(cx, "GLib"));
+        JS::RootedObject gio(cx, gjs_create_ns(cx, "Gio"));
+        JS::RootedObject privateNS(cx, JS_NewPlainObject(cx));
+
+        if (!JS_DefinePropertyById(cx, global, atoms.private_ns_marker(),
+                                   privateNS, JSPROP_PERMANENT) ||
+            !JS_DefinePropertyById(cx, global, atoms.gobject(), glib,
+                                   JSPROP_PERMANENT) ||
+            !JS_DefinePropertyById(cx, global, atoms.glib(), glib,
+                                   JSPROP_PERMANENT) ||
+            !JS_DefinePropertyById(cx, global, atoms.gio(), gio,
+                                   JSPROP_PERMANENT)) {
+            return false;
+        }
+
+        // Native Modules
+
+        JS::RootedObject byteArray(cx, JS_NewPlainObject(cx));
+
+        if (!gjs_load_native_module(cx, "_byteArrayNative", &byteArray) ||
+            !JS_DefineProperty(cx, global, "ByteUtils", byteArray,
+                               JSPROP_PERMANENT)) {
+            gjs_throw(cx, "Failed to define byteArray functions.");
+            return false;
+        }
+
+        return true;
+    }
+};
+
 /**
  * gjs_create_global_object:
  * @cx: a #JSContext
@@ -441,6 +560,9 @@ JSObject* gjs_create_global_object(JSContext* cx, GjsGlobalType global_type,
             case GjsGlobalType::DEBUGGER:
                 return GjsDebuggerGlobal::create_with_compartment(
                     cx, current_global);
+            case GjsGlobalType::INTERNAL:
+                return GjsInternalGlobal::create_with_compartment(
+                    cx, current_global);
             default:
                 return nullptr;
         }
@@ -451,14 +573,14 @@ JSObject* gjs_create_global_object(JSContext* cx, GjsGlobalType global_type,
             return GjsGlobal::create(cx);
         case GjsGlobalType::DEBUGGER:
             return GjsDebuggerGlobal::create(cx);
+        case GjsGlobalType::INTERNAL:
+            return GjsInternalGlobal::create(cx);
         default:
             return nullptr;
     }
 }
 
-GjsGlobalType gjs_global_get_type(JSContext* cx) {
-    auto global = JS::CurrentGlobalOrNull(cx);
-
+GjsGlobalType gjs_global_get_type(JSObject* global) {
     g_assert(global && "gjs_global_get_type called when no global is present");
 
     auto global_type = gjs_get_global_slot(global, GjsGlobalSlot::GLOBAL_TYPE);
@@ -469,13 +591,14 @@ GjsGlobalType gjs_global_get_type(JSContext* cx) {
     return static_cast<GjsGlobalType>(global_type.toInt32());
 }
 
-GjsGlobalType gjs_global_get_type(JSObject* global) {
+bool gjs_global_is_type(JSContext* context, GjsGlobalType type) {
+    auto global = JS::CurrentGlobalOrNull(context);
     auto global_type = gjs_get_global_slot(global, GjsGlobalSlot::GLOBAL_TYPE);
 
     g_assert(global_type.isInt32() &&
              "Invalid type for GLOBAL_TYPE slot. Expected int32.");
 
-    return static_cast<GjsGlobalType>(global_type.toInt32());
+    return type == static_cast<GjsGlobalType>(global_type.toInt32());
 }
 
 /**
@@ -518,6 +641,9 @@ bool gjs_define_global_properties(JSContext* cx, JS::HandleObject global,
         case GjsGlobalType::DEBUGGER:
             return GjsDebuggerGlobal::define_properties(cx, global, realm_name,
                                                         bootstrap_script);
+        case GjsGlobalType::INTERNAL:
+            return GjsInternalGlobal::define_properties(cx, global, realm_name,
+                                                        bootstrap_script);
     }
 
     return false;
@@ -530,6 +656,8 @@ void gjs_set_global_slot(JSObject* global, GlobalSlot slot, JS::Value value) {
 }
 template void gjs_set_global_slot(JSObject* global, GjsGlobalSlot slot,
                                   JS::Value value);
+template void gjs_set_global_slot(JSObject* global, GjsInternalGlobalSlot slot,
+                                  JS::Value value);
 
 template <typename GlobalSlot>
 JS::Value gjs_get_global_slot(JSObject* global, GlobalSlot slot) {
@@ -537,6 +665,8 @@ JS::Value gjs_get_global_slot(JSObject* global, GlobalSlot slot) {
         global, JSCLASS_GLOBAL_SLOT_COUNT + static_cast<uint32_t>(slot));
 }
 template JS::Value gjs_get_global_slot(JSObject* global, GjsGlobalSlot slot);
+template JS::Value gjs_get_global_slot(JSObject* global,
+                                       GjsInternalGlobalSlot slot);
 
 decltype(GjsGlobal::klass) constexpr GjsGlobal::klass;
 decltype(GjsGlobal::static_funcs) constexpr GjsGlobal::static_funcs;
@@ -544,3 +674,7 @@ decltype(GjsGlobal::static_funcs) constexpr GjsGlobal::static_funcs;
 decltype(GjsDebuggerGlobal::klass) constexpr GjsDebuggerGlobal::klass;
 decltype(
     GjsDebuggerGlobal::static_funcs) constexpr GjsDebuggerGlobal::static_funcs;
+
+decltype(GjsInternalGlobal::klass) constexpr GjsInternalGlobal::klass;
+decltype(
+    GjsInternalGlobal::static_funcs) constexpr GjsInternalGlobal::static_funcs;
diff --git a/gjs/global.h b/gjs/global.h
index 41df240d..88842c93 100644
--- a/gjs/global.h
+++ b/gjs/global.h
@@ -66,7 +66,14 @@ enum class GjsGlobalSlot : uint32_t {
     LAST,
 };
 
-GjsGlobalType gjs_global_get_type(JSContext* cx);
+enum class GjsInternalGlobalSlot : uint32_t {
+    MODULE_REGISTRY = static_cast<uint32_t>(GjsGlobalSlot::LAST),
+    SCRIPT_REGISTRY,
+    IMPORT_HOOK,
+    LAST
+};
+
+bool gjs_global_is_type(JSContext* cx, GjsGlobalType type);
 GjsGlobalType gjs_global_get_type(JSObject* global);
 
 GJS_JSAPI_RETURN_CONVENTION
diff --git a/gjs/importer.cpp b/gjs/importer.cpp
index d294301c..6be101fa 100644
--- a/gjs/importer.cpp
+++ b/gjs/importer.cpp
@@ -50,6 +50,7 @@
 #include <js/Utility.h>  // for UniqueChars
 #include <js/Value.h>
 #include <jsapi.h>    // for JS_DefinePropertyById, JS_DefineP...
+#include <jsfriendapi.h>
 #include <jspubtd.h>  // for JSProto_Error
 #include <mozilla/UniquePtr.h>
 #include <mozilla/Vector.h>
@@ -300,10 +301,24 @@ gjs_import_native_module(JSContext       *cx,
 {
     gjs_debug(GJS_DEBUG_IMPORTER, "Importing '%s'", parse_name);
 
+    auto native_registry = gjs_get_native_module_registry(cx);
+    auto it = native_registry->lookupForAdd(parse_name);
+
     JS::RootedObject module(cx);
+
+    if (it.found()) {
+        module.set(it->value().get());
+        return define_meta_properties(cx, module, nullptr, parse_name,
+                                      importer) &&
+               JS_DefineProperty(cx, importer, parse_name, module,
+                                 GJS_MODULE_PROP_FLAGS);
+    }
+
     return gjs_load_native_module(cx, parse_name, &module) &&
+           native_registry->put(parse_name, module) &&
            define_meta_properties(cx, module, nullptr, parse_name, importer) &&
-           JS_DefineProperty(cx, importer, parse_name, module, GJS_MODULE_PROP_FLAGS);
+           JS_DefineProperty(cx, importer, parse_name, module,
+                             GJS_MODULE_PROP_FLAGS);
 }
 
 GJS_JSAPI_RETURN_CONVENTION
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
new file mode 100644
index 00000000..e5c7ed7c
--- /dev/null
+++ b/gjs/internal.cpp
@@ -0,0 +1,450 @@
+/*
+ * Copyright (c) 2020 Evan Welsh <contact evanwelsh com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#include "gjs/internal.h"
+
+#include <config.h>
+#include <gio/gio.h>
+#include <girepository.h>
+#include <glib-object.h>
+#include <glib.h>
+#include <js/Class.h>
+#include <js/CompilationAndEvaluation.h>
+#include <js/CompileOptions.h>
+#include <js/Conversions.h>
+#include <js/GCVector.h>  // for RootedVector
+#include <js/Promise.h>
+#include <js/PropertyDescriptor.h>
+#include <js/RootingAPI.h>
+#include <js/SourceText.h>
+#include <js/TypeDecls.h>
+#include <jsapi.h>      // for JS_DefinePropertyById, ...
+#include <stddef.h>     // for size_t
+#include <sys/types.h>  // for ssize_t
+
+#include <codecvt>  // for codecvt_utf8_utf16
+#include <locale>   // for wstring_convert
+#include <string>   // for u16string
+#include <vector>
+
+#include "gjs/context-private.h"
+#include "gjs/context.h"
+#include "gjs/error-types.h"
+#include "gjs/global.h"
+#include "gjs/importer.h"
+#include "gjs/jsapi-util.h"
+#include "gjs/mem-private.h"
+#include "gjs/module.h"
+#include "gjs/native.h"
+#include "util/log.h"
+
+// You have to be very careful in this file to only do operations within the
+// correct global!
+using AutoGFile = GjsAutoUnref<GFile>;
+
+bool gjs_load_internal_script(JSContext* cx, const char* identifier) {
+    GjsAutoChar full_path(
+        g_strdup_printf("resource://org/gnome/gjs/gjs/%s.js", identifier));
+    AutoGFile gfile(g_file_new_for_uri(full_path));
+
+    char* script_text_raw;
+    gsize script_text_len;
+    GError* error = nullptr;
+
+    if (!g_file_load_contents(gfile, NULL, &script_text_raw, &script_text_len,
+                              nullptr, &error)) {
+        gjs_throw(cx, "Failed to read internal resource: %s \n%s",
+                  full_path.get(), error->message);
+        return false;
+    }
+
+    GjsAutoChar script_text(script_text_raw);
+
+    JS::CompileOptions options(cx);
+    options.setIntroductionType("Internal Script Loader");
+    options.setFileAndLine(full_path, 1);
+    options.setSelfHostingMode(false);
+
+    std::u16string utf16_string =
+        gjs_utf8_script_to_utf16(script_text, script_text_len);
+    // COMPAT: This could use JS::SourceText<mozilla::Utf8Unit> directly,
+    // but that messes up code coverage. See bug
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=1404784
+    JS::SourceText<char16_t> buf;
+    if (!buf.init(cx, utf16_string.c_str(), utf16_string.size(),
+                  JS::SourceOwnership::Borrowed))
+        return false;
+
+    JS::RootedObject internal_global(cx, gjs_get_internal_global(cx));
+
+    JSAutoRealm ar(cx, internal_global);
+    JS::RootedValue ignored_retval(cx);
+    JS::RootedObject module(cx, JS_NewPlainObject(cx));
+    JS::RootedObjectVector scope_chain(cx);
+
+    if (!scope_chain.append(module)) {
+        JS_ReportOutOfMemory(cx);
+        return false;
+    }
+
+    auto success = JS::Evaluate(cx, scope_chain, options, buf, &ignored_retval);
+
+    if (!success) {
+        gjs_log_exception(cx);
+        return false;
+    }
+    auto add = gjs_get_internal_script_registry(cx)->lookupForAdd(identifier);
+
+    if (add.found()) {
+        gjs_throw(cx, "Internal script %s already loaded.", identifier);
+        return false;
+    }
+
+    GjsAutoChar iden(g_strdup(identifier));
+
+    if (!gjs_get_internal_script_registry(cx)->add(add, iden.get(), module)) {
+        JS_ReportOutOfMemory(cx);
+        return false;
+    }
+    return true;
+}
+
+bool GetModuleUri(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args.requireAtLeast(cx, "getModuleUri", 1)) {
+        return false;
+    }
+
+    JS::RootedValue importer(cx, args[0]);
+
+    if (importer.isUndefined()) {
+        gjs_throw(cx,
+                  "Cannot import from relative path when module path "
+                  "is unknown.");
+        return false;
+    }
+    // The module from which the resolve request is coming
+    GjsESModule* priv_module = static_cast<GjsESModule*>(importer.toPrivate());
+    // Get the module's path.
+    auto module_location = priv_module->uri();
+    // Get the module's directory.
+    const gchar* module_file_location = module_location.c_str();
+    return gjs_string_from_utf8(cx, module_file_location, args.rval());
+}
+
+bool SetModuleResolveHook(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args.requireAtLeast(cx, "setModuleResolveHook", 1)) {
+        return false;
+    }
+
+    JS::RootedValue mv(cx, args[0]);
+
+    // The hook is stored in the internal global.
+    JS::RootedObject global(cx, gjs_get_internal_global(cx));
+    gjs_set_global_slot(global, GjsInternalGlobalSlot::IMPORT_HOOK, mv);
+
+    args.rval().setUndefined();
+    return true;
+}
+
+bool CompileAndEvalModule(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!args.requireAtLeast(cx, "compileAndEvalModule", 1)) {
+        return false;
+    }
+
+    JS::RootedString s1(cx, args[0].toString());
+
+    JS::UniqueChars id = JS_EncodeStringToUTF8(cx, s1);
+
+    {
+        JSAutoRealm ar(cx, gjs_get_import_global(cx));
+        auto registry = gjs_get_esm_registry(cx);
+        auto result = registry->lookup(id.get());
+        if (result) {
+            JS::RootedObject res(cx, result->value());
+            auto init = JS::ModuleInstantiate(cx, res);
+            if (!init) {
+                gjs_log_exception(cx);
+            }
+
+            auto eval = JS::ModuleEvaluate(cx, res);
+
+            if (!eval) {
+                gjs_log_exception(cx);
+            }
+
+            args.rval().setBoolean(init && eval);
+        } else {
+            args.rval().setBoolean(false);
+        }
+    }
+
+    return true;
+}
+
+bool gjs_require_module(JSContext* m_cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
+
+    if (argc != 1) {
+        gjs_throw(m_cx, "Must pass a single argument to require()");
+        return false;
+    }
+
+    JS::AutoSaveExceptionState exc_state(m_cx);
+    JS::RootedString jstr(m_cx, JS::ToString(m_cx, argv[0]));
+    exc_state.restore();
+
+    if (!jstr) {
+        g_message("JS LOG: <cannot convert value to string>");
+        return true;
+    }
+
+    JS::UniqueChars id(JS_EncodeStringToUTF8(m_cx, jstr));
+
+    if (!id) {
+        gjs_throw(m_cx, "Invalid native id.");
+        return false;
+    }
+
+    auto native_registry = gjs_get_native_module_registry(m_cx);
+
+    auto nativeModuleAdd = native_registry->lookupForAdd(id.get());
+
+    if (nativeModuleAdd.found()) {
+        JS::RootedObject obj(m_cx, nativeModuleAdd->value().get());
+
+        argv.rval().setObject(*obj);
+        return true;
+    }
+
+    JS::RootedObject native_obj(m_cx);
+
+    if (!gjs_load_native_module(m_cx, id.get(), &native_obj)) {
+        gjs_throw(m_cx, "Failed to load native module: %s", id.get());
+        return false;
+    }
+
+    if (!native_registry->add(nativeModuleAdd, id.get(), native_obj)) {
+        JS_ReportOutOfMemory(m_cx);
+        return false;
+    }
+
+    argv.rval().setObject(*native_obj);
+    return true;
+}
+
+static bool register_module(JSContext* cx, const char* identifier,
+                            const char* path, const char* text, size_t length,
+                            bool* success) {
+    auto esm_registry = gjs_get_esm_registry(cx);
+
+    auto it = esm_registry->lookupForAdd(path);
+
+    if (it.found()) {
+        gjs_throw(cx, "Module '%s' already registered", path);
+        return false;
+    }
+
+    auto module = new GjsESModule(identifier, path);
+
+    JS::RootedObject module_record(cx, module->compile(cx, text, length));
+
+    if (module_record && !esm_registry->add(it, identifier, module_record)) {
+        JS_ReportOutOfMemory(cx);
+        return false;
+    }
+
+    *success = module_record;
+
+    return true;
+}
+
+// registerModule(id: string, path: string, text: string, length: number,
+// unused: boolean)
+bool RegisterModule(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!args.requireAtLeast(cx, "registerModule", 5)) {
+        return false;
+    }
+
+    JS::RootedString str0(cx, args[0].toString()),  // id
+        str1(cx, args[1].toString()),               // path
+        str2(cx, args[2].toString());               // text
+
+    JS::UniqueChars id = JS_EncodeStringToUTF8(cx, str0);
+    JS::UniqueChars path = JS_EncodeStringToUTF8(cx, str1);
+    JS::UniqueChars text = JS_EncodeStringToUTF8(cx, str2);
+    auto length = args[3].toInt32();
+
+    {
+        JSAutoRealm ar(cx, gjs_get_import_global(cx));
+
+        bool success = false;
+        bool result = register_module(cx, id.get(), path.get(), text.get(),
+                                      length, &success);
+
+        args.rval().setBoolean(success);
+
+        return result;
+    }
+}
+
+static bool register_internal_module(JSContext* cx, const char* identifier,
+                                     const char* filename, const char* module,
+                                     size_t module_len, bool* success) {
+    auto internal_registry = gjs_get_internal_module_registry(cx);
+    auto it = internal_registry->lookupForAdd(identifier);
+
+    if (it.found()) {
+        gjs_throw(cx, "Internal module '%s' is already registered", identifier);
+        return false;
+    }
+
+    auto internal_module = new GjsESModule(identifier, filename, true);
+
+    JS::RootedObject module_record(
+        cx, internal_module->compile(cx, module, module_len));
+
+    if (module_record &&
+        !internal_registry->add(it, identifier, module_record)) {
+        JS_ReportOutOfMemory(cx);
+        return false;
+    }
+
+    *success = module_record;
+
+    return true;
+}
+
+// registerInternalModule(id: string, path: string, text: string, length:
+// number)
+bool RegisterInternalModule(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!args.requireAtLeast(cx, "registerInternalModule", 4)) {
+        return false;
+    }
+
+    JS::RootedString str0(cx, args[0].toString()),  // id
+        str1(cx, args[1].toString()),               // path
+        str2(cx, args[2].toString());               // text
+
+    JS::UniqueChars id = JS_EncodeStringToUTF8(cx, str0);
+    JS::UniqueChars path = JS_EncodeStringToUTF8(cx, str1);
+    JS::UniqueChars text = JS_EncodeStringToUTF8(cx, str2);
+    auto length = args[3].toInt32();
+
+    {
+        JSAutoRealm ar(cx, gjs_get_import_global(cx));
+
+        bool success = false;
+        bool result = register_internal_module(cx, id.get(), path.get(),
+                                               text.get(), length, &success);
+        args.rval().setBoolean(success);
+
+        return result;
+    }
+}
+
+// lookupInternalModule(id: string)
+bool LookupInternalModule(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!args.requireAtLeast(cx, "lookupInternalModule", 1)) {
+        return false;
+    }
+
+    JS::RootedString s1(cx, args[0].toString());
+
+    JS::UniqueChars id = JS_EncodeStringToUTF8(cx, s1);
+
+    {
+        JSAutoRealm ar(cx, gjs_get_import_global(cx));
+        auto registry = gjs_get_internal_module_registry(cx);
+        auto it = registry->lookup(id.get());
+
+        if (!it.found()) {
+            args.rval().setNull();
+            return true;
+        }
+        JS::RootedObject lookup(cx, it->value());
+        if (!lookup) {
+            args.rval().setNull();
+        } else {
+            args.rval().setObject(*lookup.get());
+        }
+    }
+    return true;
+}
+
+// lookupModule(id: string)
+bool LookupModule(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!args.requireAtLeast(cx, "lookupModule", 1)) {
+        return false;
+    }
+
+    JS::RootedString s1(cx, args[0].toString());
+
+    JS::UniqueChars id = JS_EncodeStringToUTF8(cx, s1);
+
+    {
+        JSAutoRealm ar(cx, gjs_get_import_global(cx));
+        auto registry = gjs_get_esm_registry(cx);
+        auto it = registry->lookup(id.get());
+
+        if (!it.found()) {
+            args.rval().setNull();
+            return true;
+        }
+        JS::RootedObject lookup(cx, it->value());
+
+        if (!lookup) {
+            args.rval().setNull();
+        } else {
+            args.rval().setObject(*lookup.get());
+        }
+    }
+    return true;
+}
+
+// debug(msg: string)
+bool Debug(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!args.requireAtLeast(cx, "debug", 1)) {
+        return false;
+    }
+
+    JS::RootedString s1(cx, args[0].toString());
+
+    JS::UniqueChars id = JS_EncodeStringToUTF8(cx, s1);
+
+    gjs_debug(GJS_DEBUG_IMPORTER, id.get());
+
+    return true;
+}
diff --git a/gjs/internal.h b/gjs/internal.h
new file mode 100644
index 00000000..98aa7f8b
--- /dev/null
+++ b/gjs/internal.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2020 Evan Welsh <contact evanwelsh com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#ifndef GJS_INTERNAL_H_
+#define GJS_INTERNAL_H_
+
+#include <config.h>
+#include <gio/gio.h>
+#include <js/GCHashTable.h>
+#include <js/GCVector.h>
+#include <js/TypeDecls.h>
+#include <jsapi.h>        // for JS_GetContextPrivate
+#include <jsfriendapi.h>  // for ScriptEnvironmentPreparer
+
+#include <string>
+
+#include "gjs/macros.h"
+
+bool gjs_load_internal_script(JSContext* cx, const char* identifier);
+
+// setModuleResolveHook
+bool SetModuleResolveHook(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// compileAndEvalModule(id: string)
+bool CompileAndEvalModule(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// registerModule(id: string, path: string, text: string, length: number, ?:
+// boolean)
+bool RegisterModule(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// registerInternalModule(id: string, path: string, text: string, length:
+// number, is_legacy: boolean)
+bool RegisterInternalModule(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// lookupInternalModule(id: string)
+bool LookupInternalModule(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// lookupModule(id: string)
+bool LookupModule(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// debug(msg: string)
+bool Debug(JSContext* cx, unsigned argc, JS::Value* vp);
+
+// getModuleUri(module): string
+bool GetModuleUri(JSContext* cx, unsigned argc, JS::Value* vp);
+
+#endif  // GJS_INTERNAL_H_
diff --git a/gjs/jsapi-util.cpp b/gjs/jsapi-util.cpp
index 4d7193b2..e997bdb7 100644
--- a/gjs/jsapi-util.cpp
+++ b/gjs/jsapi-util.cpp
@@ -657,6 +657,10 @@ JSObject* gjs_get_import_global(JSContext* cx) {
     return GjsContextPrivate::from_cx(cx)->global();
 }
 
+JSObject* gjs_get_internal_global(JSContext* cx) {
+    return GjsContextPrivate::from_cx(cx)->internal_global();
+}
+
 #if defined(G_OS_WIN32) && (defined(_MSC_VER) && (_MSC_VER >= 1900))
 /* Unfortunately Visual Studio's C++ .lib somehow did not contain the right
  * codecvt stuff that we need to convert from utf8 to utf16 (char16_t), so we
diff --git a/gjs/jsapi-util.h b/gjs/jsapi-util.h
index a4bee3bc..91bf08da 100644
--- a/gjs/jsapi-util.h
+++ b/gjs/jsapi-util.h
@@ -212,6 +212,9 @@ struct GCPolicy<GjsAutoParam> : public IgnoreGCPolicy<GjsAutoParam> {};
 GJS_USE
 JSObject*   gjs_get_import_global            (JSContext       *context);
 
+GJS_USE
+JSObject* gjs_get_internal_global(JSContext* context);
+
 void gjs_throw_constructor_error             (JSContext       *context);
 
 void gjs_throw_abstract_constructor_error(JSContext    *context,
diff --git a/gjs/module.cpp b/gjs/module.cpp
index 6942ee46..bbe21afd 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -1,6 +1,7 @@
 /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
 /*
  * Copyright (c) 2017  Philip Chimento <philip chimento gmail com>
+ * Copyright (c) 2020  Evan Welsh <contact evanwelsh com>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to
@@ -26,15 +27,22 @@
 #include <stddef.h>     // for size_t
 #include <sys/types.h>  // for ssize_t
 
+#include <codecvt>  // for codecvt_utf8_utf16
+#include <locale>   // for wstring_convert
 #include <string>  // for u16string
+#include <vector>
 
 #include <gio/gio.h>
+#include <girepository.h>
+#include <glib-object.h>
 #include <glib.h>
 
 #include <js/Class.h>
 #include <js/CompilationAndEvaluation.h>
 #include <js/CompileOptions.h>
+#include <js/Conversions.h>
 #include <js/GCVector.h>  // for RootedVector
+#include <js/Promise.h>
 #include <js/PropertyDescriptor.h>
 #include <js/RootingAPI.h>
 #include <js/SourceText.h>
@@ -42,15 +50,21 @@
 #include <jsapi.h>  // for JS_DefinePropertyById, ...
 
 #include "gjs/context-private.h"
+#include "gjs/error-types.h"
+#include "gjs/global.h"
+#include "gjs/importer.h"
 #include "gjs/jsapi-util.h"
 #include "gjs/mem-private.h"
 #include "gjs/module.h"
+#include "gjs/native.h"
 #include "util/log.h"
 
+using AutoGFile = GjsAutoUnref<GFile>;
+
 class GjsScriptModule {
     char *m_name;
 
-    GjsScriptModule(const char* name) {
+    explicit GjsScriptModule(const char* name) {
         m_name = g_strdup(name);
         GJS_INC_COUNTER(module);
     }
@@ -240,6 +254,30 @@ class GjsScriptModule {
     }
 };
 
+JSObject* GjsESModule::compile(JSContext* m_cx, const char* mod_text,
+                               size_t mod_len) {
+    JS::CompileOptions options(m_cx);
+    options.setFileAndLine(m_uri.c_str(), 1).setSourceIsLazy(false);
+
+    std::u16string utf16_string(gjs_utf8_script_to_utf16(mod_text, mod_len));
+
+    JS::SourceText<char16_t> buf;
+    if (!buf.init(m_cx, utf16_string.c_str(), utf16_string.size(),
+                  JS::SourceOwnership::Borrowed))
+        return nullptr;
+
+    JS::RootedObject new_module(m_cx);
+
+    if (!JS::CompileModule(m_cx, options, buf, &new_module)) {
+        gjs_log_exception(m_cx);
+        return nullptr;
+    }
+
+    JS::SetModulePrivate(new_module, JS::PrivateValue(this));
+
+    return new_module;
+}
+
 /**
  * gjs_module_import:
  * @cx: the JS context
@@ -268,3 +306,114 @@ gjs_module_import(JSContext       *cx,
 
 decltype(GjsScriptModule::klass) constexpr GjsScriptModule::klass;
 decltype(GjsScriptModule::class_ops) constexpr GjsScriptModule::class_ops;
+
+GjsModuleRegistry* gjs_get_native_module_registry(JSContext* js_context) {
+    auto global = gjs_get_import_global(js_context);
+    auto native_registry =
+        gjs_get_global_slot(global, GjsGlobalSlot::NATIVE_MODULE_REGISTRY);
+
+    return static_cast<GjsModuleRegistry*>(native_registry.toPrivate());
+}
+
+GjsModuleRegistry* gjs_get_esm_registry(JSContext* js_context) {
+    auto global = gjs_get_import_global(js_context);
+    auto esm_registry =
+        gjs_get_global_slot(global, GjsGlobalSlot::ES_MODULE_REGISTRY);
+
+    return static_cast<GjsModuleRegistry*>(esm_registry.toPrivate());
+}
+
+GjsModuleRegistry* gjs_get_internal_script_registry(JSContext* js_context) {
+    auto global = gjs_get_internal_global(js_context);
+    auto script_registry =
+        gjs_get_global_slot(global, GjsInternalGlobalSlot::SCRIPT_REGISTRY);
+
+    return static_cast<GjsModuleRegistry*>(script_registry.toPrivate());
+}
+
+GjsModuleRegistry* gjs_get_internal_module_registry(JSContext* js_context) {
+    auto global = gjs_get_internal_global(js_context);
+    auto script_registry =
+        gjs_get_global_slot(global, GjsInternalGlobalSlot::MODULE_REGISTRY);
+
+    return static_cast<GjsModuleRegistry*>(script_registry.toPrivate());
+}
+
+static bool populate_module_meta(JSContext* m_cx,
+                                 JS::Handle<JS::Value> private_ref,
+                                 JS::Handle<JSObject*> meta_object_handle) {
+    JS::RootedObject meta_object(m_cx, meta_object_handle);
+    JS::RootedValue uri_val(m_cx, JS::UndefinedValue());
+    bool allow_require = true;
+
+    if (!private_ref.isUndefined()) {
+        GjsESModule* module =
+            static_cast<GjsESModule*>(private_ref.toPrivate());
+
+        auto uri = module->uri();
+
+        allow_require = module->isInternal();
+
+        JS::Rooted<JSString*> uri_str(m_cx,
+                                      JS_NewStringCopyZ(m_cx, uri.c_str()));
+
+        if (!uri_str) {
+            JS_ReportOutOfMemory(m_cx);
+            return false;
+        }
+
+        uri_val.setString(uri_str);
+    }
+
+    if (!JS_DefineProperty(m_cx, meta_object, "url", uri_val,
+                           JSPROP_ENUMERATE)) {
+        gjs_throw(m_cx, "Could not define import.meta.url");
+        return false;
+    }
+
+    if (allow_require) {
+        if (!JS_DefineFunction(m_cx, meta_object, "require", gjs_require_module,
+                               1, GJS_MODULE_PROP_FLAGS)) {
+            gjs_throw(m_cx, "Could not define require!");
+            return false;
+        }
+    }
+
+    return true;
+}
+
+JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importer,
+                             JS::HandleString specifier) {
+    g_assert(gjs_global_is_type(cx, GjsGlobalType::DEFAULT) &&
+             "gjs_module_resolve can only be called from on the default "
+             "global.");
+
+    GjsContextPrivate* gjs_cx = GjsContextPrivate::from_cx(cx);
+
+    JS::RootedObject global(cx, gjs_cx->internal_global());
+    JSAutoRealm ar(cx, global);
+
+    JS::RootedValue hookValue(
+        cx, gjs_get_global_slot(global, GjsInternalGlobalSlot::IMPORT_HOOK));
+
+    JS::AutoValueArray<3> args(cx);
+    args[0].set(importer);
+    args[1].setString(specifier);
+
+    JS::RootedValue result(cx);
+
+    if (!JS_CallFunctionValue(cx, nullptr, hookValue, args, &result)) {
+        gjs_log_exception(cx);
+        return nullptr;
+    }
+
+    JS::RootedObject module(cx, result.toObjectOrNull());
+
+    return module;
+}
+
+bool gjs_populate_module_meta(JSContext* m_cx,
+                              JS::Handle<JS::Value> private_ref,
+                              JS::Handle<JSObject*> meta_object) {
+    return populate_module_meta(m_cx, private_ref, meta_object);
+}
diff --git a/gjs/module.h b/gjs/module.h
index c63d6852..a61bc470 100644
--- a/gjs/module.h
+++ b/gjs/module.h
@@ -1,6 +1,7 @@
 /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
 /*
  * Copyright (c) 2017  Philip Chimento <philip chimento gmail com>
+ * Copyright (c) 2020  Evan Welsh <contact evanwelsh com>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to
@@ -28,10 +29,42 @@
 
 #include <gio/gio.h>
 
+#include <js/GCHashTable.h>
+#include <js/GCVector.h>
 #include <js/TypeDecls.h>
+#include <jsapi.h>        // for JS_GetContextPrivate
+#include <jsfriendapi.h>  // for ScriptEnvironmentPreparer
+
+#include <string>
 
 #include "gjs/macros.h"
 
+class CppStringHashPolicy {
+ public:
+    typedef std::string Lookup;
+
+    static js::HashNumber hash(const Lookup& l) {
+        return std::hash<std::string>{}(std::string(l));
+    }
+
+    static bool match(const std::string& k, const Lookup& l) {
+        return k.compare(l) == 0;
+    }
+
+    static void rekey(std::string* k, const std::string& newKey) {
+        *k = newKey;
+    }
+};
+
+namespace JS {
+template <>
+struct GCPolicy<std::string> : public IgnoreGCPolicy<std::string> {};
+}  // namespace JS
+
+using GjsModuleRegistry =
+    JS::GCHashMap<std::string, JS::Heap<JSObject*>, CppStringHashPolicy,
+                  js::SystemAllocPolicy>;
+
 GJS_JSAPI_RETURN_CONVENTION
 JSObject *
 gjs_module_import(JSContext       *cx,
@@ -40,4 +73,47 @@ gjs_module_import(JSContext       *cx,
                   const char      *name,
                   GFile           *file);
 
+class GjsESModule {
+    std::string m_identifier;
+    std::string m_uri;
+    bool m_is_internal;
+
+ public:
+    GjsESModule(std::string module_identifier, std::string module_uri,
+                bool is_internal) {
+        m_is_internal = is_internal;
+        m_uri = module_uri;
+        m_identifier = module_identifier;
+    }
+
+    GjsESModule(std::string module_identifier, std::string module_uri)
+        : GjsESModule(module_identifier, module_uri, false) {}
+
+    void setUri(std::string uri) { m_uri = uri; }
+
+    std::string uri() { return m_uri; }
+
+    std::string identifier() { return m_identifier; }
+
+    bool isInternal() { return m_is_internal; }
+
+    GJS_JSAPI_RETURN_CONVENTION
+    JSObject* compile(JSContext* cx, const char* mod_text, size_t mod_len);
+};
+
+bool gjs_require_module(JSContext* js_context, unsigned argc, JS::Value* vp);
+
+GjsModuleRegistry* gjs_get_native_module_registry(JSContext* js_context);
+GjsModuleRegistry* gjs_get_esm_registry(JSContext* js_context);
+GjsModuleRegistry* gjs_get_internal_module_registry(JSContext* js_context);
+GjsModuleRegistry* gjs_get_internal_script_registry(JSContext* js_context);
+
+GJS_JSAPI_RETURN_CONVENTION
+JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue mod_val,
+                             JS::HandleString specifier);
+
+bool gjs_populate_module_meta(JSContext* m_cx,
+                              JS::Handle<JS::Value> private_ref,
+                              JS::Handle<JSObject*> meta_object);
+
 #endif  // GJS_MODULE_H_
diff --git a/gjs/module.js b/gjs/module.js
new file mode 100644
index 00000000..ff44ff41
--- /dev/null
+++ b/gjs/module.js
@@ -0,0 +1,218 @@
+/*
+ * Copyright (c) 2020 Evan Welsh <contact evanwelsh com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/* global debug */
+
+// NOTE: Gio, GLib, and GObject are *raw* and have no overrides.
+
+function isValidUri(uri) {
+    const uri_scheme = GLib.uri_parse_scheme(uri);
+
+    // Handle relative imports from URI-based modules.
+    if (!uri_scheme)
+        return false;
+
+    // Only allow imports from file:// and resource:// URIs
+    return uri_scheme === 'file' || uri_scheme === 'resource';
+}
+
+function isRelativePath(id) {
+    // Check if the path is relative.
+    return id.startsWith('./') || id.startsWith('../');
+}
+
+function resolveAbsolutePath(id) {
+    const output = Gio.File.new_for_path(id);
+
+    return {output, full_path: output.get_path()};
+}
+
+function isAbsolutePath(id) {
+    return id.startsWith('/');
+}
+
+function resolveURI(id) {
+    const output = Gio.File.new_for_uri(id);
+
+    return {output, full_path: output.get_uri()};
+}
+
+function resolveRelativePath(id, module_uri) {
+    // If a module has a path, we'll have stored it in the host field
+    let full_path;
+    let output;
+
+    if (!module_uri)
+        throw new Error('Cannot import from relative path when module path is unknown.');
+
+
+    debug(`module_uri: ${module_uri}`);
+
+    const uri_scheme = GLib.uri_parse_scheme(module_uri);
+
+    // Handle relative imports from URI-based modules.
+    if (uri_scheme) {
+        // Only allow relative imports from file:// and resource:// URIs
+        if (isValidUri(uri_scheme)) {
+            let module_file = Gio.File.new_for_uri(module_uri);
+            let module_parent_file = module_file.get_parent();
+
+            output = module_parent_file.resolve_relative_path(id);
+            full_path = output.get_uri();
+        } else {
+            throw new Error(
+                'Relative imports can only occur from file:// and resource:// URIs');
+        }
+    } else {
+        // Get the module directory path.
+        const module_file = Gio.File.new_for_path(module_uri);
+        const module_parent_file = module_file.get_parent();
+
+        // Resolve file relative to the module directory.
+        output = module_parent_file.resolve_relative_path(id);
+        full_path = output.get_path();
+    }
+
+    return {output, full_path};
+}
+
+function resolveId(id, module_uri) {
+    // If a module has a path, we'll have stored it in the host field
+    let full_path = null;
+    let output = null;
+
+    if (isAbsolutePath(id))
+        ({output, full_path} = resolveAbsolutePath(id));
+    else if (isRelativePath(id))
+        ({output, full_path} = resolveRelativePath(id, module_uri));
+    else if (isValidUri(id))
+        ({output, full_path} = resolveURI(id));
+
+
+    if (!output || !full_path)
+        return null;
+
+
+    return {output, full_path};
+}
+
+function fromBytes(bytes) {
+    return ByteUtils.toString(bytes, 'utf-8');
+}
+
+function loadFileSync(output, full_path) {
+    try {
+        const [, bytes] = output.load_contents(null);
+        return fromBytes(bytes);
+    } catch (error) {
+        throw new Error(`Unable to load file from: ${full_path}`);
+    }
+}
+
+const ESM_MODULE_URI = 'resource:///org/gnome/gjs/modules/esm/';
+const CORE_MODULE_URI = 'resource:///org/gnome/gjs/modules/core/';
+
+function buildInternalPaths(id) {
+    const directory_uris = [ESM_MODULE_URI, CORE_MODULE_URI];
+
+    return directory_uris
+        .map(uri => {
+            const full_path = GLib.build_pathv('/', [uri, `${id}.js`]);
+
+            debug(`Built path ${full_path} with ${id} for ${uri}.`);
+
+            return {uri, full_path, file: Gio.File.new_for_uri(full_path)};
+        });
+}
+
+function resolveModule(id, module_uri) {
+    // Check if the module has already been loaded
+    //
+    // Order:
+    // - Local imports
+    // - Internal imports
+
+    debug(`Resolving: ${id}`);
+
+    let lookup_module = lookupModule(id);
+
+    if (lookup_module)
+        return lookup_module;
+
+
+    lookup_module = lookupInternalModule(id);
+
+    if (lookup_module)
+        return lookup_module;
+
+
+    // 1) Resolve path and URI-based imports.
+
+    const resolved = resolveId(id, module_uri);
+
+    if (resolved) {
+        const {output, full_path} = resolved;
+
+        debug(`Full path found: ${full_path}`);
+
+        lookup_module = lookupModule(full_path);
+
+        // Check if module is already loaded (relative handling)
+        if (lookup_module)
+            return lookup_module;
+
+
+        const text = loadFileSync(output);
+
+        if (!registerModule(full_path, full_path, text, text.length, false))
+            throw new Error(`Failed to register module: ${full_path}`);
+
+
+        return lookupModule(full_path);
+    }
+
+    // 2) Resolve internal imports.
+
+    const result = buildInternalPaths(id).find(({file}) => file && file.query_exists(null));
+
+    if (!result)
+        throw new Error(`Attempted to load unregistered global module: ${id}`);
+
+
+    const {full_path, file} = result;
+
+    const text = loadFileSync(file, full_path);
+
+    if (!registerInternalModule(id, full_path, text, text.length))
+        return null;
+
+
+    return lookupInternalModule(id);
+}
+
+setModuleResolveHook((referencingInfo, specifier) => {
+    debug('Starting module import...');
+    const uri = getModuleUri(referencingInfo);
+    debug(`Found base URI: ${uri}`);
+
+    return resolveModule(specifier, uri);
+});
diff --git a/installed-tests/esm-test.sh b/installed-tests/esm-test.sh
new file mode 100755
index 00000000..f19b3657
--- /dev/null
+++ b/installed-tests/esm-test.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+if test "$GJS_USE_UNINSTALLED_FILES" = "1"; then
+    gjs="$TOP_BUILDDIR/gjs-console"
+else
+    gjs=gjs-console
+fi
+
+echo 1..1
+
+JS_SCRIPT="$1"
+EXPECTED_OUTPUT="$1.output"
+THE_DIFF=$("$gjs" -m "$JS_SCRIPT" \
+    | diff -u "$EXPECTED_OUTPUT" -)
+if test $? -ne 0; then
+    echo "not ok 1 - $1  # command failed"
+    exit 1
+fi
+
+if test -n "$THE_DIFF"; then
+    echo "not ok 1 - $1"
+    echo "$THE_DIFF" | while read line; do echo "#$line"; done
+else
+    echo "ok 1 - $1"
+fi
diff --git a/installed-tests/esm.test.in b/installed-tests/esm.test.in
new file mode 100644
index 00000000..763091fd
--- /dev/null
+++ b/installed-tests/esm.test.in
@@ -0,0 +1,4 @@
+[Test]
+Type=session
+Exec=@installed_tests_execdir@/esm-test.sh @installed_tests_execdir@/esm/@name@
+Output=TAP
diff --git a/installed-tests/esm/default-import.js b/installed-tests/esm/default-import.js
new file mode 100644
index 00000000..d070eda2
--- /dev/null
+++ b/installed-tests/esm/default-import.js
@@ -0,0 +1,3 @@
+import $ from './exports.js';
+
+print($);
\ No newline at end of file
diff --git a/installed-tests/esm/default-import.js.output b/installed-tests/esm/default-import.js.output
new file mode 100644
index 00000000..7ed6ff82
--- /dev/null
+++ b/installed-tests/esm/default-import.js.output
@@ -0,0 +1 @@
+5
diff --git a/installed-tests/esm/exports.js b/installed-tests/esm/exports.js
new file mode 100644
index 00000000..c846291d
--- /dev/null
+++ b/installed-tests/esm/exports.js
@@ -0,0 +1,3 @@
+export default 5;
+
+export const NamedExport = 'Hello, World';
diff --git a/installed-tests/esm/gi-import.js b/installed-tests/esm/gi-import.js
new file mode 100644
index 00000000..734b1658
--- /dev/null
+++ b/installed-tests/esm/gi-import.js
@@ -0,0 +1,3 @@
+import gi from 'gi';
+
+print(gi.require('GObject'));
diff --git a/installed-tests/esm/gi-import.js.output b/installed-tests/esm/gi-import.js.output
new file mode 100644
index 00000000..12a2890d
--- /dev/null
+++ b/installed-tests/esm/gi-import.js.output
@@ -0,0 +1 @@
+[object GIRepositoryNamespace]
diff --git a/installed-tests/esm/named-import.js b/installed-tests/esm/named-import.js
new file mode 100644
index 00000000..0c4ef4e0
--- /dev/null
+++ b/installed-tests/esm/named-import.js
@@ -0,0 +1,3 @@
+import { NamedExport } from './exports.js';
+
+print(NamedExport);
diff --git a/installed-tests/esm/named-import.js.output b/installed-tests/esm/named-import.js.output
new file mode 100644
index 00000000..3fa0d4b9
--- /dev/null
+++ b/installed-tests/esm/named-import.js.output
@@ -0,0 +1 @@
+Hello, World
diff --git a/installed-tests/esm/system-import.js b/installed-tests/esm/system-import.js
new file mode 100644
index 00000000..756c0725
--- /dev/null
+++ b/installed-tests/esm/system-import.js
@@ -0,0 +1,3 @@
+import system from 'system';
+
+print(system.exit);
diff --git a/installed-tests/esm/system-import.js.output b/installed-tests/esm/system-import.js.output
new file mode 100644
index 00000000..06e7382b
--- /dev/null
+++ b/installed-tests/esm/system-import.js.output
@@ -0,0 +1,3 @@
+function exit() {
+    [native code]
+}
diff --git a/installed-tests/meson.build b/installed-tests/meson.build
index 9210957f..9ed1fa8d 100644
--- a/installed-tests/meson.build
+++ b/installed-tests/meson.build
@@ -40,6 +40,45 @@ endforeach
 
 subdir('js')
 
+# esm script tests #
+
+esm_tests = [
+    'default-import',
+    'named-import',
+    'gi-import',
+    'system-import'
+]
+
+esm_test_driver = find_program(files('esm-test.sh'))
+if get_option('installed_tests')
+    install_data('esm-test.sh', install_dir: installed_tests_execdir)
+endif
+
+foreach test : esm_tests
+    test_file = files('esm' / '@0@.js'.format(test))
+
+    test('@0@ command'.format(test), esm_test_driver,
+        args: test_file, env: tests_environment, protocol: 'tap',
+        suite: 'ESM')
+
+    test_description_subst = {
+        'name': '@0@.js'.format(test),
+        'installed_tests_execdir': installed_tests_execdir,
+    }
+    test_description = configure_file(configuration: test_description_subst,
+        input: 'esm.test.in',
+        output: '@0@.test'.format(test),
+        install: get_option('installed_tests'),
+        install_dir: installed_tests_metadir)
+
+    if get_option('installed_tests')
+        install_data(test_file, install_dir: installed_tests_execdir / 'esm')
+        install_data('esm' / '@0@.js'.format(test),
+            'esm' / '@0@.js.output'.format(test),
+            install_dir: installed_tests_execdir / 'esm')
+    endif
+endforeach
+
 # Debugger script tests #
 
 debugger_tests = [
diff --git a/js.gresource.xml b/js.gresource.xml
index f0c3b934..dd5a5341 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -1,6 +1,13 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/org/gnome/gjs">
+    <!-- Internal scripts -->
+    <file>gjs/module.js</file>
+    
+    <!-- ESM-based modules -->
+    <file>modules/esm/gi.js</file>
+    <file>modules/esm/system.js</file>
+
     <!-- Script-based Modules -->
     <file>modules/script/_bootstrap/debugger.js</file>
     <file>modules/script/_bootstrap/default.js</file>
diff --git a/meson.build b/meson.build
index 574ae1b1..3b72adc4 100644
--- a/meson.build
+++ b/meson.build
@@ -380,6 +380,7 @@ libgjs_sources = [
     'gjs/error-types.cpp',
     'gjs/global.cpp', 'gjs/global.h',
     'gjs/importer.cpp', 'gjs/importer.h',
+    'gjs/internal.cpp', 'gjs/internal.h',
     'gjs/mem.cpp', 'gjs/mem-private.h',
     'gjs/module.cpp', 'gjs/module.h',
     'gjs/native.cpp', 'gjs/native.h',
diff --git a/modules/esm/.eslintrc.yml b/modules/esm/.eslintrc.yml
new file mode 100644
index 00000000..610e5085
--- /dev/null
+++ b/modules/esm/.eslintrc.yml
@@ -0,0 +1,4 @@
+---
+extends: '../../.eslintrc.yml'
+parserOptions:
+  ecmaVersion: 2020
diff --git a/modules/esm/gi.js b/modules/esm/gi.js
new file mode 100644
index 00000000..274d3827
--- /dev/null
+++ b/modules/esm/gi.js
@@ -0,0 +1,14 @@
+const gi = import.meta.require('gi');
+
+const Gi = {
+    require(name, version = null) {
+        if (version !== null) {
+            gi.versions[name] = version;
+        }
+
+        return gi[name];
+    }
+}
+Object.freeze(Gi);
+
+export default Gi;
\ No newline at end of file
diff --git a/modules/esm/system.js b/modules/esm/system.js
new file mode 100644
index 00000000..1523b7be
--- /dev/null
+++ b/modules/esm/system.js
@@ -0,0 +1 @@
+export default import.meta.require('system');


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]