[gjs/esm/static-imports] esm: Enable ESM static imports



commit 8490cfd957e4b8f945a1570993cc8bd1b4298c97
Author: Evan Welsh <noreply evanwelsh com>
Date:   Mon Jun 22 20:14:24 2020 -0500

    esm: Enable ESM static imports

 gi/repo.cpp                         |  38 +++
 gjs/.eslintrc.yml                   |  22 ++
 gjs/console.cpp                     |   8 +-
 gjs/context-private.h               |   7 +-
 gjs/context.cpp                     | 223 +++++++++++++++---
 gjs/global.cpp                      | 166 ++++++++++++-
 gjs/global.h                        |   8 +
 gjs/internal.cpp                    | 453 ++++++++++++++++++++++++++++++++++++
 gjs/internal.h                      |  66 ++++++
 gjs/internal/errorTypes.js          |  31 +++
 gjs/internal/module.js              | 254 ++++++++++++++++++++
 gjs/internal/module/loaders/file.js |  50 ++++
 gjs/jsapi-util.cpp                  |   4 +
 gjs/jsapi-util.h                    |   3 +
 gjs/module.cpp                      | 127 ++++++++++
 gjs/module.h                        |  42 +++-
 js.gresource.xml                    |   9 +
 meson.build                         |   1 +
 modules/esm/.eslintrc.yml           |   5 +
 modules/esm/gi.js                   |  17 ++
 modules/esm/system.js               |   1 +
 21 files changed, 1502 insertions(+), 33 deletions(-)
---
diff --git a/gi/repo.cpp b/gi/repo.cpp
index a0ebf1cd..46ae34e4 100644
--- a/gi/repo.cpp
+++ b/gi/repo.cpp
@@ -670,6 +670,41 @@ static JSObject* lookup_namespace(JSContext* context, JS::HandleId ns_name) {
     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);
@@ -679,6 +714,9 @@ JSObject* gjs_lookup_namespace_object_by_name(JSContext* context,
         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;
diff --git a/gjs/.eslintrc.yml b/gjs/.eslintrc.yml
new file mode 100644
index 00000000..9b4f5542
--- /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/console.cpp b/gjs/console.cpp
index 3b375775..08e68789 100644
--- a/gjs/console.cpp
+++ b/gjs/console.cpp
@@ -208,15 +208,15 @@ int define_argv_and_eval_script(GjsContext* js_context, int argc,
     int code;
     if (exec_as_module) {
         GjsAutoUnref<GFile> output = g_file_new_for_commandline_arg(filename);
-        char* full_path = g_file_get_path(output);
-        if (!gjs_context_register_module(js_context, full_path, full_path,
-                                         script, len, &error)) {
+        char* uri = g_file_get_uri(output);
+        if (!gjs_context_register_module(js_context, uri, uri, script, len,
+                                         &error)) {
             g_printerr("%s\n", error->message);
             code = 1;
         }
 
         uint8_t code_8 = 0;
-        if (!gjs_context_eval_module(js_context, full_path, &code_8, &error)) {
+        if (!gjs_context_eval_module(js_context, uri, &code_8, &error)) {
             code = code_8;
             if (!g_error_matches(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT))
                 g_critical("%s", error->message);
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 7721a915..72d8b740 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -94,6 +94,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;
@@ -184,6 +185,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; }
@@ -252,7 +256,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 2303ecac..b01082e4 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -83,8 +83,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 +336,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 +424,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 +510,55 @@ 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.
+
+    // TODO(ewlsh): Consider moving internal imports into JavaScript
+
+    if (!gjs_load_internal_script(cx, "errorTypes") ||
+        !gjs_load_internal_script(cx, "module") ||
+        !gjs_load_internal_script(cx, "module/loaders/file")) {
+        gjs_log_exception(cx);
+        g_error("Failed to load internal module loaders.");
+    }
+
+    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 +580,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);
 }
 
 static void
@@ -1046,33 +1086,164 @@ bool GjsContextPrivate::eval(const char* script, ssize_t script_len,
 }
 
 bool GjsContextPrivate::eval_module(const char* identifier,
-                                    uint8_t* exit_status_p,
-                                    GError** error G_GNUC_UNUSED) {
-    // TODO(ewlsh): Implement eval_module
-    g_print("GjsContextPrivate::eval_module called on identifier %s.",
-            identifier);
+                                    uint8_t* exit_status_p, GError** error) {
+    bool auto_profile = m_should_profile;
 
-    *exit_status_p = 1;
+    if (auto_profile &&
+        (_gjs_profiler_is_running(m_profiler) || m_should_listen_sigusr2))
+        auto_profile = false;
 
-    g_error(
-        "GjsContextPrivate::eval_module is not implemented. Exiting with "
-        "error.");
+    if (auto_profile)
+        gjs_profiler_start(m_profiler);
 
-    return false;
+    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,
-                                        GError** error G_GNUC_UNUSED) {
-    // TODO(ewlsh): Implement register_module
-    g_warning(
-        "GjsContextPrivate::register_module is not yet implemented. Printing "
-        "module...");
-    g_warning("Identifier: %s\nFilename: %s\nModule Length: %lu\n", identifier,
-              filename, module_len);
-    g_warning("%s", module);
-    return true;
+                                        const char* script, ssize_t script_len,
+                                        GError** error) {
+    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(identifier);
+
+    if (it.found()) {
+        gjs_throw(m_cx, "Module '%s' already registered", identifier);
+        return false;
+    }
+
+    JS::RootedObject module_record(m_cx,
+                                   module->compile(m_cx, script, script_len));
+
+    GjsAutoChar iden(g_strdup(identifier));
+
+    if (module_record && !esm_registry->add(it, iden.get(), 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 4e4d6b92..f8fdeb48 100644
--- a/gjs/global.cpp
+++ b/gjs/global.cpp
@@ -46,13 +46,16 @@
 #include <js/Utility.h>  // for UniqueChars
 #include <jsapi.h>       // for AutoSaveExceptionState, ...
 
-
+#include "gi/ns.h"
 #include "gjs/atoms.h"
 #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"
+#include "gjs/native.h"
 
 namespace mozilla {
 union Utf8Unit;
@@ -274,6 +277,9 @@ const JSClassOps defaultclassops = JS::DefaultGlobalClassOps;
 
 class GjsGlobal {
     static void finalize(JSFreeOp* op G_GNUC_UNUSED, JSObject* obj) {
+        delete static_cast<GjsModuleRegistry*>(
+            gjs_get_global_slot(obj, GjsGlobalSlot::MODULE_REGISTRY)
+                .toPrivate());
         delete static_cast<GjsModuleRegistry*>(
             gjs_get_global_slot(obj, GjsGlobalSlot::NATIVE_REGISTRY)
                 .toPrivate());
@@ -308,11 +314,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;
     }
 
@@ -321,6 +340,10 @@ class GjsGlobal {
                                              JSObject* cmp_global) {
         auto global = global_create_with_existing(cx, cmp_global, &klass);
 
+        if (global) {
+            setup_global(cx, global);
+        }
+
         return global;
     }
 
@@ -339,6 +362,10 @@ class GjsGlobal {
         // const_cast is allowed here if we never free the realm data
         JS::SetRealmPrivate(realm, const_cast<char*>(realm_name));
 
+        JSAutoRealm ar(cx, global);
+
+        gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
+                            JS::PrivateValue(new GjsModuleRegistry()));
         gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY,
                             JS::PrivateValue(new GjsModuleRegistry()));
 
@@ -411,6 +438,126 @@ 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 void finalize(JSFreeOp* op G_GNUC_UNUSED, JSObject* obj) {
+        delete static_cast<GjsModuleRegistry*>(
+            gjs_get_global_slot(obj, GjsInternalGlobalSlot::SCRIPT_REGISTRY)
+                .toPrivate());
+        delete static_cast<GjsModuleRegistry*>(
+            gjs_get_global_slot(obj, GjsGlobalSlot::MODULE_REGISTRY)
+                .toPrivate());
+    }
+
+    static constexpr JSClassOps classops = {nullptr,  // addProperty
+                                            nullptr,  // deleteProperty
+                                            nullptr,  // enumerate
+                                            JS_NewEnumerateStandardClasses,
+                                            JS_ResolveStandardClass,
+                                            JS_MayResolveStandardClass,
+                                            GjsInternalGlobal::finalize,
+                                            nullptr,  // call
+                                            nullptr,  // hasInstance
+                                            nullptr,  // construct
+                                            JS_GlobalObjectTraceHook};
+
+    static constexpr JSClass klass = {
+        "GjsInternalGlobal",
+        JSCLASS_FOREGROUND_FINALIZE |
+            JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(
+                static_cast<uint32_t>(GjsInternalGlobalSlot::LAST)),
+        &classops,
+    };
+
+ 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) {
+        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));
+
+        JSAutoRealm ar(cx, global);
+
+        gjs_set_global_slot(global, GjsInternalGlobalSlot::SCRIPT_REGISTRY,
+                            JS::PrivateValue(new GjsModuleRegistry()));
+        gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
+                            JS::PrivateValue(new GjsModuleRegistry()));
+
+        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
@@ -429,6 +576,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;
         }
@@ -439,6 +589,8 @@ 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;
     }
@@ -505,6 +657,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;
@@ -517,6 +672,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) {
@@ -524,6 +681,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::classops) constexpr GjsGlobal::classops;
@@ -532,3 +691,8 @@ 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::classops) constexpr GjsInternalGlobal::classops;
+decltype(
+    GjsInternalGlobal::static_funcs) constexpr GjsInternalGlobal::static_funcs;
diff --git a/gjs/global.h b/gjs/global.h
index dac36e96..1d561acf 100644
--- a/gjs/global.h
+++ b/gjs/global.h
@@ -37,11 +37,13 @@
 enum class GjsGlobalType {
     DEFAULT,
     DEBUGGER,
+    INTERNAL,
 };
 
 enum class GjsGlobalSlot : uint32_t {
     GLOBAL_TYPE = 0,
     IMPORTS,
+    MODULE_REGISTRY,
     NATIVE_REGISTRY,
     PROTOTYPE_gtype,
     PROTOTYPE_importer,
@@ -66,6 +68,12 @@ enum class GjsGlobalSlot : uint32_t {
     LAST,
 };
 
+enum class GjsInternalGlobalSlot : uint32_t {
+    SCRIPT_REGISTRY = static_cast<uint32_t>(GjsGlobalSlot::LAST),
+    IMPORT_HOOK,
+    LAST
+};
+
 bool gjs_global_is_type(JSContext* cx, GjsGlobalType type);
 GjsGlobalType gjs_global_get_type(JSObject* global);
 
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
new file mode 100644
index 00000000..6863978c
--- /dev/null
+++ b/gjs/internal.cpp
@@ -0,0 +1,453 @@
+/*
+ * 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/internal/%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));
+
+    GjsAutoChar iden(g_strdup(identifier));
+
+    if (module_record && !internal_registry->add(it, iden, 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..475b6ce2
--- /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/internal/errorTypes.js b/gjs/internal/errorTypes.js
new file mode 100644
index 00000000..94a6c355
--- /dev/null
+++ b/gjs/internal/errorTypes.js
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+class ImportError extends Error {
+    constructor(message) {
+        super(message);
+
+        this.name = 'ImportError';
+    }
+}
+
+globalThis.ImportError = ImportError;
diff --git a/gjs/internal/module.js b/gjs/internal/module.js
new file mode 100644
index 00000000..6681e8a7
--- /dev/null
+++ b/gjs/internal/module.js
@@ -0,0 +1,254 @@
+/*
+ * 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, ImportError */
+
+if (typeof ImportError !== 'function')
+    throw new Error('ImportError is not defined in module loader.');
+
+// NOTE: Gio, GLib, and GObject have no overrides.
+
+function isRelativePath(id) {
+    // Check if the path is relative.
+    return id.startsWith('./') || id.startsWith('../');
+}
+
+const allowedRelatives = ['file', 'resource'];
+
+const relativeResolvers = new Map();
+const loaders = new Map();
+
+function registerScheme(...schemes) {
+    function forEach(fn, ...args) {
+        schemes.forEach(s => fn(s, ...args));
+    }
+
+    const schemeBuilder = {
+        relativeResolver(handler) {
+            forEach(scheme => {
+                allowedRelatives.push(scheme);
+                relativeResolvers.set(scheme, handler);
+            });
+
+            return schemeBuilder;
+        },
+        loader(handler) {
+            forEach(scheme => {
+                loaders.set(scheme, handler);
+            });
+
+            return schemeBuilder;
+        },
+    };
+
+    return Object.freeze(schemeBuilder);
+}
+
+globalThis.registerScheme = registerScheme;
+
+function parseURI(uri) {
+    const parsed = GLib.uri_parse_scheme(uri);
+
+    if (!parsed)
+        return null;
+
+    return {
+        raw: uri,
+        scheme: parsed,
+    };
+
+}
+
+/**
+ * @type {Set<string>}
+ *
+ * The set of "module" URIs (the module search path)
+ */
+const moduleURIs = new Set();
+
+function registerModuleURI(uri) {
+    moduleURIs.add(uri);
+}
+
+// Always let ESM-specific modules take priority over core modules.
+registerModuleURI('resource:///org/gnome/gjs/modules/esm/');
+registerModuleURI('resource:///org/gnome/gjs/modules/core/');
+
+/**
+ * @param {string} specifier the package specifier
+ * @returns {string[]} the possible internal URIs
+ */
+function buildInternalURIs(specifier) {
+    const builtURIs = [];
+
+    for (const uri of moduleURIs) {
+        const builtURI = `${uri}/${specifier}.js`;
+
+        debug(`Built internal URI ${builtURI} with ${specifier} for ${uri}.`);
+
+        builtURIs.push(builtURI);
+    }
+
+    return builtURIs;
+}
+
+function resolveRelativePath(moduleURI, relativePath) {
+    // If a module has a path, we'll have stored it in the host field
+    if (!moduleURI)
+        throw new ImportError('Cannot import from relative path when module path is unknown.');
+
+    debug(`moduleURI: ${moduleURI}`);
+
+    const parsed = parseURI(moduleURI);
+
+    // Handle relative imports from URI-based modules.
+    if (parsed) {
+        const resolver = relativeResolvers.get(parsed.scheme);
+
+        if (resolver) {
+            return resolver(parsed, relativePath);
+        } else {
+            throw new ImportError(
+                `Relative imports can only occur from the following URI schemes: ${
+                    Array.from(relativeResolvers.keys()).map(s => `${s}://`).join(', ')
+                }`);
+        }
+    } else {
+        throw new ImportError(`Module has invalid URI: ${moduleURI}`);
+    }
+}
+
+function loadURI(uri) {
+    debug(`URI: ${uri.raw}`);
+
+    if (uri.scheme) {
+        const loader = loaders.get(uri.scheme);
+
+        if (loader)
+            return loader(uri);
+        else
+            throw new ImportError(`No resolver found for URI: ${uri.raw || uri}`);
+
+    } else {
+        throw new ImportError(`Unable to load module, module has invalid URI: ${uri.raw || uri}`);
+    }
+}
+
+function resolveSpecifier(specifier, moduleURI = null) {
+    // If a module has a path, we'll have stored it in the host field
+    let output = null;
+    let uri = null;
+    let parsedURI = null;
+
+    if (isRelativePath(specifier)) {
+        let resolved = resolveRelativePath(moduleURI, specifier);
+
+        parsedURI = parseURI(resolved);
+        uri = resolved;
+    } else {
+        const parsed = parseURI(specifier);
+
+        if (parsed) {
+            uri = parsed.raw;
+            parsedURI = parsed;
+        }
+    }
+
+    if (parsedURI)
+        output = loadURI(parsedURI);
+
+    if (!output)
+        return null;
+
+    return {output, uri};
+}
+
+function resolveModule(specifier, moduleURI) {
+    // Check if the module has already been loaded
+    //
+    // Order:
+    // - Local imports
+    // - Internal imports
+
+    debug(`Resolving: ${specifier}`);
+
+    let lookup_module = lookupModule(specifier);
+
+    if (lookup_module)
+        return lookup_module;
+
+    lookup_module = lookupInternalModule(specifier);
+
+    if (lookup_module)
+        return lookup_module;
+
+    // 1) Resolve path and URI-based imports.
+
+    const resolved = resolveSpecifier(specifier, moduleURI);
+
+    if (resolved) {
+        const {output, uri} = resolved;
+
+        debug(`Full path found: ${uri}`);
+
+        lookup_module = lookupModule(uri);
+
+        // Check if module is already loaded (relative handling)
+        if (lookup_module)
+            return lookup_module;
+
+        const text = output;
+
+        if (!registerModule(uri, uri, text, text.length, false))
+            throw new ImportError(`Failed to register module: ${uri}`);
+
+        return lookupModule(uri);
+    }
+
+    // 2) Resolve internal imports.
+
+    const uri = buildInternalURIs(specifier).find(u => {
+        let file = Gio.File.new_for_uri(u);
+
+        return file && file.query_exists(null);
+    });
+
+    if (!uri)
+        throw new ImportError(`Attempted to load unregistered global module: ${specifier}`);
+
+    const text = loaders.get('resource')(parseURI(uri));
+
+    if (!registerInternalModule(specifier, uri, text, text.length))
+        return null;
+
+    return lookupInternalModule(specifier);
+}
+
+setModuleResolveHook((referencingInfo, specifier) => {
+    debug('Starting module import...');
+    const uri = getModuleURI(referencingInfo);
+
+    if (uri)
+        debug(`Found base URI: ${uri}`);
+
+    return resolveModule(specifier, uri);
+});
diff --git a/gjs/internal/module/loaders/file.js b/gjs/internal/module/loaders/file.js
new file mode 100644
index 00000000..e36b1135
--- /dev/null
+++ b/gjs/internal/module/loaders/file.js
@@ -0,0 +1,50 @@
+/*
+ * 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 registerScheme */
+
+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}`);
+    }
+}
+
+registerScheme('file', 'resource')
+    .relativeResolver((moduleURI, relativePath) => {
+        let module_file = Gio.File.new_for_uri(moduleURI.raw);
+        let module_parent_file = module_file.get_parent();
+
+        let output = module_parent_file.resolve_relative_path(relativePath);
+
+        return output.get_uri();
+    }).loader(uri => {
+        const file = Gio.File.new_for_uri(uri.raw);
+
+        return loadFileSync(file, file.get_uri());
+    });
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 714decad..79dbb59e 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -243,6 +243,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
@@ -279,3 +303,106 @@ GjsModuleRegistry* gjs_get_native_module_registry(JSContext* js_context) {
 
     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::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, GjsGlobalSlot::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 0919cfa4..bd1ccd87 100644
--- a/gjs/module.h
+++ b/gjs/module.h
@@ -82,7 +82,47 @@ gjs_module_import(JSContext       *cx,
                   const char      *name,
                   GFile           *file);
 
-GJS_JSAPI_RETURN_CONVENTION
+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/js.gresource.xml b/js.gresource.xml
index f0c3b934..10653e86 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -1,6 +1,15 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/org/gnome/gjs">
+    <!-- Internal scripts -->
+    <file>gjs/internal/errorTypes.js</file>
+    <file>gjs/internal/module.js</file>
+    <file>gjs/internal/module/loaders/file.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..0b3bc0a2
--- /dev/null
+++ b/modules/esm/.eslintrc.yml
@@ -0,0 +1,5 @@
+---
+extends: '../../.eslintrc.yml'
+parserOptions:
+  sourceType: 'module'
+  ecmaVersion: 2020
diff --git a/modules/esm/gi.js b/modules/esm/gi.js
new file mode 100644
index 00000000..c8c172be
--- /dev/null
+++ b/modules/esm/gi.js
@@ -0,0 +1,17 @@
+const gi = import.meta.require('gi');
+
+const Gi = {
+    require(name, version = null) {
+        if (version !== null)
+            gi.versions[name] = version;
+
+        if (name === 'versions') {
+            throw new Error('Cannot import namespace "versions", not such namespace exists.');
+        }
+
+        return gi[name];
+    },
+};
+Object.freeze(Gi);
+
+export default Gi;
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]