[gjs/esm/static-imports: 2/4] esm: Enable static module imports.




commit 4b7ac551da13e81f490e2764dbfc5296a6d2d1ea
Author: Evan Welsh <contact evanwelsh com>
Date:   Thu Dec 3 19:35:59 2020 -0600

    esm: Enable static module imports.

 gi/repo.cpp                    |  36 +++-
 gjs/context-private.h          |   4 +
 gjs/context.cpp                | 240 ++++++++++++++++++++----
 gjs/global.cpp                 | 153 ++++++++++++++++
 gjs/global.h                   |  15 ++
 gjs/internal.cpp               | 404 +++++++++++++++++++++++++++++++++++++++++
 gjs/internal.h                 |  37 ++++
 gjs/jsapi-util.cpp             |  18 ++
 gjs/jsapi-util.h               |   2 +
 gjs/module.cpp                 | 123 +++++++++++++
 gjs/module.h                   |  16 ++
 js.gresource.xml               |  13 ++
 lib/.eslintrc.yml              |  26 +++
 lib/bootstrap/module.js        | 310 +++++++++++++++++++++++++++++++
 lib/entry.js                   |   7 +
 lib/jsconfig.json              |  29 +++
 lib/modules/esm.js             | 218 ++++++++++++++++++++++
 lib/modules/gi.js              |  22 +++
 lib/types.d.ts                 | 111 +++++++++++
 meson.build                    |   1 +
 modules/core/overrides/GLib.js |   5 +
 modules/esm/.eslintrc.yml      |   5 +
 modules/esm/cairo.js           |   7 +
 modules/esm/format.js          |   5 +
 modules/esm/gettext.js         |  16 ++
 modules/esm/gi.js              |  22 +++
 modules/esm/system.js          |  19 ++
 tools/package.json             |  11 +-
 tools/yarn.lock                |  73 ++++++++
 29 files changed, 1913 insertions(+), 35 deletions(-)
---
diff --git a/gi/repo.cpp b/gi/repo.cpp
index d110d8d3..9e457b8d 100644
--- a/gi/repo.cpp
+++ b/gi/repo.cpp
@@ -581,12 +581,44 @@ static JSObject* lookup_namespace(JSContext* cx, JSObject* global,
     return retval;
 }
 
+GJS_JSAPI_RETURN_CONVENTION
+static JSObject* lookup_internal_namespace(JSContext* cx, JSObject* global,
+                                           JS::HandleId ns_name) {
+    JS::RootedObject native_registry(cx, gjs_get_native_registry(global));
+
+    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+
+    // The internal global only supports GObject, Gio, GLib, and private
+    // namespaces.
+    g_assert((ns_name == atoms.gobject() || ns_name == atoms.gio() ||
+              ns_name == atoms.glib() ||
+              ns_name == atoms.private_ns_marker()) &&
+             "Attempted to load unknown GI namespace on internal global.");
+
+    JS::RootedObject retval(cx);
+
+    if (!gjs_global_registry_get(cx, native_registry, ns_name, &retval))
+        return nullptr;
+
+    return retval;
+}
+
 JSObject* gjs_lookup_namespace_object_by_name(JSContext* cx,
                                               JS::HandleId ns_name) {
     JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
 
-    g_assert(gjs_global_get_type(global) == GjsGlobalType::DEFAULT);
-    return lookup_namespace(cx, global, ns_name);
+    switch (gjs_global_get_type(global)) {
+        case GjsGlobalType::DEFAULT:
+            return lookup_namespace(cx, global, ns_name);
+        case GjsGlobalType::INTERNAL:
+            return lookup_internal_namespace(cx, global, ns_name);
+        case GjsGlobalType::DEBUGGER:
+        default:
+            // It is not possible to load namespace objects in other globals
+            // global.
+            g_assert_not_reached();
+            return nullptr;
+    }
 }
 
 const char*
diff --git a/gjs/context-private.h b/gjs/context-private.h
index bdc22120..33df0d77 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -71,6 +71,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;
@@ -165,6 +166,9 @@ class GjsContextPrivate : public JS::JobQueue {
     }
     [[nodiscard]] JSContext* context() const { return m_cx; }
     [[nodiscard]] JSObject* global() const { return m_global.get(); }
+    [[nodiscard]] JSObject* internal_global() const {
+        return m_internal_global.get();
+    }
     [[nodiscard]] GjsProfiler* profiler() const { return m_profiler; }
     [[nodiscard]] const GjsAtoms& atoms() const { return *m_atoms; }
     [[nodiscard]] bool destroying() const { return m_destroying; }
diff --git a/gjs/context.cpp b/gjs/context.cpp
index 4271244c..df515501 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -33,11 +33,15 @@
 
 #include <js/AllocPolicy.h>  // for SystemAllocPolicy
 #include <js/CallArgs.h>     // for UndefinedHandleValue
+#include <js/CharacterEncoding.h>
 #include <js/CompilationAndEvaluation.h>
 #include <js/CompileOptions.h>
+#include <js/Conversions.h>
+#include <js/ErrorReport.h>
 #include <js/GCAPI.h>               // for JS_GC, JS_AddExtraGCRootsTr...
 #include <js/GCHashTable.h>         // for WeakCache
 #include <js/GCVector.h>            // for RootedVector
+#include <js/Modules.h>
 #include <js/Promise.h>             // for JobQueue::SavedJobQueue
 #include <js/PropertyDescriptor.h>  // for JSPROP_PERMANENT, JSPROP_RE...
 #include <js/RootingAPI.h>
@@ -62,8 +66,10 @@
 #include "gjs/error-types.h"
 #include "gjs/global.h"
 #include "gjs/importer.h"
+#include "gjs/internal.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"
@@ -304,6 +310,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);
@@ -390,6 +398,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;
@@ -475,17 +484,17 @@ 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)) {
@@ -493,26 +502,80 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
         g_error("Failed to initialize global strings");
     }
 
-    std::vector<std::string> paths;
-    if (m_search_path)
-        paths = {m_search_path, m_search_path + g_strv_length(m_search_path)};
-    JS::RootedObject importer(m_cx, gjs_create_root_importer(m_cx, paths));
-    if (!importer) {
+    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;
+
+    {
+        JSAutoRealm ar(cx, global);
+
+        std::vector<std::string> paths;
+        if (m_search_path)
+            paths = {m_search_path,
+                     m_search_path + g_strv_length(m_search_path)};
+        JS::RootedObject importer(m_cx, gjs_create_root_importer(m_cx, paths));
+        if (!importer) {
+            gjs_log_exception(cx);
+            g_error("Failed to create root importer");
+        }
+
+        g_assert(
+            gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS).isUndefined() &&
+            "Someone else already created root importer");
+
+        gjs_set_global_slot(global, GjsGlobalSlot::IMPORTS,
+                            JS::ObjectValue(*importer));
+
+        if (!gjs_define_global_properties(m_cx, global, GjsGlobalType::DEFAULT,
+                                          "GJS", "default")) {
+            gjs_log_exception(m_cx);
+            g_error("Failed to define properties on global object");
+        }
+    }
+
+    JS::SetModuleResolveHook(rt, gjs_module_resolve);
+    JS::SetModuleMetadataHook(rt, gjs_populate_module_meta);
+
+    if (!JS_DefineProperty(m_cx, internal_global, "moduleGlobalThis", global,
+                           JSPROP_PERMANENT))
+        g_error("Failed to define module global in internal global.");
+
+    if (!gjs_load_internal_module(cx, "bootstrap/module")) {
         gjs_log_exception(cx);
-        g_error("Failed to create root importer");
+        g_error("Failed to load internal module loaders.");
     }
 
-    g_assert(
-        gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS).isUndefined() &&
-        "Someone else already created root importer");
+    JS::RootedObject entry(
+        cx, gjs_module_load(cx, "resource:///org/gnome/gjs/lib/entry.js",
+                            "resource:///org/gnome/gjs/lib/entry.js"));
+
+    if (!entry) {
+        gjs_log_exception(cx);
+        g_error("Failed to load internal entry module.");
+    }
 
-    gjs_set_global_slot(global, GjsGlobalSlot::IMPORTS,
-                        JS::ObjectValue(*importer));
+    if (!JS::ModuleInstantiate(cx, entry)) {
+        gjs_log_exception(cx);
+        g_error("Failed to instantiate internal entry module.");
+    }
 
-    if (!gjs_define_global_properties(m_cx, global, GjsGlobalType::DEFAULT,
-                                      "GJS", "default")) {
-        gjs_log_exception(m_cx);
-        g_error("Failed to define properties on global object");
+    if (!JS::ModuleEvaluate(cx, entry)) {
+        gjs_log_exception(cx);
+        g_error("Failed to evaluate internal entry module.");
     }
 }
 
@@ -1017,22 +1080,137 @@ bool GjsContextPrivate::eval(const char* script, ssize_t script_len,
 
 bool GjsContextPrivate::eval_module(const char* identifier,
                                     uint8_t* exit_status_p, GError** error) {
-    *exit_status_p = 1;
-    *error = nullptr;
+    bool auto_profile = m_should_profile;
+
+    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);
+
+    JS::RootedObject registry(m_cx, gjs_get_module_registry(m_global));
+    JS::RootedId key(m_cx, gjs_intern_string_to_id(m_cx, identifier));
+    JS::RootedObject obj(m_cx);
+    if (!gjs_global_registry_get(m_cx, registry, key, &obj) || !obj) {
+        g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+                    "Cannot find module with identifier %s.", identifier);
+        return false;
+    }
+
+    bool ok = true;
+
+    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;
+    }
+
+    /* 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, GError** error) {
-    *error = nullptr;
+                                        const char* file_uri, GError** error) {
+    JSAutoRealm ac(m_cx, m_global);
 
-    g_warning("Identifier: %s\nFilename: %s\n", identifier, filename);
-    return true;
+    // 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);
+
+    JS::RootedObject module(m_cx, gjs_module_load(m_cx, identifier, file_uri));
+
+    if (module) {
+        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 3a1fc84b..3614ffa9 100644
--- a/gjs/global.cpp
+++ b/gjs/global.cpp
@@ -27,10 +27,12 @@
 #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.h"
 #include "gjs/native.h"
 
@@ -187,6 +189,13 @@ class GjsGlobal : GjsBaseGlobal {
         gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY,
                             JS::ObjectValue(*native_registry));
 
+        JS::RootedObject module_registry(cx, JS::NewMapObject(cx));
+        if (!module_registry)
+            return false;
+
+        gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
+                            JS::ObjectValue(*module_registry));
+
         JS::Value v_importer =
             gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS);
         g_assert(((void) "importer should be defined before passing null "
@@ -255,6 +264,137 @@ class GjsDebuggerGlobal : GjsBaseGlobal {
     }
 };
 
+class GjsInternalGlobal : GjsBaseGlobal {
+    static constexpr JSFunctionSpec static_funcs[] = {
+        JS_FN("compileModule", gjs_internal_compile_module, 2, 0),
+        JS_FN("compileInternalModule", gjs_internal_compile_internal_module, 2,
+              0),
+        JS_FN("getRegistry", gjs_internal_global_get_registry, 1, 0),
+        JS_FN("importSync", gjs_internal_global_import_sync, 1, 0),
+        JS_FN("setModuleLoadHook", gjs_internal_global_set_module_hook, 3, 0),
+        JS_FN("setModuleMetaHook", gjs_internal_global_set_module_meta_hook, 2,
+              0),
+        JS_FN("setModulePrivate", gjs_internal_set_module_private, 2, 0),
+        JS_FN("setModuleResolveHook",
+              gjs_internal_global_set_module_resolve_hook, 2, 0),
+        JS_FS_END};
+
+    static constexpr JSClassOps classops = {nullptr,  // addProperty
+                                            nullptr,  // deleteProperty
+                                            nullptr,  // enumerate
+                                            JS_NewEnumerateStandardClasses,
+                                            JS_ResolveStandardClass,
+                                            JS_MayResolveStandardClass,
+                                            nullptr,  // finalize
+                                            nullptr,  // call
+                                            nullptr,  // hasInstance
+                                            nullptr,  // construct
+                                            JS_GlobalObjectTraceHook};
+
+    static constexpr JSClass klass = {
+        "GjsInternalGlobal",
+        JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(
+            static_cast<uint32_t>(GjsInternalGlobalSlot::LAST)),
+        &classops,
+    };
+
+ public:
+    [[nodiscard]] static JSObject* create(JSContext* cx) {
+        return GjsBaseGlobal::create(cx, &klass);
+    }
+
+    [[nodiscard]] static JSObject* create_with_compartment(
+        JSContext* cx, JS::HandleObject cmp_global) {
+        return GjsBaseGlobal::create_with_compartment(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);
+        JS::RootedObject native_registry(cx, JS::NewMapObject(cx));
+
+        if (!native_registry) {
+            return false;
+        }
+
+        gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY,
+                            JS::ObjectValue(*native_registry));
+
+        JS::RootedObject module_registry(cx, JS::NewMapObject(cx));
+        if (!module_registry)
+            return false;
+
+        gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
+                            JS::ObjectValue(*module_registry));
+        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 (!gjs_global_registry_set(cx, native_registry,
+                                     atoms.private_ns_marker(), privateNS) ||
+            !gjs_global_registry_set(cx, native_registry, atoms.gobject(),
+                                     gobject) ||
+            !gjs_global_registry_set(cx, native_registry, atoms.glib(), glib) ||
+            !gjs_global_registry_set(cx, native_registry, atoms.gio(), gio) ||
+            !JS_DefinePropertyById(cx, global, atoms.glib(), glib,
+                                   JSPROP_PERMANENT) ||
+            !JS_DefinePropertyById(cx, global, atoms.gio(), gio,
+                                   JSPROP_PERMANENT) ||
+            !JS_DefinePropertyById(cx, global, atoms.gobject(), gobject,
+                                   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;
+        }
+        JS::RootedObject io(cx, JS_NewPlainObject(cx));
+
+        if (!gjs_load_native_module(cx, "_print", &io) ||
+            !JS_DefineProperty(cx, global, "IO", io, JSPROP_PERMANENT)) {
+            gjs_throw(cx, "Failed to define IO functions.");
+            return false;
+        }
+
+        return true;
+    }
+};
+
 /**
  * gjs_create_global_object:
  * @cx: a #JSContext
@@ -273,6 +413,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;
         }
@@ -283,6 +426,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;
     }
@@ -435,6 +580,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);
     }
 
     // Global type does not handle define_properties
@@ -456,3 +604,8 @@ decltype(GjsGlobal::static_props) constexpr GjsGlobal::static_props;
 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 2b7c0e16..fad32ffc 100644
--- a/gjs/global.h
+++ b/gjs/global.h
@@ -23,6 +23,7 @@ struct PropertyKey;
 enum class GjsGlobalType {
     DEFAULT,
     DEBUGGER,
+    INTERNAL,
 };
 
 enum class GjsBaseGlobalSlot : uint32_t {
@@ -36,6 +37,14 @@ enum class GjsDebuggerGlobalSlot : uint32_t {
 
 enum class GjsGlobalSlot : uint32_t {
     IMPORTS = static_cast<uint32_t>(GjsBaseGlobalSlot::LAST),
+    // Stores the import resolution hook
+    IMPORT_HOOK,
+    // Stores the module creation hook
+    MODULE_HOOK,
+    // Stores the metadata population hook
+    META_HOOK,
+    // Stores the module registry
+    MODULE_REGISTRY,
     NATIVE_REGISTRY,
     PROTOTYPE_gtype,
     PROTOTYPE_importer,
@@ -58,6 +67,10 @@ enum class GjsGlobalSlot : uint32_t {
     LAST,
 };
 
+enum class GjsInternalGlobalSlot : uint32_t {
+    LAST = static_cast<uint32_t>(GjsGlobalSlot::LAST),
+};
+
 bool gjs_global_is_type(JSContext* cx, GjsGlobalType type);
 GjsGlobalType gjs_global_get_type(JSContext* cx);
 GjsGlobalType gjs_global_get_type(JSObject* global);
@@ -89,6 +102,7 @@ template <typename Slot>
 inline void gjs_set_global_slot(JSObject* global, Slot slot, JS::Value value) {
     static_assert(std::is_same_v<GjsBaseGlobalSlot, Slot> ||
                       std::is_same_v<GjsGlobalSlot, Slot> ||
+                      std::is_same_v<GjsInternalGlobalSlot, Slot> ||
                       std::is_same_v<GjsDebuggerGlobalSlot, Slot>,
                   "Must use a GJS global slot enum");
     detail::set_global_slot(global, static_cast<uint32_t>(slot), value);
@@ -98,6 +112,7 @@ template <typename Slot>
 inline JS::Value gjs_get_global_slot(JSObject* global, Slot slot) {
     static_assert(std::is_same_v<GjsBaseGlobalSlot, Slot> ||
                       std::is_same_v<GjsGlobalSlot, Slot> ||
+                      std::is_same_v<GjsInternalGlobalSlot, Slot> ||
                       std::is_same_v<GjsDebuggerGlobalSlot, Slot>,
                   "Must use a GJS global slot enum");
     return detail::get_global_slot(global, static_cast<uint32_t>(slot));
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
new file mode 100644
index 00000000..4f032a35
--- /dev/null
+++ b/gjs/internal.cpp
@@ -0,0 +1,404 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+#include "gjs/internal.h"
+
+#include <config.h>
+#include <gio/gio.h>
+#include <girepository.h>
+#include <glib-object.h>
+#include <glib.h>
+#include <js/Array.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/Modules.h>
+#include <js/Promise.h>
+#include <js/PropertyDescriptor.h>
+#include <js/RootingAPI.h>
+#include <js/SourceText.h>
+#include <js/TypeDecls.h>
+#include <js/Wrapper.h>
+#include <jsapi.h>  // for JS_DefinePropertyById, ...
+#include <jsfriendapi.h>
+#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/engine.h"
+#include "gjs/error-types.h"
+#include "gjs/global.h"
+#include "gjs/importer.h"
+#include "gjs/jsapi-util-args.h"
+#include "gjs/jsapi-util.h"
+#include "gjs/mem-private.h"
+#include "gjs/module.h"
+#include "gjs/native.h"
+#include "util/log.h"
+
+#include "gi/repo.h"
+
+// NOTE: You have to be very careful in this file to only do operations within
+// the correct global!
+
+/**
+ * gjs_load_internal_module:
+ *
+ * @brief Loads a module source from an internal resource,
+ * resource:///org/gnome/gjs/lib/{#identifier}.js, registers it in the internal
+ * global's module registry, and proceeds to compile, initialize, and evaluate
+ * the module.
+ *
+ * @param cx the current JSContext
+ * @param identifier the identifier of the internal module
+ *
+ * @returns whether an error occurred while loading or evaluating the module.
+ */
+bool gjs_load_internal_module(JSContext* cx, const char* identifier) {
+    GjsAutoChar full_path(
+        g_strdup_printf("resource:///org/gnome/gjs/lib/%s.js", identifier));
+
+    char* script;
+    size_t script_len;
+
+    if (!gjs_load_internal_source(cx, full_path, &script, &script_len)) {
+        return false;
+    }
+
+    std::u16string utf16_string = gjs_utf8_script_to_utf16(script, script_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::CompileOptions options(cx);
+    options.setIntroductionType("Internal Module Bootstrap");
+    options.setFileAndLine(full_path, 1);
+    options.setSelfHostingMode(false);
+
+    JS::RootedObject internal_global(cx, gjs_get_internal_global(cx));
+    JSAutoRealm ar(cx, internal_global);
+
+    JS::RootedObject module(cx, JS::CompileModule(cx, options, buf));
+    JS::RootedObject registry(cx, gjs_get_module_registry(internal_global));
+
+    JS::RootedId key(cx, gjs_intern_string_to_id(cx, full_path));
+
+    if (!gjs_global_registry_set(cx, registry, key, module) ||
+        !JS::ModuleInstantiate(cx, module) || !JS::ModuleEvaluate(cx, module)) {
+        gjs_log_exception(cx);
+        return false;
+    }
+
+    return true;
+}
+
+/**
+ * Asserts the correct arguments for a hook setting function.
+ *
+ * Asserts: (arg0: object, arg1: Function) => void
+ */
+static bool set_module_hook(JSContext* cx, JS::CallArgs args,
+                            GjsGlobalSlot slot) {
+    JS::RootedValue v_global(cx, args[0]);
+    JS::RootedValue v_hook(cx, args[1]);
+
+    g_assert(v_global.isObject());
+    g_assert(v_hook.isObject());
+
+    JS::RootedObject hook(cx, &v_hook.toObject());
+    g_assert(JS::IsCallable(hook));
+    gjs_set_global_slot(&v_global.toObject(), slot, v_hook);
+
+    args.rval().setUndefined();
+    return true;
+}
+
+/**
+ * gjs_internal_global_set_module_hook:
+ *
+ * @brief Sets the MODULE_HOOK slot of the passed global to the second argument
+ * which must be an object. Setting a non-function object is possible but will
+ * throw a not-callable error when gjs_module_load is used.
+ *
+ * @example (in JavaScript)
+ * setModuleLoadHook(globalThis, (id, uri) => {
+ *   id // the module's identifier
+ *   uri // the URI to load from
+ * });
+ *
+ * @returns whether an error occurred while setting the module hook.
+ */
+bool gjs_internal_global_set_module_hook(JSContext* cx, unsigned argc,
+                                         JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args.requireAtLeast(cx, "setModuleLoadHook", 2))
+        return false;
+
+    return set_module_hook(cx, args, GjsGlobalSlot::MODULE_HOOK);
+}
+
+/**
+ * gjs_internal_global_set_module_resolve_hook:
+ *
+ * @brief Sets the IMPORT_HOOK slot of the passed global to the second argument
+ * which must be an object. Setting a non-function object is possible but will
+ * throw a not-callable error when gjs_module_resolve is used.
+ *
+ * @example (in JavaScript)
+ * setModuleResolveHook(globalThis, (module, specifier) => {
+ *   module // the importing module object
+ *   specifier // the import specifier
+ * });
+ *
+ * @returns whether an error occurred while setting the import hook.
+ */
+bool gjs_internal_global_set_module_resolve_hook(JSContext* cx, unsigned argc,
+                                                 JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args.requireAtLeast(cx, "setModuleResolveHook", 2))
+        return false;
+
+    return set_module_hook(cx, args, GjsGlobalSlot::IMPORT_HOOK);
+}
+
+/**
+ * gjs_internal_global_set_module_meta_hook:
+ *
+ * @brief Sets the META_HOOK slot of the passed global to the second argument
+ * which must be an object. Setting a non-function object is possible but will
+ * throw a not-callable error when gjs_populate_module_meta is used.
+ *
+ * The META_HOOK is passed two parameters, a plain object for population with
+ * meta properties and the module's private object.
+ *
+ * @example (in JavaScript)
+ * setModuleMetaHook(globalThis, (module, meta) => {
+ *   module // the module object
+ *   meta // the meta object
+ * });
+ *
+ * @returns whether an error occurred while setting the meta hook.
+ */
+bool gjs_internal_global_set_module_meta_hook(JSContext* cx, unsigned argc,
+                                              JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args.requireAtLeast(cx, "setModuleMetaHook", 2))
+        return false;
+
+    return set_module_hook(cx, args, GjsGlobalSlot::META_HOOK);
+}
+
+/**
+ * compile_module:
+ *
+ * @brief Compiles the a module source text into an internal #Module object
+ * given the module's URI as the first argument.
+ *
+ * @param cx the current JSContext
+ * @param args the call args from the native function call
+ *
+ * @returns whether an error occurred while compiling the module.
+ */
+static bool compile_module(JSContext* cx, JS::CallArgs args) {
+    g_assert(args[0].isString());
+    g_assert(args[1].isString());
+
+    JS::RootedString s1(cx, args[0].toString());
+    JS::RootedString s2(cx, args[1].toString());
+
+    JS::UniqueChars uri = JS_EncodeStringToUTF8(cx, s1);
+    if (!uri)
+        return false;
+
+    JS::CompileOptions options(cx);
+    options.setFileAndLine(uri.get(), 1).setSourceIsLazy(false);
+
+    size_t text_len;
+    char16_t* text;
+    if (!gjs_string_get_char16_data(cx, s2, &text, &text_len))
+        return false;
+
+    JS::SourceText<char16_t> buf;
+    if (!buf.init(cx, text, text_len, JS::SourceOwnership::Borrowed))
+        return false;
+
+    JS::RootedObject new_module(cx, JS::CompileModule(cx, options, buf));
+    if (!new_module)
+        return false;
+
+    args.rval().setObject(*new_module);
+    return true;
+}
+
+/**
+ * gjs_internal_compile_internal_module:
+ *
+ * @brief Compiles a module source text within the internal global's realm.
+ *
+ * NOTE: Modules compiled with this function can only be executed
+ * within the internal global's realm.
+ *
+ * @param cx the current JSContext
+ * @param argc
+ * @param vp
+ *
+ * @returns whether an error occurred while compiling the module.
+ */
+bool gjs_internal_compile_internal_module(JSContext* cx, unsigned argc,
+                                          JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!args.requireAtLeast(cx, "compileInternalModule", 2)) {
+        return false;
+    }
+
+    JS::RootedObject global(cx, gjs_get_internal_global(cx));
+    JSAutoRealm ar(cx, global);
+
+    return compile_module(cx, args);
+}
+
+/**
+ * gjs_internal_compile_module:
+ *
+ * @brief Compiles a module source text within the import global's realm.
+ *
+ * NOTE: Modules compiled with this function can only be executed
+ * within the import global's realm.
+ *
+ * @param cx the current JSContext
+ * @param argc
+ * @param vp
+ *
+ * @returns whether an error occurred while compiling the module.
+ */
+bool gjs_internal_compile_module(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!args.requireAtLeast(cx, "compileModule", 2)) {
+        return false;
+    }
+
+    JS::RootedObject global(cx, gjs_get_import_global(cx));
+    JSAutoRealm ar(cx, global);
+
+    return compile_module(cx, args);
+}
+
+/**
+ * gjs_internal_set_module_private:
+ *
+ * @brief Sets the private object of an internal #Module object.
+ * The private object must be a #JSObject.
+ *
+ * @param cx the current JSContext
+ * @param argc
+ * @param vp
+ *
+ * @returns whether an error occurred while setting the private.
+ */
+bool gjs_internal_set_module_private(JSContext* cx, unsigned argc,
+                                     JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args.requireAtLeast(cx, "setModulePrivate", 2))
+        return false;
+
+    g_assert(args[0].isObject());
+    g_assert(args[1].isObject());
+
+    JS::RootedObject moduleObj(cx, &args[0].toObject());
+    JS::RootedObject privateObj(cx, &args[1].toObject());
+
+    JS::SetModulePrivate(moduleObj, JS::ObjectValue(*privateObj));
+    return true;
+}
+
+/**
+ * gjs_internal_global_import_sync:
+ *
+ * @brief Synchronously imports native "modules" from the import global's
+ * native registry. This function does not do blocking I/O so it is
+ * safe to call it synchronously for accessing native "modules" within
+ * modules. This function is always called within the import global's
+ * realm.
+ *
+ * @param cx the current JSContext
+ * @param argc
+ * @param vp
+ *
+ * @returns whether an error occurred while importing the native module.
+ */
+bool gjs_internal_global_import_sync(JSContext* cx, unsigned argc,
+                                     JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    JS::UniqueChars id;
+    if (!gjs_parse_call_args(cx, "importSync", args, "s", "identifier", &id))
+        return false;
+
+    JS::RootedObject global(cx, gjs_get_import_global(cx));
+    JSAutoRealm ar(cx, global);
+
+    JS::AutoSaveExceptionState exc_state(cx);
+
+    JS::RootedObject native_registry(cx, gjs_get_native_registry(global));
+    JS::RootedObject v_module(cx);
+
+    JS::RootedId key(cx, gjs_intern_string_to_id(cx, id.get()));
+    if (!gjs_global_registry_get(cx, native_registry, key, &v_module))
+        return false;
+
+    if (v_module) {
+        args.rval().setObject(*v_module);
+        return true;
+    }
+
+    JS::RootedObject native_obj(cx);
+    if (!gjs_load_native_module(cx, id.get(), &native_obj)) {
+        gjs_throw(cx, "Failed to load native module: %s", id.get());
+        return false;
+    }
+
+    if (!gjs_global_registry_set(cx, native_registry, key, native_obj))
+        return false;
+
+    args.rval().setObject(*native_obj);
+    return true;
+}
+
+/**
+ * gjs_internal_global_get_registry:
+ *
+ * @brief Retrieves the module registry for the passed global object.
+ *
+ * @param cx the current JSContext
+ * @param argc
+ * @param vp
+ *
+ * @returns whether an error occurred while retrieving the registry.
+ */
+bool gjs_internal_global_get_registry(JSContext* cx, unsigned argc,
+                                      JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args.requireAtLeast(cx, "getRegistry", 1))
+        return false;
+
+    JS::RootedObject global(cx, &args[0].toObject());
+    JSAutoRealm ar(cx, global);
+
+    JS::RootedObject registry(cx, gjs_get_module_registry(global));
+    args.rval().setObject(*registry);
+    return true;
+}
diff --git a/gjs/internal.h b/gjs/internal.h
new file mode 100644
index 00000000..bf550c8d
--- /dev/null
+++ b/gjs/internal.h
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+#ifndef GJS_INTERNAL_H_
+#define GJS_INTERNAL_H_
+
+#include <config.h>
+
+#include <js/TypeDecls.h>
+#include <jsapi.h>
+
+bool gjs_load_internal_module(JSContext* cx, const char* identifier);
+
+bool gjs_internal_compile_module(JSContext* cx, unsigned argc, JS::Value* vp);
+
+bool gjs_internal_compile_internal_module(JSContext* cx, unsigned argc,
+                                          JS::Value* vp);
+
+bool gjs_internal_global_get_registry(JSContext* cx, unsigned argc,
+                                      JS::Value* vp);
+
+bool gjs_internal_global_import_sync(JSContext* cx, unsigned argc,
+                                     JS::Value* vp);
+
+bool gjs_internal_global_set_module_hook(JSContext* cx, unsigned argc,
+                                         JS::Value* vp);
+
+bool gjs_internal_global_set_module_meta_hook(JSContext* cx, unsigned argc,
+                                              JS::Value* vp);
+
+bool gjs_internal_set_module_private(JSContext* cx, unsigned argc,
+                                     JS::Value* vp);
+
+bool gjs_internal_global_set_module_resolve_hook(JSContext* cx, unsigned argc,
+                                                 JS::Value* vp);
+
+#endif  // GJS_INTERNAL_H_
diff --git a/gjs/jsapi-util.cpp b/gjs/jsapi-util.cpp
index 945c9fb6..80f50d7a 100644
--- a/gjs/jsapi-util.cpp
+++ b/gjs/jsapi-util.cpp
@@ -631,6 +631,24 @@ JSObject* gjs_get_import_global(JSContext* cx) {
     return GjsContextPrivate::from_cx(cx)->global();
 }
 
+/**
+ * gjs_get_internal_global:
+ *
+ * @brief Gets the "internal global" for the context's runtime. The internal
+ * global object is the global object used for all "internal" JavaScript
+ * code (e.g. the module loader) that should not be accessible from users'
+ * code.
+ *
+ * @param context a #JSContext
+ *
+ * @returns the "internal global" for the context's
+ *  runtime. Will never return %NULL while GJS has an active context
+ *  for the runtime.
+ */
+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 2d2aac87..fb4dcd5e 100644
--- a/gjs/jsapi-util.h
+++ b/gjs/jsapi-util.h
@@ -387,6 +387,8 @@ struct GCPolicy<GjsAutoParam> : public IgnoreGCPolicy<GjsAutoParam> {};
 
 [[nodiscard]] JSObject* gjs_get_import_global(JSContext* cx);
 
+[[nodiscard]] JSObject* gjs_get_internal_global(JSContext* cx);
+
 void gjs_throw_constructor_error             (JSContext       *context);
 
 void gjs_throw_abstract_constructor_error(JSContext* cx,
diff --git a/gjs/module.cpp b/gjs/module.cpp
index d10b4727..cdf04b8e 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -269,3 +269,126 @@ JSObject* gjs_get_native_registry(JSObject* global) {
     g_assert(native_registry.isObject());
     return &native_registry.toObject();
 }
+
+/**
+ * gjs_get_module_registry:
+ *
+ * @brief Retrieves a global's module registry from the MODULE_REGISTRY slot.
+ * Registries are JS Maps. See gjs_get_native_registry for more detail.
+ *
+ * @param cx the current #JSContext
+ * @param global a global #JSObject
+ *
+ * @returns the registry map as a #JSObject
+ */
+JSObject* gjs_get_module_registry(JSObject* global) {
+    JS::Value esm_registry =
+        gjs_get_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY);
+
+    g_assert(esm_registry.isObject());
+    return &esm_registry.toObject();
+}
+
+/**
+ * gjs_module_load:
+ *
+ * Loads and registers a module given a specifier and
+ * URI.
+ *
+ * @param importer the private value of the #Module object initiating the import
+ *                 or undefined.
+ * @param meta_object the import.meta object
+ *
+ * @returns whether an error occurred while resolving the specifier.
+ */
+JSObject* gjs_module_load(JSContext* cx, const char* identifier,
+                          const char* file_uri) {
+    g_assert((gjs_global_is_type(cx, GjsGlobalType::DEFAULT) ||
+              gjs_global_is_type(cx, GjsGlobalType::INTERNAL)) &&
+             "gjs_module_load can only be called from module-enabled "
+             "globals.");
+
+    JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
+    JS::RootedValue hook(
+        cx, gjs_get_global_slot(global, GjsGlobalSlot::MODULE_HOOK));
+
+    JS::ConstUTF8CharsZ id_chars(identifier, strlen(identifier));
+    JS::ConstUTF8CharsZ uri_chars(file_uri, strlen(file_uri));
+    JS::RootedString id(cx, JS_NewStringCopyUTF8Z(cx, id_chars));
+    JS::RootedString uri(cx, JS_NewStringCopyUTF8Z(cx, uri_chars));
+
+    JS::RootedValueArray<2> args(cx);
+    args[0].setString(id);
+    args[1].setString(uri);
+
+    JS::RootedValue result(cx);
+    if (!JS_CallFunctionValue(cx, nullptr, hook, args, &result))
+        return nullptr;
+
+    g_assert(result.isObject() && "Module hook failed to return an object!");
+    return &result.toObject();
+}
+
+/**
+ * gjs_populate_module_meta:
+ *
+ * Hook SpiderMonkey calls to populate the import.meta object.
+ *
+ * @param private_ref the private value for the #Module object
+ * @param meta_object the import.meta object
+ *
+ * @returns whether an error occurred while populating the module meta.
+ */
+bool gjs_populate_module_meta(JSContext* cx, JS::HandleValue private_ref,
+                              JS::HandleObject meta_object_handle) {
+    if (!private_ref.isObject())
+        return true;
+    JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
+    JS::RootedValue hook(cx,
+                         gjs_get_global_slot(global, GjsGlobalSlot::META_HOOK));
+
+    JS::RootedObject meta(cx, meta_object_handle);
+    JS::RootedObject module(cx, &private_ref.toObject());
+    JS::RootedValueArray<2> args(cx);
+    args[0].setObject(*module);
+    args[1].setObject(*meta);
+
+    JS::RootedValue ignore_result(cx);
+    if (!JS_CallFunctionValue(cx, nullptr, hook, args, &ignore_result))
+        return false;
+
+    return true;
+}
+
+/**
+ * gjs_module_resolve:
+ *
+ * Hook SpiderMonkey calls to resolve import specifiers.
+ *
+ * @param importer the private value of the #Module object initiating the import
+ *                 or undefined.
+ * @param meta_object the import.meta object
+ *
+ * @returns whether an error occurred while resolving the specifier.
+ */
+JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importer,
+                             JS::HandleString specifier) {
+    g_assert((gjs_global_is_type(cx, GjsGlobalType::DEFAULT) ||
+              gjs_global_is_type(cx, GjsGlobalType::INTERNAL)) &&
+             "gjs_module_resolve can only be called from module-enabled "
+             "globals.");
+
+    JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
+    JS::RootedValue v_hook(
+        cx, gjs_get_global_slot(global, GjsGlobalSlot::IMPORT_HOOK));
+
+    JS::RootedValueArray<2> args(cx);
+    args[0].set(importer);
+    args[1].setString(specifier);
+
+    JS::RootedValue result(cx);
+    if (!JS_CallFunctionValue(cx, nullptr, v_hook, args, &result))
+        return nullptr;
+
+    return &result.toObject();
+}
diff --git a/gjs/module.h b/gjs/module.h
index 0d35af26..6ced3c4c 100644
--- a/gjs/module.h
+++ b/gjs/module.h
@@ -24,4 +24,20 @@ gjs_module_import(JSContext       *cx,
 GJS_JSAPI_RETURN_CONVENTION
 JSObject* gjs_get_native_registry(JSObject* global);
 
+GJS_JSAPI_RETURN_CONVENTION
+JSObject* gjs_get_module_registry(JSObject* global);
+
+GJS_JSAPI_RETURN_CONVENTION
+JSObject* gjs_module_load(JSContext* cx, const char* identifier,
+                          const char* uri);
+
+GJS_JSAPI_RETURN_CONVENTION
+JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue mod_val,
+                             JS::HandleString specifier);
+
+GJS_JSAPI_RETURN_CONVENTION
+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 bdb6b665..674717ba 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -3,6 +3,19 @@
 <!-- SPDX-FileCopyrightText: 2014 Red Hat, Inc. -->
 <gresources>
   <gresource prefix="/org/gnome/gjs">
+    <!-- Internal scripts -->
+    <file>lib/bootstrap/module.js</file>
+    <file>lib/modules/esm.js</file>
+    <file>lib/modules/gi.js</file>
+    <file>lib/entry.js</file>
+
+    <!-- ESM-based modules -->
+    <file>modules/esm/cairo.js</file>
+    <file>modules/esm/gi.js</file>
+    <file>modules/esm/system.js</file>
+    <file>modules/esm/format.js</file>
+    <file>modules/esm/gettext.js</file>
+
     <!-- Script-based Modules -->
     <file>modules/script/_bootstrap/debugger.js</file>
     <file>modules/script/_bootstrap/default.js</file>
diff --git a/lib/.eslintrc.yml b/lib/.eslintrc.yml
new file mode 100644
index 00000000..3ed3d6ef
--- /dev/null
+++ b/lib/.eslintrc.yml
@@ -0,0 +1,26 @@
+---
+extends: ../.eslintrc.yml
+parserOptions:
+  sourceType: 'module'
+  ecmaVersion: 2020
+globals:
+  ARGV: off
+  Debugger: readonly
+  GIRepositoryGType: off
+  imports: off
+  Intl: readonly
+  log: off
+  logError: off
+  print: off
+  printerr: off
+  GLib: readonly
+  Gio: readonly
+  ByteUtils: readonly
+  moduleGlobalThis: readonly
+  compileModule: readonly
+  compileInternalModule: readonly
+  setModuleResolveHook: readonly
+  setModuleMetaHook: readonly
+  setModuleLoadHook: readonly
+  setModulePrivate: readonly
+  getRegistry: readonly
\ No newline at end of file
diff --git a/lib/bootstrap/module.js b/lib/bootstrap/module.js
new file mode 100644
index 00000000..0c5fda23
--- /dev/null
+++ b/lib/bootstrap/module.js
@@ -0,0 +1,310 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+// NOTE: Gio, GLib, and GObject have no overrides.
+
+/** @typedef {{ uri: string; scheme: string; host: string; path: string; query: Query }} Uri */
+
+/** @typedef {{ load(uri: Uri): [string, boolean]; }} SchemeHandler */
+/** @typedef {{ [key: string]: string | undefined; }} Query */
+
+/**
+ * Thrown when there is an error importing a module.
+ */
+export class ImportError extends Error {
+    /**
+     * @param {string | undefined} message the import error message
+     */
+    constructor(message) {
+        super(message);
+
+        this.name = 'ImportError';
+    }
+}
+
+/**
+ * ESModule is the "private" object of every module.
+ */
+export class ESModule {
+    /**
+     *
+     * @param {string} id the module's identifier
+     * @param {string} uri the module's URI
+     * @param {boolean} [internal] whether this module is "internal"
+     */
+    constructor(id, uri, internal = false) {
+        this.id = id;
+        this.uri = uri;
+        this.internal = internal;
+    }
+}
+
+/**
+ * Returns whether a string represents a relative path (e.g. ./, ../)
+ *
+ * @param {string} path a path to check if relative
+ * @returns {boolean}
+ */
+function isRelativePath(path) {
+    // Check if the path is relative.
+    return path.startsWith('./') || path.startsWith('../');
+}
+
+/**
+ * Encodes a Uint8Array into a UTF-8 string
+ *
+ * @param {Uint8Array} bytes the bytes to convert
+ * @returns {string}
+ */
+function fromBytes(bytes) {
+    return ByteUtils.toString(bytes, 'utf-8');
+}
+
+/**
+ * @param {import("gio").File} file the Gio.File to load from.
+ * @returns {string}
+ */
+function loadFileSync(file) {
+    try {
+        const [, bytes] = file.load_contents(null);
+
+        return fromBytes(bytes);
+    } catch (error) {
+        throw new ImportError(`Unable to load file from: ${file.get_uri()}`);
+    }
+}
+
+/**
+ * Synchronously loads a file's text from a URI.
+ *
+ * @param {string} uri the URI to load
+ * @returns {string}
+ */
+export function loadResourceOrFile(uri) {
+    let output = Gio.File.new_for_uri(uri);
+
+    return loadFileSync(output);
+}
+
+/**
+ * Resolves a relative path against a URI.
+ *
+ * @param {string} uri the base URI
+ * @param {string} relativePath the relative path to resolve against the base URI
+ * @returns {Uri}
+ */
+function resolveRelativeResourceOrFile(uri, relativePath) {
+    let module_file = Gio.File.new_for_uri(uri);
+    let module_parent_file = module_file.get_parent();
+
+    if (module_parent_file) {
+        let output = module_parent_file.resolve_relative_path(relativePath);
+
+        return parseURI(output.get_uri());
+    }
+
+    throw new ImportError('File does not have a valid parent!');
+}
+
+
+/**
+ * Parses a string into a Uri object
+ *
+ * @param {string} uri the URI to parse as a string
+ * @returns {Uri}
+ */
+export function parseURI(uri) {
+    try {
+        const parsed = GLib.Uri.parse(uri, GLib.UriFlags.NONE);
+
+        const raw_query = parsed.get_query();
+        const query = raw_query ? GLib.Uri.parse_params(raw_query, -1, '&', GLib.UriParamsFlags.NONE) : {};
+
+        return {
+            uri,
+            scheme: parsed.get_scheme(),
+            host: parsed.get_host(),
+            path: parsed.get_path(),
+            query,
+
+        };
+    } catch (error) {
+        throw new ImportError(`Attempted to import invalid URI: ${uri}`);
+    }
+}
+
+/**
+ * Handles resolving and loading URIs.
+ *
+ * @class
+ */
+export class ModuleLoader {
+    /**
+     * @param {typeof globalThis} global the global object to handle module resolution
+     */
+    constructor(global) {
+        this.global = global;
+    }
+
+    /**
+     * Loads a file or resource URI synchronously
+     *
+     * @param {Uri} uri the file or resource URI to load
+     * @returns {[string] | [string, boolean] | null}
+     */
+    loadURI(uri) {
+        if (uri.scheme === 'file' || uri.scheme === 'resource')
+            return [loadResourceOrFile(uri.uri)];
+
+
+        return null;
+    }
+
+    /**
+     * Resolves an import specifier given an optional parent importer.
+     *
+     * @param {string} specifier the import specifier
+     * @param {string | null} [parentURI] the URI of the module importing the specifier
+     * @returns {Uri | null}
+     */
+    resolveSpecifier(specifier, parentURI = null) {
+        try {
+            const uri = parseURI(specifier);
+
+            if (uri)
+                return uri;
+        } catch (err) {
+
+        }
+
+        if (isRelativePath(specifier)) {
+            if (!parentURI)
+                throw new ImportError('Cannot import from relative path when module path is unknown.');
+
+
+            return this.resolveRelativePath(specifier, parentURI);
+        }
+
+
+        return null;
+    }
+
+    /**
+     * Resolves a path relative to a URI, throwing an ImportError if
+     * the parentURI isn't valid.
+     *
+     * @param {string} relativePath the relative path to resolve against the base URI
+     * @param {string} parentURI the parent URI
+     * @returns {Uri}
+     */
+    resolveRelativePath(relativePath, parentURI) {
+        // Ensure the parent URI is valid.
+        parseURI(parentURI);
+
+        // Handle relative imports from URI-based modules.
+        return resolveRelativeResourceOrFile(parentURI, relativePath);
+    }
+
+    /**
+     * Compiles a module source text with the module's URI
+     *
+     * @param {ESModule} module a module private object
+     * @param {string} text the module source text to compile
+     * @returns {import("../types").Module}
+     */
+    compileModule(module, text) {
+        const compiled = compileInternalModule(module.uri, text);
+
+        setModulePrivate(compiled, module);
+
+        return compiled;
+    }
+
+    /**
+     * @param {string} specifier the specifier (e.g. relative path, root package) to resolve
+     * @param {string | null} parentURI the URI of the module triggering this resolve
+     *
+     * @returns {import("../types").Module | null}
+     */
+    resolveModule(specifier, parentURI) {
+        const registry = getRegistry(this.global);
+
+
+        // Check if the module has already been loaded
+
+        let module = registry.get(specifier);
+
+        if (module)
+            return module;
+
+
+        // 1) Resolve path and URI-based imports.
+
+        const uri = this.resolveSpecifier(specifier, parentURI);
+
+        if (uri) {
+            module = registry.get(uri.uri);
+            //
+            // Check if module is already loaded (relative handling)
+            if (module)
+                return module;
+
+
+            const result = this.loadURI(uri);
+
+            if (!result)
+                return null;
+
+
+            const [text, internal = false] = result;
+
+            const esmodule = new ESModule(uri.uri, uri.uri, internal);
+
+            const compiled = this.compileModule(esmodule, text);
+
+            if (!compiled)
+                throw new ImportError(`Failed to register module: ${uri}`);
+
+
+            registry.set(uri.uri, compiled);
+            return compiled;
+        }
+
+        return null;
+    }
+}
+
+export const internalModuleLoader = new ModuleLoader(globalThis);
+
+setModuleResolveHook(globalThis, (module, specifier) => {
+    const resolved = internalModuleLoader.resolveModule(specifier, module?.uri ?? null);
+
+    if (!resolved)
+        throw new ImportError(`Module not found: ${specifier}`);
+
+
+    return resolved;
+});
+
+setModuleMetaHook(globalThis, (module, meta) => {
+    meta.url = module.uri;
+});
+
+setModuleLoadHook(globalThis, (id, uri) => {
+    const m = new ESModule(id, uri);
+
+    const result = internalModuleLoader.loadURI(parseURI(uri));
+
+    if (!result)
+        throw new ImportError(`URI not found: ${uri}`);
+
+
+    const [text] = result;
+    const compiled = internalModuleLoader.compileModule(m, text);
+
+    const registry = getRegistry(globalThis);
+
+    registry.set(uri, compiled);
+
+    return compiled;
+});
diff --git a/lib/entry.js b/lib/entry.js
new file mode 100644
index 00000000..64ee7e06
--- /dev/null
+++ b/lib/entry.js
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+// This file is called *after* ./bootstrap sets up internal module resolution.
+
+// Setup ES modules.
+import './modules/esm.js';
diff --git a/lib/jsconfig.json b/lib/jsconfig.json
new file mode 100644
index 00000000..bde26124
--- /dev/null
+++ b/lib/jsconfig.json
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+{
+    "compilerOptions": {
+        "target": "es2019",
+        "module": "es2020",
+        "moduleResolution": "node",
+        "checkJs": true,
+        "noImplicitAny": true,
+        "noImplicitReturns": true,
+        "noImplicitThis": true,
+        "strict": true,
+        "paths": {
+            "*": [
+                "node_modules/@gi-types/*",
+                "*"
+            ]
+        },
+        "baseUrl": "../"
+    },
+    "include": [
+        "entry.js",
+        "types.d.ts",
+        "bootstrap/lib.js",
+        "modules/esm.js",
+        "modules/gi.js"
+    ]
+}
\ No newline at end of file
diff --git a/lib/modules/esm.js b/lib/modules/esm.js
new file mode 100644
index 00000000..ee423922
--- /dev/null
+++ b/lib/modules/esm.js
@@ -0,0 +1,218 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+import {ESModule, ImportError, loadResourceOrFile, ModuleLoader, parseURI} from '../bootstrap/module.js';
+
+import {generateModule} from './gi.js';
+
+export class ESModuleLoader extends ModuleLoader {
+    /**
+     * @param {typeof globalThis} global the global object to register modules with.
+     */
+    constructor(global) {
+        super(global);
+
+        /**
+         * @type {Set<string>}
+         *
+         * The set of "module" URIs (the module search path)
+         */
+        this.moduleURIs = new Set();
+
+        /**
+         * @type {Map<string, import("../bootstrap/module.js").SchemeHandler>}
+         *
+         * A map of handlers for URI schemes (e.g. gi://)
+         */
+        this.schemeHandlers = new Map();
+    }
+
+    /**
+     * @param {ESModule} module a module private object
+     * @param {string} text the module source text
+     */
+    compileModule(module, text) {
+        const compiled = compileModule(module.uri, text);
+
+        setModulePrivate(compiled, module);
+
+        return compiled;
+    }
+
+    /**
+     * @param {string} specifier the package specifier
+     * @returns {string[]} the possible internal URIs
+     */
+    buildInternalURIs(specifier) {
+        const {moduleURIs} = this;
+
+        const builtURIs = [];
+
+        for (const uri of moduleURIs) {
+            const builtURI = `${uri}/${specifier}.js`;
+
+            builtURIs.push(builtURI);
+        }
+
+        return builtURIs;
+    }
+
+    /**
+     * @param {string} scheme the URI scheme to register
+     * @param {import("../bootstrap/module.js").SchemeHandler} handler a handler
+     */
+    registerScheme(scheme, handler) {
+        this.schemeHandlers.set(scheme, handler);
+    }
+
+    /**
+     * @param {import("../bootstrap/module.js").Uri} uri a Uri object to load
+     */
+    loadURI(uri) {
+        const {schemeHandlers} = this;
+
+        if (uri.scheme) {
+            const loader = schemeHandlers.get(uri.scheme);
+
+            if (loader)
+                return loader.load(uri);
+        }
+
+        const result = super.loadURI(uri);
+
+        if (result)
+            return result;
+
+        throw new ImportError(`Unable to load module from invalid URI: ${uri.uri}`);
+    }
+
+    /**
+     * Registers an internal resource URI as a bare-specifier root.
+     *
+     * For example, registering "resource:///org/gnome/gjs/modules/esm/" allows
+     * import "system" if "resource:///org/gnome/gjs/modules/esm/system.js"
+     * exists.
+     *
+     * @param {string} uri the URI to register.
+     */
+    registerModuleURI(uri) {
+        const {moduleURIs} = this;
+
+        moduleURIs.add(uri);
+    }
+
+    /**
+     * Resolves a module import with optional handling for relative imports.
+     *
+     * @param {string} specifier the module specifier to resolve for an import
+     * @param {string | null} moduleURI the importing module's URI or null if importing from the entry point
+     * @returns {import("../types").Module}
+     */
+    resolveModule(specifier, moduleURI) {
+        const module = super.resolveModule(specifier, moduleURI);
+
+        if (module)
+            return module;
+
+
+        // 2) Resolve internal imports.
+
+        const uri = this.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 parsed = parseURI(uri);
+
+        if (parsed.scheme !== 'file' && parsed.scheme !== 'resource')
+            throw new ImportError('Only file:// and resource:// URIs are currently supported.');
+
+
+        const text = loadResourceOrFile(parsed.uri);
+
+        const priv = new ESModule(specifier, uri, true);
+
+        const compiled = this.compileModule(priv, text);
+
+        if (!compiled)
+            throw new ImportError(`Failed to register module: ${uri}`);
+
+        const registry = getRegistry(this.global);
+
+        if (!registry.has(specifier))
+            registry.set(specifier, compiled);
+
+        return compiled;
+    }
+}
+
+export const moduleLoader = new ESModuleLoader(moduleGlobalThis);
+
+// Always let ESM-specific modules take priority over core modules.
+moduleLoader.registerModuleURI('resource:///org/gnome/gjs/modules/esm/');
+moduleLoader.registerModuleURI('resource:///org/gnome/gjs/modules/core/');
+
+const giVersionMap = new Map();
+
+giVersionMap.set('GLib', '2.0');
+giVersionMap.set('Gio', '2.0');
+giVersionMap.set('GObject', '2.0');
+
+/**
+ * @param {string} lib the GI namespace to get the version for.
+ */
+function getGIVersionMap(lib) {
+    return giVersionMap.get(lib);
+}
+
+moduleLoader.registerScheme('gi', {
+    /**
+     * @param {import("../bootstrap/module.js").Uri} uri the URI to load
+     */
+    load(uri) {
+        const version = uri.query.version ?? getGIVersionMap(uri.host);
+
+        if (version)
+            giVersionMap.set(uri.host, version);
+
+        return [generateModule(uri.host, version), true];
+    },
+});
+
+
+/**
+ * @param {ESModule} module
+ * @param {ImportMeta} meta
+ */
+setModuleMetaHook(moduleGlobalThis, (module, meta) => {
+    meta.url = module.uri;
+
+    if (module.internal)
+        meta.importSync = globalThis.importSync;
+});
+
+/**
+ * @param {string} id
+ * @param {string} uri
+ */
+setModuleLoadHook(moduleGlobalThis, (id, uri) => {
+    const priv = new ESModule(id, uri);
+
+    const [text] = moduleLoader.loadURI(parseURI(uri));
+    const compiled = moduleLoader.compileModule(priv, text);
+
+    const registry = getRegistry(moduleGlobalThis);
+
+    registry.set(id, compiled);
+
+    return compiled;
+});
+
+setModuleResolveHook(moduleGlobalThis, (module, specifier) => {
+    return moduleLoader.resolveModule(specifier, module.uri);
+});
+
diff --git a/lib/modules/gi.js b/lib/modules/gi.js
new file mode 100644
index 00000000..d38bd347
--- /dev/null
+++ b/lib/modules/gi.js
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+/**
+ * Creates a module source text to expose a GI namespace via a default export.
+ *
+ * @param {string} namespace the GI namespace to import
+ * @param {string} [version] the version string of the namespace
+ *
+ * @returns {string} the generated module source text
+ */
+export function generateModule(namespace, version) {
+    const source = `
+    import $$gi from 'gi';
+    
+    const $$ns = $$gi.require${version ? `('${namespace}', '${version}')` : `('${namespace}')`};
+
+    export default $$ns;
+    `;
+
+    return source;
+}
diff --git a/lib/types.d.ts b/lib/types.d.ts
new file mode 100644
index 00000000..935cddc5
--- /dev/null
+++ b/lib/types.d.ts
@@ -0,0 +1,111 @@
+import * as glib from "glib";
+import * as gio from "gio";
+
+import { ESModule } from "./bootstrap/module"; 
+
+interface Module {
+    // This prevents any object's type from structurally matching Module
+    __internal: never;
+}
+
+export type { Module };
+
+declare global {
+    export interface ImportMeta {
+        /**
+         * 
+         */
+        url?: string;
+
+        /**
+         * 
+         */
+        importSync?: <T = any>(id: string) => T;
+    }
+
+    /**
+     * 
+     * @param id 
+     */
+    export function importSync<T = any>(id: string): T
+
+    /**
+     * 
+     * @param msg 
+     */
+    export function debug(msg: string): void;
+
+    /**
+     * 
+     * @param uri 
+     * @param text 
+     */
+    export function compileModule(uri: string, text: string): Module;
+
+    /**
+     * 
+     * @param uri 
+     * @param text 
+     */
+    export function compileInternalModule(uri: string, text: string): Module;
+
+    /**
+     * 
+     * @param module 
+     * @param private 
+     */
+    export function setModulePrivate(module: Module, private: ESModule);
+
+    /**
+     * 
+     * @param global 
+     * @param hook 
+     */
+    export function setModuleLoadHook(global: typeof globalThis, hook: (id: string, uri: string) => Module);
+
+    /**
+     * 
+     * @param global 
+     * @param hook 
+     */
+    export function setModuleResolveHook(global: typeof globalThis, hook: (module: ESModule, specifier: 
string) => Module): void;
+
+    /**
+     * 
+     * @param global 
+     * @param hook 
+     */
+    export function setModuleMetaHook(global: typeof globalThis, hook: (module: ESModule, meta: ImportMeta) 
=> void): void;
+
+    /**
+     * 
+     * @param args 
+     */
+    export function registerModule(...args: any[]): any;
+
+    /**
+     * 
+     * @param global 
+     */
+    export function getRegistry(global: typeof globalThis): Map<string, Module>;
+
+    /**
+     * 
+     */
+    export const moduleGlobalThis: typeof globalThis;
+
+    /**
+     * 
+     */
+    export const GLib: typeof glib
+
+    /**
+     * 
+     */
+    export const Gio: typeof gio;
+
+    /**
+     * 
+     */
+    export const ByteUtils: any;
+}
diff --git a/meson.build b/meson.build
index 35dd0486..fe2ed57b 100644
--- a/meson.build
+++ b/meson.build
@@ -401,6 +401,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/core/overrides/GLib.js b/modules/core/overrides/GLib.js
index 20ae7e04..8d17f790 100644
--- a/modules/core/overrides/GLib.js
+++ b/modules/core/overrides/GLib.js
@@ -166,6 +166,11 @@ function _packVariant(signature, value) {
     }
 }
 
+/**
+ * @param {*} variant 
+ * @param {boolean} deep 
+ * @param {boolean} [recursive] 
+ */
 function _unpackVariant(variant, deep, recursive = false) {
     switch (String.fromCharCode(variant.classify())) {
     case 'b':
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/cairo.js b/modules/esm/cairo.js
new file mode 100644
index 00000000..642f2ccc
--- /dev/null
+++ b/modules/esm/cairo.js
@@ -0,0 +1,7 @@
+const cairo = import.meta.importSync('cairoNative');
+
+export default Object.assign(
+    {},
+    imports._cairo,
+    cairo
+);
\ No newline at end of file
diff --git a/modules/esm/format.js b/modules/esm/format.js
new file mode 100644
index 00000000..1d5dc4c9
--- /dev/null
+++ b/modules/esm/format.js
@@ -0,0 +1,5 @@
+export const vprintf = imports._format.vprintf;
+
+export function format(...args) {
+    return vprintf(this, args);
+}
\ No newline at end of file
diff --git a/modules/esm/gettext.js b/modules/esm/gettext.js
new file mode 100644
index 00000000..29392d05
--- /dev/null
+++ b/modules/esm/gettext.js
@@ -0,0 +1,16 @@
+export let setlocale = imports._gettext.setlocale;
+
+export let textdomain = imports._gettext.textdomain;
+export let bindtextdomain = imports._gettext.bindtextdomain;
+
+export let gettext = imports._gettext.gettext;
+export let dgettext = imports._gettext.dgettext;
+export let dcgettext = imports._gettext.dcgettext;
+
+export let ngettext = imports._gettext.ngettext;
+export let dngettext= imports._gettext.dngettext;
+
+export let pgettext = imports._gettext.pgettext;
+export let dpgettext = imports._gettext.dpgettext;
+
+export let domain = imports._gettext.domain;
diff --git a/modules/esm/gi.js b/modules/esm/gi.js
new file mode 100644
index 00000000..f9cbc374
--- /dev/null
+++ b/modules/esm/gi.js
@@ -0,0 +1,22 @@
+const gi = import.meta.importSync('gi');
+
+const Gi = {
+    require(name, version = null) {
+        if (version !== null)
+            gi.versions[name] = version;
+
+        if (name === 'versions')
+            throw new Error('Cannot import namespace "versions", use the version parameter of Gi.require to 
specify versions.');
+
+
+        return gi[name];
+    },
+};
+Object.freeze(Gi);
+
+Gi.require('GjsPrivate');
+Gi.require('GLib');
+Gi.require('GObject');
+Gi.require('Gio');
+
+export default Gi;
diff --git a/modules/esm/system.js b/modules/esm/system.js
new file mode 100644
index 00000000..52c47f8e
--- /dev/null
+++ b/modules/esm/system.js
@@ -0,0 +1,19 @@
+const system = import.meta.importSync('system');
+
+export default system;
+
+export let addressOf = system.addressOf;
+
+export let refcount = system.refcount;
+
+export let breakpoint = system.breakpoint;
+
+export let gc = system.gc;
+
+export let exit = system.exit;
+
+export let version = system.version;
+
+export let programInvocationName = system.programInvocationName;
+
+export let clearDateCaches = system.clearDateCaches;
\ No newline at end of file
diff --git a/tools/package.json b/tools/package.json
index 5920f17b..812eacda 100644
--- a/tools/package.json
+++ b/tools/package.json
@@ -6,10 +6,17 @@
   "private": true,
   "scripts": {
     "lint": "cd .. && eslint . --format unix --resolve-plugins-relative-to tools",
-    "fix": "yarn lint --fix"
+    "fix": "yarn lint --fix",
+    "typecheck": "yarn run typecheck:lib",
+    "typecheck:lib": "cd .. && tsc -p lib/jsconfig.json"
   },
   "devDependencies": {
+    "@gi-types/gio": "^2.66.3",
+    "@gi-types/glib": "^2.66.3",
+    "@gi-types/gobject": "^2.66.3",
+    "@gi-types/gtk": "^3.24.3",
     "eslint": "^7.12.1",
-    "eslint-plugin-jsdoc": "^30.7.3"
+    "eslint-plugin-jsdoc": "^30.7.3",
+    "typescript": "^4.0.5"
   }
 }
diff --git a/tools/yarn.lock b/tools/yarn.lock
index 7a1e6daa..5e256b2d 100644
--- a/tools/yarn.lock
+++ b/tools/yarn.lock
@@ -39,6 +39,74 @@
     minimatch "^3.0.4"
     strip-json-comments "^3.1.1"
 
+"@gi-types/atk@^2.36.3":
+  version "2.36.3"
+  resolved 
"https://registry.yarnpkg.com/@gi-types/atk/-/atk-2.36.3.tgz#5853b6eb48d92a2cafcfa92c5c1d1ade4d63f559";
+  integrity sha512-bUU8nGdMwZlnidgHsH4Rk4VfSX+ZXNbJJw27E98ujZG1dTM/qeIam5nWq4z74K9YvFK6E1g0b5c76LTbDpxLCg==
+  dependencies:
+    "@gi-types/gobject" "^2.66.3"
+
+"@gi-types/cairo@^1.0.2":
+  version "1.0.2"
+  resolved 
"https://registry.yarnpkg.com/@gi-types/cairo/-/cairo-1.0.2.tgz#abc51eb22331d6d20f01cea7e86a8538befdac70";
+  integrity sha512-b6jOZtt7XqeHlMPk+6MjL76f6rhPjt0oFlsZG7nOoP3OILRD9146dCvOmQTybdmLtycvgW2P73O7GAFFtf8zIA==
+
+"@gi-types/gdk@^3.24.2":
+  version "3.24.2"
+  resolved 
"https://registry.yarnpkg.com/@gi-types/gdk/-/gdk-3.24.2.tgz#06708399d9c66babc88299aa21bbd51b5499b04c";
+  integrity sha512-0fgRJFJXRAUU7/ePgbznqPSrc33vOIhWMFNLhlanNS7G2qWyjsm8GLyXD+h8iWAtCg4JxznBJKoDPyfAul3e+Q==
+  dependencies:
+    "@gi-types/cairo" "^1.0.2"
+    "@gi-types/gdkpixbuf" "^2.0.2"
+    "@gi-types/gio" "^2.66.2"
+    "@gi-types/gobject" "^2.66.2"
+    "@gi-types/pango" "^1.0.2"
+
+"@gi-types/gdkpixbuf@^2.0.2":
+  version "2.0.3"
+  resolved 
"https://registry.yarnpkg.com/@gi-types/gdkpixbuf/-/gdkpixbuf-2.0.3.tgz#a2a19b8d8604a5d5706dee43b051e129543480be";
+  integrity sha512-ATfMjEtmNHKNgjaBuIjpzh5RgTO1zk3D6ltwqHbPg5qQxWREe7kCSiI7eRsikP285u4e0tSGb6UJDamYZxUU2Q==
+  dependencies:
+    "@gi-types/gio" "^2.66.3"
+    "@gi-types/gobject" "^2.66.3"
+
+"@gi-types/gio@^2.66.2", "@gi-types/gio@^2.66.3":
+  version "2.66.3"
+  resolved 
"https://registry.yarnpkg.com/@gi-types/gio/-/gio-2.66.3.tgz#a9a071be3617d929778ee4036270031b341be2d8";
+  integrity sha512-ROjWtIqzWAODgEKIK0umz2i2kZoKiWks2beV111u+XOo/CdVpIQn3F8VJAAPhXoTD+hyhrBDt9NcnOQZGHV3ug==
+  dependencies:
+    "@gi-types/gobject" "^2.66.3"
+
+"@gi-types/glib@^2.66.2", "@gi-types/glib@^2.66.3":
+  version "2.66.3"
+  resolved 
"https://registry.yarnpkg.com/@gi-types/glib/-/glib-2.66.3.tgz#795e6566f25de5dddaab72fa30c16136334c80c7";
+  integrity sha512-EoT/w9ImW5QyMicZ/I9yh3CzgZWcTPrm96siyC5qApNzblLsNixt3MzPQvnFoKqdLNCHrbAcgXTHZKzyqVztww==
+
+"@gi-types/gobject@^2.66.2", "@gi-types/gobject@^2.66.3":
+  version "2.66.3"
+  resolved 
"https://registry.yarnpkg.com/@gi-types/gobject/-/gobject-2.66.3.tgz#41bc8b141a5bb78bcbce6252646d7ef71fa8b275";
+  integrity sha512-VP756l82z8jVPNWowpsdbJORYGx1HyZMxZzL10+FGTdgKiTzx+cMuMD066h4g2PYTTuOGy6vr+xbUcnhLonYhQ==
+  dependencies:
+    "@gi-types/glib" "^2.66.2"
+
+"@gi-types/gtk@^3.24.3":
+  version "3.24.3"
+  resolved 
"https://registry.yarnpkg.com/@gi-types/gtk/-/gtk-3.24.3.tgz#a5d5c12edf27313c54f22d8def69625e933eb0c7";
+  integrity sha512-nCyRGXsd8SJ94iqfrA8rCWBKC4x07YeUIBWeDCqJzU+9Gfacfl/XTxptoEFXRFnksrHRbj5DmKllix9N25U4KA==
+  dependencies:
+    "@gi-types/atk" "^2.36.3"
+    "@gi-types/gdk" "^3.24.2"
+    "@gi-types/gio" "^2.66.3"
+    "@gi-types/gobject" "^2.66.3"
+
+"@gi-types/pango@^1.0.2":
+  version "1.0.3"
+  resolved 
"https://registry.yarnpkg.com/@gi-types/pango/-/pango-1.0.3.tgz#4fba9b6c110180674ec42e3581fd061d12ef7f62";
+  integrity sha512-tQ7c5yf2cyla3ta5ctEOtI/z5nX4NJteYCV3XoNkC8EMRvcyiDqYi6tGUG3ErnUG4EjczgVBh902gZ2K5lQwvg==
+  dependencies:
+    "@gi-types/cairo" "^1.0.2"
+    "@gi-types/gobject" "^2.66.2"
+
 acorn-jsx@^5.2.0:
   version "5.3.1"
   resolved 
"https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b";
@@ -734,6 +802,11 @@ type-fest@^0.8.1:
   resolved 
"https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d";
   integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
 
+typescript@^4.0.5:
+  version "4.0.5"
+  resolved 
"https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389";
+  integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
+
 uri-js@^4.2.2:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602";


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