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




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

    esm: Enable static module imports.
    
    (Changes from Philip folded in: tests, moving file operations into
    internal.cpp, store module loader in global, some renames, some added
    comments)

 .reuse/dep5                                |   1 +
 doc/ESModules.md                           | 225 ++++++++++++++
 doc/Modules.md                             |  35 ++-
 gjs/atoms.h                                |   4 +
 gjs/context-private.h                      |   4 +
 gjs/context.cpp                            | 186 +++++++++---
 gjs/global.cpp                             |  80 +++++
 gjs/global.h                               |  11 +
 gjs/internal.cpp                           | 461 +++++++++++++++++++++++++++++
 gjs/internal.h                             |  54 ++++
 gjs/jsapi-util.cpp                         |  18 ++
 gjs/jsapi-util.h                           |   2 +
 gjs/module.cpp                             | 215 ++++++++++++++
 gjs/module.h                               |  15 +
 installed-tests/js/.eslintrc.yml           |   7 +
 installed-tests/js/jsunit.gresources.xml   |   3 +
 installed-tests/js/meson.build             |  19 ++
 installed-tests/js/modules/data.txt        |   1 +
 installed-tests/js/modules/exports.js      |  12 +
 installed-tests/js/modules/importmeta.js   |   7 +
 installed-tests/js/testESModules.js        |  56 ++++
 installed-tests/minijasmine-module.test.in |   7 +
 installed-tests/scripts/testCommandLine.sh |  11 +-
 js.gresource.xml                           |   6 +
 meson.build                                |   3 +-
 modules/esm/.eslintrc.yml                  |   7 +
 modules/esm/gi.js                          |  25 ++
 modules/internal/.eslintrc.yml             |  27 ++
 modules/internal/internalLoader.js         | 229 ++++++++++++++
 modules/internal/loader.js                 | 170 +++++++++++
 test/gjs-tests.cpp                         | 167 +++++++++++
 test/mock-js-resources.gresource.xml       |   5 +
 test/modules/default.js                    |   5 +
 test/modules/exit.js                       |   5 +
 test/modules/exit0.js                      |   5 +
 test/modules/import.js                     |   6 +
 test/modules/throws.js                     |   4 +
 37 files changed, 2054 insertions(+), 44 deletions(-)
---
diff --git a/.reuse/dep5 b/.reuse/dep5
index 7a10a45b..7c50dc32 100644
--- a/.reuse/dep5
+++ b/.reuse/dep5
@@ -2,6 +2,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
 
 Files: .gitlab/issue_templates/*
     doc/* CONTRIBUTING.md NEWS *README* tools/heapgraph.md tools/yarn.lock
+    installed-tests/js/modules/data.txt
 Copyright: No rights reserved
 License: CC0-1.0
 
diff --git a/doc/ESModules.md b/doc/ESModules.md
new file mode 100644
index 00000000..80017a37
--- /dev/null
+++ b/doc/ESModules.md
@@ -0,0 +1,225 @@
+# Modules: ECMAScript modules
+
+> _This documentation is inspired by [Node.js' 
documentation](https://github.com/nodejs/node/blob/master/doc/api/esm.md)
+> on ECMAScript modules._
+
+ECMAScript Modules or "ES modules" are the [official ECMAScript
+standard][] for importing, exporting, and reusing JavaScript code.
+
+ES modules can export `function`, `class`, `const`, `let`, and `var`
+statements using the `export` keyword.
+
+```js
+// animalSounds.js
+export function bark(num) {
+  log('bark');
+}
+
+export const ANIMALS = ['dog', 'cat'];
+```
+
+Other ES modules can then import those declarations using `import`
+statements like the one below.
+
+```js
+// main.js
+import { ANIMALS, bark } from './animalSounds.js';
+
+// Logs 'bark'
+bark();
+
+// Logs 'dog, cat'
+log(ANIMALS);
+```
+
+## Loading ES Modules
+
+### Command Line
+
+From the command line ES modules can be loaded with the `-m` flag:
+
+```sh
+gjs -m module.js
+```
+
+### JavaScript
+
+ES modules cannot be loaded from strings at this time.
+
+`import('./module.js')` can be used to load modules from any GJS script
+or module.
+`import` will always default to loading a file as an ES Module.
+
+### C API
+
+Using the C API in `gjs.h`, ES modules can be loaded from a file or
+resource using `gjs_load_module_file()`. <!-- TODO -->
+
+## `import` Specifiers
+
+### Terminology
+
+The _specifier_ of an `import` statement is the string after the `from`
+keyword, e.g. `'path'` in `import { sep } from 'path'`.
+Specifiers are also used in `export from` statements, and as the
+argument to an `import()` expression.
+
+There are three types of specifiers:
+
+* _Relative specifiers_ like `'./window.js'`.
+  They refer to a path relative to the location of the importing file.
+  _The file extension is always necessary for these._
+
+* _Bare specifiers_ like `'some-package'`.
+  In GJS bare specifiers typically refer to built-in modules like `gi`.
+
+* _Absolute specifiers_ like `'file:///usr/share/gjs-app/file.js'`.
+  They refer directly and explicitly to a full path or library.
+
+Bare specifier resolutions import built-in modules.
+All other specifier resolutions are always only resolved with the
+standard relative URL resolution semantics.
+
+### Mandatory file extensions
+
+A file extension must be provided when using the `import` keyword to
+resolve relative or absolute specifiers.
+Directory files (e.g. `'./extensions/__init__.js'`) must also be fully
+specified.
+
+The recommended replacement for directory files (`__init__.js`) is:
+
+```js
+'./extensions.js'
+'./extensions/a.js'
+'./extensions/b.js'
+```
+
+Because file extensions are required, folders and `.js` files with the
+same "name" should not conflict as they did with `imports`.
+
+### URLs
+
+ES modules are resolved and cached as URLs.
+This means that files containing special characters such as `#` and `?`
+need to be escaped.
+
+`file:`, `resource:`, and `gi:` URL schemes are supported.
+A specifier like `'https://example.com/app.js'` is not supported in GJS.
+
+#### `file:` URLs
+
+Modules are loaded multiple times if the `import` specifier used to
+resolve them has a different query or fragment.
+
+```js
+import './foo.js?query=1'; // loads ./foo.js with query of "?query=1"
+import './foo.js?query=2'; // loads ./foo.js with query of "?query=2"
+```
+
+The root directory may be referenced via `file:///`.
+
+#### `gi:` Imports
+
+`gi:` URLs are supported as an alternative means to load GI (GObject
+Introspected) modules.
+
+`gi:` URLs support declaring libraries' versions.
+An error will be thrown when resolving imports if multiple versions of a
+library are present and a version has not been specified.
+The version is cached, so it only needs to be specified once.
+
+```js
+import Gtk from 'gi://Gtk?version=4.0';
+import Gdk from 'gi://Gdk?version=4.0';
+import GLib from 'gi://GLib';
+// GLib, GObject, and Gio are required by GJS so no version is necessary.
+```
+
+It is recommended to create a "version block" at your application's
+entry point.
+
+```js
+import 'gi://Gtk?version=3.0'
+import 'gi://Gdk?version=3.0'
+import 'gi://Hdy?version=1.0'
+```
+
+After these declarations, you can import the libraries without version
+parameters.
+
+```js
+import Gtk from 'gi://Gtk';
+import Gdk from 'gi://Gdk';
+import Hdy from 'gi://Hdy';
+```
+
+## `import()` expressions
+
+Dynamic [`import()` statements][] are not currently supported in GJS.
+
+## `import.meta`
+
+* {Object}
+
+The `import.meta` meta property is an `Object` that contains the
+following properties:
+
+### `import.meta.url`
+
+* {string} The absolute `file:` or `resource:` URL of the module.
+
+This is identical to Node.js and browser environments.
+It will always provide the URI of the current module.
+
+This enables useful patterns such as relative file loading:
+
+```js
+import Gio from 'gi://Gio';
+const file = Gio.File.new_for_uri(import.meta.url);
+const data = file.get_parent().resolve_relative_path('data.json');
+const [, contents] = data.load_contents(null);
+```
+
+## Interoperability with legacy `imports` modules
+
+Because `imports` is a global object, it is still available in ES
+modules.
+It is not recommended to purposely mix import styles unless absolutely
+necessary.
+
+### `import` statements
+
+An `import` statement can only reference an ES module.
+`import` statements are permitted only in ES modules, but dynamic
+[`import()`][] expressions will be supported in legacy `imports` modules
+for loading ES modules.
+
+When importing legacy `imports` modules, all `var` declarations are
+provided as properties on the default export.
+
+### Differences between ES modules and legacy `imports` modules
+
+#### No `imports` and `var` exports
+
+You must use the [`export`][] syntax instead.
+
+#### No meta path properties
+
+These `imports` properties are not available in ES modules:
+
+ * `__modulePath__`
+ * `__moduleName__`
+ * `__parentModule__`
+
+`__modulePath__`, `__moduleName__` and `__parentModule__` use cases can
+be replaced with [`import.meta.url`][].
+
+[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
+[`import()`]: #esm_import_expressions
+[`import()` statements]: 
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports
+[`import.meta.url`]: #esm_import_meta_url
+[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
+[`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
+[special scheme]: https://url.spec.whatwg.org/#special-scheme
+[official ECMAScript standard]: https://tc39.github.io/ecma262/#sec-modules
diff --git a/doc/Modules.md b/doc/Modules.md
index c3b9baf4..6400cbed 100644
--- a/doc/Modules.md
+++ b/doc/Modules.md
@@ -2,7 +2,7 @@ GJS includes some built-in modules, as well as helpers for some core APIs like D
 
 ## [Gio](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/core/overrides/Gio.js)
 
-**Import with `const Gio = imports.gi.Gio;`**
+**Import with `const Gio = gi.require('Gio');` or `import Gio from 'gi://Gio'`**
 
 The `Gio` override includes a number of utilities for DBus that will be documented further at a later date. 
Below is a reasonable overview.
 
@@ -32,7 +32,7 @@ The `Gio` override includes a number of utilities for DBus that will be document
 
 ## [GLib](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/core/overrides/GLib.js)
 
-**Import with `const GLib = imports.gi.GLib;`**
+**Import with `const GLib = gi.require('GLib');` or `import GLib from 'gi://GLib'`**
 
 Mostly GVariant and GBytes compatibility.
 
@@ -43,13 +43,13 @@ Mostly GVariant and GBytes compatibility.
 
 ## [GObject](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/core/overrides/GObject.js)
 
-**Import with `const GObject = imports.gi.GObject;`**
+**Import with `const GObject = gi.require('GObject');` or `import GObject from 'gi://GObject'`**
 
 Mostly GObject implementation (properties, signals, GType mapping). May be useful as a reference.
 
 ## [Gtk](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/core/overrides/Gtk.js)
 
-**Import with `const Gtk = imports.gi.Gtk;`**
+**Import with `const Gtk = gi.require('Gtk', '3.0');` or `import Gtk from 'gi://Gtk'`**
 
 Mostly GtkBuilder/composite template implementation. May be useful as a reference.
 
@@ -57,8 +57,7 @@ Mostly GtkBuilder/composite template implementation. May be useful as a referenc
 **REMINDER:** You should specify a version prior to importing a library with multiple versions:
 
 ```js
-imports.gi.versions.Gtk = "3.0";
-const Gtk = imports.gi.Gtk;
+import Gtk from 'gi://Gtk?version=3.0';
 ```

 
@@ -284,10 +283,26 @@ Built-in version of the well-known [Tweener][tweener-www] animation/property tra
 
 ## GObject Introspection
 
+**Import with `import gi from 'gi';`**
+
+A wrapper of **libgirepository** to import native gobject-introspection libraries.
+
+* `gi.require(library: string, version?: string)`
+
+Loads a native gobject-introspection library.
+Version is required if more than one version of a library is installed.
+
+You can also import libraries through the `gi://` URL scheme.
+This function is only intended to be used when you want to import a
+library conditionally, since top-level import statements are resolved
+statically.
+
+### Legacy Imports (`imports.gi`)
+
 **Import with `const gi = imports.gi;`**
 
-A wrapper of **libgirepository** to import native gobject-introspection libraries. This
-object has a property `versions` which is an object on which you can set string-valued
+A wrapper for **libgirepository** is also available via the global `imports` object.
+This object has a property `versions` which is an object on which you can set string-valued
 properties indicating the version of that gobject-introspection library you want to load,
 and loading multiple versions in the same process is forbidden. So if you want to
 use gtk-3.0, set `imports.gi.versions.Gtk = '3.0';`.
@@ -295,7 +310,9 @@ use gtk-3.0, set `imports.gi.versions.Gtk = '3.0';`.
 Any other properties of `imports.gi` will attempt to import a gobject-introspection library
 with the property name, picking the latest version if there is no entry for it in `imports.gi.versions`.
 
-## More about **imports**
+## Legacy Imports
+
+Prior to the introduction of [ES Modules](ESModules.md), GJS had its own import system.
 
 **imports** is a global object that you can use to import any js file or GObject
 Introspection lib as module, there are 4 special properties of **imports**:
diff --git a/gjs/atoms.h b/gjs/atoms.h
index 966999c8..7fe061f1 100644
--- a/gjs/atoms.h
+++ b/gjs/atoms.h
@@ -35,9 +35,11 @@ class JSTracer;
     macro(gtype, "$gtype") \
     macro(height, "height") \
     macro(imports, "imports") \
+    macro(importSync, "importSync") \
     macro(init, "_init") \
     macro(instance_init, "_instance_init") \
     macro(interact, "interact") \
+    macro(internal, "internal") \
     macro(length, "length") \
     macro(line_number, "lineNumber") \
     macro(message, "message") \
@@ -56,6 +58,8 @@ class JSTracer;
     macro(signal_id, "signalId") \
     macro(stack, "stack") \
     macro(to_string, "toString") \
+    macro(uri, "uri") \
+    macro(url, "url") \
     macro(value_of, "valueOf") \
     macro(version, "version") \
     macro(versions, "versions") \
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 600ba05a..3a1db048 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;
@@ -170,6 +171,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 4f595048..c3810d97 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -33,11 +33,16 @@
 
 #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/ErrorReport.h>
+#include <js/Exception.h>           // for StealPendingExceptionStack
 #include <js/GCAPI.h>               // for JS_GC, JS_AddExtraGCRootsTr...
 #include <js/GCHashTable.h>         // for WeakCache
 #include <js/GCVector.h>            // for RootedVector
+#include <js/Id.h>
+#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 +67,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 +311,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 +399,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 +485,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 +503,83 @@ 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) {
-        gjs_log_exception(cx);
-        g_error("Failed to create root importer");
+    if (!gjs_define_global_properties(m_cx, internal_global,
+                                      GjsGlobalType::INTERNAL,
+                                      "GJS internal global", "nullptr")) {
+        gjs_log_exception(m_cx);
+        g_error("Failed to define properties on internal global object");
     }
 
-    g_assert(
-        gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS).isUndefined() &&
-        "Someone else already created root importer");
+    JS::RootedObject global(
+        m_cx,
+        gjs_create_global_object(cx, GjsGlobalType::DEFAULT, internal_global));
 
-    gjs_set_global_slot(global, GjsGlobalSlot::IMPORTS,
-                        JS::ObjectValue(*importer));
+    if (!global) {
+        gjs_log_exception(m_cx);
+        g_error("Failed to initialize global object");
+    }
+
+    m_global = global;
 
-    if (!gjs_define_global_properties(m_cx, global, GjsGlobalType::DEFAULT,
-                                      "GJS", "default")) {
+    {
+        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)) {
         gjs_log_exception(m_cx);
-        g_error("Failed to define properties on global object");
+        g_error("Failed to define module global in internal global.");
+    }
+
+    if (!gjs_load_internal_module(cx, "internalLoader")) {
+        gjs_log_exception(cx);
+        g_error("Failed to load internal module loaders.");
+    }
+
+    JS::RootedObject loader(
+        cx, gjs_module_load(
+                cx, "resource:///org/gnome/gjs/modules/internal/loader.js",
+                "resource:///org/gnome/gjs/modules/internal/loader.js"));
+
+    if (!loader) {
+        gjs_log_exception(cx);
+        g_error("Failed to load module loader module.");
+    }
+
+    if (!JS::ModuleInstantiate(cx, loader)) {
+        gjs_log_exception(cx);
+        g_error("Failed to instantiate module loader module.");
+    }
+
+    if (!JS::ModuleEvaluate(cx, loader)) {
+        gjs_log_exception(cx);
+        g_error("Failed to evaluate module loader module.");
     }
 }
 
@@ -1033,25 +1100,78 @@ 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;
-    if (error)
-        *error = nullptr;
+    AutoResetExit reset(this);
 
-    g_error(
-        "GjsContextPrivate::eval_module(%s) is not implemented. Exiting with "
-        "error.",
-        identifier);
+    bool auto_profile = auto_profile_enter();
 
-    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 load module with identifier: '%s'", identifier);
+        *exit_status_p = 1;
+        return false;
+    }
+
+    if (!JS::ModuleInstantiate(m_cx, obj)) {
+        gjs_log_exception(m_cx);
+        g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+                    "Failed to resolve imports for module: '%s'", identifier);
+        *exit_status_p = 1;
+        return false;
+    }
+
+    bool ok = true;
+    if (!JS::ModuleEvaluate(m_cx, obj))
+        ok = 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;
+    }
+
+    auto_profile_exit(auto_profile);
+
+    if (!ok) {
+        *exit_status_p = handle_exit_code("Module", identifier, error);
+        return false;
+    }
+
+    /* Assume success if no errors were thrown or exit code set. */
+    *exit_status_p = 0;
+    return true;
 }
 
 bool GjsContextPrivate::register_module(const char* identifier, const char* uri,
                                         GError** error) {
-    if (error)
-        *error = nullptr;
+    JSAutoRealm ar(m_cx, m_global);
 
-    g_warning("Identifier: %s\nURI: %s\n", identifier, uri);
-    return true;
+    if (gjs_module_load(m_cx, identifier, uri))
+        return true;
+
+    const char* msg = "unknown";
+    JS::ExceptionStack exn_stack(m_cx);
+    JS::ErrorReportBuilder builder(m_cx);
+    if (JS::StealPendingExceptionStack(m_cx, &exn_stack) &&
+        builder.init(m_cx, exn_stack,
+                     JS::ErrorReportBuilder::WithSideEffects)) {
+        msg = builder.toStringResult().c_str();
+    } else {
+        JS_ClearPendingException(m_cx);
+    }
+
+    g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+                "Failed to parse module '%s': %s", identifier,
+                msg ? msg : "unknown");
+
+    return false;
 }
 
 bool
diff --git a/gjs/global.cpp b/gjs/global.cpp
index 3a1fc84b..6e0dac3b 100644
--- a/gjs/global.cpp
+++ b/gjs/global.cpp
@@ -31,6 +31,7 @@
 #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 +188,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 +263,66 @@ 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_get_registry, 1, 0),
+        JS_FN("loadResourceOrFile", gjs_internal_load_resource_or_file, 1, 0),
+        JS_FN("parseURI", gjs_internal_parse_uri, 1, 0),
+        JS_FN("resolveRelativeResourceOrFile",
+              gjs_internal_resolve_relative_resource_or_file, 2, 0),
+        JS_FN("setGlobalModuleLoader", gjs_internal_set_global_module_loader, 2,
+              0),
+        JS_FN("setModulePrivate", gjs_internal_set_module_private, 2, 0),
+        JS_FN("uriExists", gjs_internal_uri_exists, 1, 0),
+        JS_FS_END};
+
+    static constexpr JSClass klass = {
+        "GjsInternalGlobal",
+        JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(
+            static_cast<uint32_t>(GjsInternalGlobalSlot::LAST)),
+        &defaultclassops,
+    };
+
+ 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) {
+        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));
+
+        return JS_DefineFunctions(cx, global, static_funcs);
+    }
+};
+
 /**
  * gjs_create_global_object:
  * @cx: a #JSContext
@@ -273,6 +341,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 +354,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 +508,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 +532,7 @@ 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::static_funcs) constexpr GjsInternalGlobal::static_funcs;
diff --git a/gjs/global.h b/gjs/global.h
index 2b7c0e16..569a8ce1 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,10 @@ enum class GjsDebuggerGlobalSlot : uint32_t {
 
 enum class GjsGlobalSlot : uint32_t {
     IMPORTS = static_cast<uint32_t>(GjsBaseGlobalSlot::LAST),
+    // Stores an object with methods to resolve and load modules
+    MODULE_LOADER,
+    // Stores the module registry (a Map object)
+    MODULE_REGISTRY,
     NATIVE_REGISTRY,
     PROTOTYPE_gtype,
     PROTOTYPE_importer,
@@ -58,6 +63,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 +98,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 +108,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..184f2457
--- /dev/null
+++ b/gjs/internal.cpp
@@ -0,0 +1,461 @@
+// 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/byteArray.h"
+#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/modules/internal/{#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/modules/internal/%s.js", identifier));
+
+    gjs_debug(GJS_DEBUG_IMPORTER, "Loading internal module '%s' (%s)",
+              identifier, full_path.get());
+
+    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);
+    g_free(script);
+
+    // 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)) {
+        return false;
+    }
+
+    return true;
+}
+
+/**
+ * gjs_internal_set_global_module_loader:
+ *
+ * @brief Sets the MODULE_LOADER slot of the passed global object.
+ * The second argument should be an instance of ModuleLoader or
+ * InternalModuleLoader. Its moduleResolveHook and moduleLoadHook properties
+ * will be called.
+ *
+ * @returns guaranteed to return true or assert.
+ */
+bool gjs_internal_set_global_module_loader(JSContext*, unsigned argc,
+                                           JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    g_assert(args.length() == 2 && "setGlobalModuleLoader takes 2 arguments");
+
+    JS::Value v_global = args[0];
+    JS::Value v_loader = args[1];
+
+    g_assert(v_global.isObject() && "first argument must be an object");
+    g_assert(v_loader.isObject() && "second argument must be an object");
+
+    gjs_set_global_slot(&v_global.toObject(), GjsGlobalSlot::MODULE_LOADER,
+                        v_loader);
+
+    args.rval().setUndefined();
+    return true;
+}
+
+/**
+ * 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::TakeOwnership))
+        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 = JS::CallArgsFromVp(argc, vp);
+    g_assert(args.length() == 2 && "compileInternalModule takes 2 arguments");
+
+    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 = JS::CallArgsFromVp(argc, vp);
+    g_assert(args.length() == 2 && "compileModule takes 2 arguments");
+
+    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 = JS::CallArgsFromVp(argc, vp);
+    g_assert(args.length() == 2 && "setModulePrivate takes 2 arguments");
+    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_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_get_registry(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    g_assert(args.length() == 1 && "getRegistry takes 1 argument");
+    g_assert(args[0].isObject());
+
+    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;
+}
+
+bool gjs_internal_parse_uri(JSContext* cx, unsigned argc, JS::Value* vp) {
+    using AutoHashTable =
+        GjsAutoPointer<GHashTable, GHashTable, g_hash_table_destroy>;
+    using AutoURI = GjsAutoPointer<GUri, GUri, g_uri_unref>;
+
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    g_assert(args.length() == 1 && "parseUri() takes one string argument");
+    g_assert(args[0].isString() && "parseUri() takes one string argument");
+
+    JS::RootedString string_arg(cx, args[0].toString());
+    JS::UniqueChars uri = JS_EncodeStringToUTF8(cx, string_arg);
+    if (!uri)
+        return false;
+
+    GError* error = nullptr;
+    AutoURI parsed = g_uri_parse(uri.get(), G_URI_FLAGS_NONE, &error);
+    if (!parsed) {
+        gjs_throw_custom(cx, JSProto_Error, "ImportError",
+                         "Attempted to import invalid URI: %s (%s)", uri.get(),
+                         error->message);
+        g_clear_error(&error);
+        return false;
+    }
+
+    JS::RootedObject query_obj(cx, JS_NewPlainObject(cx));
+    if (!query_obj)
+        return false;
+
+    const char* raw_query = g_uri_get_query(parsed);
+    if (raw_query) {
+        AutoHashTable query =
+            g_uri_parse_params(raw_query, -1, "&", G_URI_PARAMS_NONE, &error);
+        if (!query) {
+            gjs_throw_custom(cx, JSProto_Error, "ImportError",
+                             "Attempted to import invalid URI: %s (%s)",
+                             uri.get(), error->message);
+            g_clear_error(&error);
+            return false;
+        }
+
+        GHashTableIter iter;
+        g_hash_table_iter_init(&iter, query);
+
+        void* key_ptr;
+        void* value_ptr;
+        while (g_hash_table_iter_next(&iter, &key_ptr, &value_ptr)) {
+            auto* key = static_cast<const char*>(key_ptr);
+            auto* value = static_cast<const char*>(value_ptr);
+
+            JS::ConstUTF8CharsZ value_chars{value, strlen(value)};
+            JS::RootedString value_str(cx,
+                                       JS_NewStringCopyUTF8Z(cx, value_chars));
+            if (!value_str || !JS_DefineProperty(cx, query_obj, key, value_str,
+                                                 JSPROP_ENUMERATE))
+                return false;
+        }
+    }
+
+    JS::RootedObject return_obj(cx, JS_NewPlainObject(cx));
+    if (!return_obj)
+        return false;
+
+    // JS_NewStringCopyZ() used here and below because the URI components are
+    // %-encoded, meaning ASCII-only
+    JS::RootedString scheme(cx,
+                            JS_NewStringCopyZ(cx, g_uri_get_scheme(parsed)));
+    if (!scheme)
+        return false;
+
+    JS::RootedString host(cx, JS_NewStringCopyZ(cx, g_uri_get_host(parsed)));
+    if (!host)
+        return false;
+
+    JS::RootedString path(cx, JS_NewStringCopyZ(cx, g_uri_get_path(parsed)));
+    if (!path)
+        return false;
+
+    if (!JS_DefineProperty(cx, return_obj, "uri", string_arg,
+                           JSPROP_ENUMERATE) ||
+        !JS_DefineProperty(cx, return_obj, "scheme", scheme,
+                           JSPROP_ENUMERATE) ||
+        !JS_DefineProperty(cx, return_obj, "host", host, JSPROP_ENUMERATE) ||
+        !JS_DefineProperty(cx, return_obj, "path", path, JSPROP_ENUMERATE) ||
+        !JS_DefineProperty(cx, return_obj, "query", query_obj,
+                           JSPROP_ENUMERATE))
+        return false;
+
+    args.rval().setObject(*return_obj);
+    return true;
+}
+
+bool gjs_internal_resolve_relative_resource_or_file(JSContext* cx,
+                                                    unsigned argc,
+                                                    JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    g_assert(args.length() == 2 && "resolveRelativeResourceOrFile(str, str)");
+    g_assert(args[0].isString() && "resolveRelativeResourceOrFile(str, str)");
+    g_assert(args[1].isString() && "resolveRelativeResourceOrFile(str, str)");
+
+    JS::RootedString string_arg(cx, args[0].toString());
+    JS::UniqueChars uri = JS_EncodeStringToUTF8(cx, string_arg);
+    if (!uri)
+        return false;
+    string_arg = args[1].toString();
+    JS::UniqueChars relative_path = JS_EncodeStringToUTF8(cx, string_arg);
+    if (!relative_path)
+        return false;
+
+    GjsAutoUnref<GFile> module_file = g_file_new_for_uri(uri.get());
+    GjsAutoUnref<GFile> module_parent_file = g_file_get_parent(module_file);
+
+    if (module_parent_file) {
+        GjsAutoUnref<GFile> output = g_file_resolve_relative_path(
+            module_parent_file, relative_path.get());
+        GjsAutoChar output_uri = g_file_get_uri(output);
+
+        JS::ConstUTF8CharsZ uri_chars(output_uri, strlen(output_uri));
+        JS::RootedString retval(cx, JS_NewStringCopyUTF8Z(cx, uri_chars));
+        if (!retval)
+            return false;
+
+        args.rval().setString(retval);
+        return true;
+    }
+
+    args.rval().setNull();
+    return true;
+}
+
+bool gjs_internal_load_resource_or_file(JSContext* cx, unsigned argc,
+                                        JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    g_assert(args.length() == 1 && "loadResourceOrFile(str)");
+    g_assert(args[0].isString() && "loadResourceOrFile(str)");
+
+    JS::RootedString string_arg(cx, args[0].toString());
+    JS::UniqueChars uri = JS_EncodeStringToUTF8(cx, string_arg);
+    if (!uri)
+        return false;
+
+    GjsAutoUnref<GFile> file = g_file_new_for_uri(uri.get());
+
+    char* contents;
+    size_t length;
+    GError* error = nullptr;
+    if (!g_file_load_contents(file, /* cancellable = */ nullptr, &contents,
+                              &length, /* etag_out = */ nullptr, &error)) {
+        gjs_throw_custom(cx, JSProto_Error, "ImportError",
+                         "Unable to load file from: %s (%s)", uri.get(),
+                         error->message);
+        g_clear_error(&error);
+        return false;
+    }
+
+    JS::ConstUTF8CharsZ contents_chars{contents, length};
+    JS::RootedString contents_str(cx,
+                                  JS_NewStringCopyUTF8Z(cx, contents_chars));
+    g_free(contents);
+    if (!contents_str)
+        return false;
+
+    args.rval().setString(contents_str);
+    return true;
+}
+
+bool gjs_internal_uri_exists(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    g_assert(args.length() >= 1 && "uriExists(str)");  // extra args are OK
+    g_assert(args[0].isString() && "uriExists(str)");
+
+    JS::RootedString string_arg(cx, args[0].toString());
+    JS::UniqueChars uri = JS_EncodeStringToUTF8(cx, string_arg);
+    if (!uri)
+        return false;
+
+    GjsAutoUnref<GFile> file = g_file_new_for_uri(uri.get());
+
+    args.rval().setBoolean(g_file_query_exists(file, nullptr));
+    return true;
+}
diff --git a/gjs/internal.h b/gjs/internal.h
new file mode 100644
index 00000000..212e5458
--- /dev/null
+++ b/gjs/internal.h
@@ -0,0 +1,54 @@
+// 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>
+
+#include "gjs/macros.h"
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_load_internal_module(JSContext* cx, const char* identifier);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_compile_module(JSContext* cx, unsigned argc, JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_compile_internal_module(JSContext* cx, unsigned argc,
+                                          JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_get_registry(JSContext* cx, unsigned argc, JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_set_global_module_loader(JSContext* cx, unsigned argc,
+                                           JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_set_module_private(JSContext* cx, unsigned argc,
+                                     JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_parse_uri(JSContext* cx, unsigned argc, JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_resolve_relative_resource_or_file(JSContext* cx,
+                                                    unsigned argc,
+                                                    JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_load_resource_or_file(JSContext* cx, unsigned argc,
+                                        JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_load_resource_or_file_async(JSContext* cx, unsigned argc,
+                                              JS::Value* vp);
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_internal_uri_exists(JSContext* cx, unsigned argc, JS::Value* vp);
+
+#endif  // GJS_INTERNAL_H_
diff --git a/gjs/jsapi-util.cpp b/gjs/jsapi-util.cpp
index 1e7d2aa8..6262ac2d 100644
--- a/gjs/jsapi-util.cpp
+++ b/gjs/jsapi-util.cpp
@@ -621,6 +621,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 cx 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 273e9a29..11c23776 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..c810f7c9 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -5,6 +5,7 @@
 #include <config.h>
 
 #include <stddef.h>     // for size_t
+#include <string.h>
 #include <sys/types.h>  // for ssize_t
 
 #include <string>  // for u16string
@@ -12,22 +13,31 @@
 #include <gio/gio.h>
 #include <glib.h>
 
+#include <js/CallArgs.h>
+#include <js/CharacterEncoding.h>  // for ConstUTF8CharsZ
 #include <js/Class.h>
 #include <js/CompilationAndEvaluation.h>
 #include <js/CompileOptions.h>
+#include <js/Conversions.h>
 #include <js/GCVector.h>  // for RootedVector
+#include <js/Id.h>
 #include <js/PropertyDescriptor.h>
 #include <js/RootingAPI.h>
 #include <js/SourceText.h>
 #include <js/TypeDecls.h>
+#include <js/Utility.h>  // for UniqueChars
 #include <js/Value.h>
+#include <js/ValueArray.h>
 #include <jsapi.h>  // for JS_DefinePropertyById, ...
 
+#include "gjs/atoms.h"
 #include "gjs/context-private.h"
 #include "gjs/global.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"
 
 class GjsScriptModule {
@@ -269,3 +279,208 @@ 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.
+ *
+ * @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 v_loader(
+        cx, gjs_get_global_slot(global, GjsGlobalSlot::MODULE_LOADER));
+    g_assert(v_loader.isObject());
+    JS::RootedObject loader(cx, &v_loader.toObject());
+
+    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));
+    if (!id)
+        return nullptr;
+    JS::RootedString uri(cx, JS_NewStringCopyUTF8Z(cx, uri_chars));
+    if (!uri)
+        return nullptr;
+
+    JS::RootedValueArray<2> args(cx);
+    args[0].setString(id);
+    args[1].setString(uri);
+
+    gjs_debug(GJS_DEBUG_IMPORTER,
+              "Module resolve hook for module '%s' (%s), global %p", identifier,
+              file_uri, global.get());
+
+    JS::RootedValue result(cx);
+    if (!JS::Call(cx, loader, "moduleLoadHook", args, &result))
+        return nullptr;
+
+    g_assert(result.isObject() && "Module hook failed to return an object!");
+    return &result.toObject();
+}
+
+/**
+ * import_native_module_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.
+ *
+ * Compare gjs_import_native_module() for the legacy importer.
+ *
+ * @param cx the current JSContext
+ * @param argc
+ * @param vp
+ *
+ * @returns whether an error occurred while importing the native module.
+ */
+static bool import_native_module_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_populate_module_meta:
+ *
+ * Hook SpiderMonkey calls to populate the import.meta object.
+ * Defines a property "import.meta.url", and additionally a method
+ * "import.meta.importSync" if this is an internal module.
+ *
+ * @param private_ref the private value for the #Module object
+ * @param meta 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) {
+    g_assert(private_ref.isObject());
+    JS::RootedObject module(cx, &private_ref.toObject());
+
+    gjs_debug(GJS_DEBUG_IMPORTER, "Module metadata hook for module %p",
+              &private_ref.toObject());
+
+    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+    JS::RootedValue v_uri(cx);
+    if (!JS_GetPropertyById(cx, module, atoms.uri(), &v_uri) ||
+        !JS_DefinePropertyById(cx, meta, atoms.url(), v_uri,
+                               GJS_MODULE_PROP_FLAGS))
+        return false;
+
+    JS::RootedValue v_internal(cx);
+    if (!JS_GetPropertyById(cx, module, atoms.internal(), &v_internal))
+        return false;
+    if (JS::ToBoolean(v_internal)) {
+        gjs_debug(GJS_DEBUG_IMPORTER, "Defining meta.importSync for module %p",
+                  &private_ref.toObject());
+        if (!JS_DefineFunctionById(cx, meta, atoms.importSync(),
+                                   import_native_module_sync, 1,
+                                   GJS_MODULE_PROP_FLAGS))
+            return false;
+    }
+
+    return true;
+}
+
+/**
+ * gjs_module_resolve:
+ *
+ * Hook SpiderMonkey calls to resolve import specifiers.
+ *
+ * @param importingModulePriv the private value of the #Module object initiating
+ *   the import.
+ * @param specifier the import specifier to resolve
+ *
+ * @returns whether an error occurred while resolving the specifier.
+ */
+JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importingModulePriv,
+                             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.");
+    g_assert(importingModulePriv.isObject() &&
+             "the importing module can't be null, don't add import to the "
+             "bootstrap script");
+
+    JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
+    JS::RootedValue v_loader(
+        cx, gjs_get_global_slot(global, GjsGlobalSlot::MODULE_LOADER));
+    g_assert(v_loader.isObject());
+    JS::RootedObject loader(cx, &v_loader.toObject());
+
+    JS::RootedValueArray<2> args(cx);
+    args[0].set(importingModulePriv);
+    args[1].setString(specifier);
+
+    gjs_debug(GJS_DEBUG_IMPORTER,
+              "Module resolve hook for module '%s' (relative to %p), global %p",
+              gjs_debug_string(specifier).c_str(),
+              &importingModulePriv.toObject(), global.get());
+
+    JS::RootedValue result(cx);
+    if (!JS::Call(cx, loader, "moduleResolveHook", args, &result))
+        return nullptr;
+
+    g_assert(result.isObject() && "resolve hook failed to return an object!");
+    return &result.toObject();
+}
diff --git a/gjs/module.h b/gjs/module.h
index 0d35af26..c754b114 100644
--- a/gjs/module.h
+++ b/gjs/module.h
@@ -24,4 +24,19 @@ 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* cx, JS::HandleValue private_ref,
+                              JS::HandleObject meta_object);
+
 #endif  // GJS_MODULE_H_
diff --git a/installed-tests/js/.eslintrc.yml b/installed-tests/js/.eslintrc.yml
index a9c37e58..7f329a58 100644
--- a/installed-tests/js/.eslintrc.yml
+++ b/installed-tests/js/.eslintrc.yml
@@ -29,3 +29,10 @@ globals:
   clearTimeout: writable
   setInterval: writable
   setTimeout: writable
+overrides:
+  - files:
+      - testESModules.js
+      - modules/importmeta.js
+      - modules/exports.js
+    parserOptions:
+      sourceType: module
diff --git a/installed-tests/js/jsunit.gresources.xml b/installed-tests/js/jsunit.gresources.xml
index 674e342f..3e100b1b 100644
--- a/installed-tests/js/jsunit.gresources.xml
+++ b/installed-tests/js/jsunit.gresources.xml
@@ -12,6 +12,9 @@
     <file>modules/badOverrides/Gio.js</file>
     <file>modules/badOverrides/Regress.js</file>
     <file>modules/badOverrides/WarnLib.js</file>
+    <file>modules/data.txt</file>
+    <file>modules/importmeta.js</file>
+    <file>modules/exports.js</file>
     <file>modules/foobar.js</file>
     <file>modules/lexicalScope.js</file>
     <file>modules/modunicode.js</file>
diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build
index dae48e07..58385030 100644
--- a/installed-tests/js/meson.build
+++ b/installed-tests/js/meson.build
@@ -194,3 +194,22 @@ gdbus_test_description = configure_file(
 if get_option('installed_tests')
     install_data('testGDBus.js', install_dir: installed_js_tests_dir)
 endif
+
+# testESModules.js is also separate because it needs an extra minijasmine flag
+
+test('ESModules', minijasmine, args: [files('testESModules.js'), '-m'],
+    env: tests_environment, protocol: 'tap', suite: 'JS')
+
+esm_test_description_subst = {
+    'name': 'testESModules.js',
+    'installed_tests_execdir': installed_tests_execdir,
+}
+esm_test_description = configure_file(
+    configuration: esm_test_description_subst,
+    input: '../minijasmine-module.test.in', output: 'testESModules.test',
+    install: get_option('installed_tests'),
+    install_dir: installed_tests_metadir)
+
+if get_option('installed_tests')
+    install_data('testESModules.js', install_dir: installed_js_tests_dir)
+endif
diff --git a/installed-tests/js/modules/data.txt b/installed-tests/js/modules/data.txt
new file mode 100644
index 00000000..082b3465
--- /dev/null
+++ b/installed-tests/js/modules/data.txt
@@ -0,0 +1 @@
+test data
diff --git a/installed-tests/js/modules/exports.js b/installed-tests/js/modules/exports.js
new file mode 100644
index 00000000..26e76c17
--- /dev/null
+++ b/installed-tests/js/modules/exports.js
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+import Gio from 'gi://Gio';
+
+export default 5;
+
+export const NamedExport = 'Hello, World';
+
+const thisFile = Gio.File.new_for_uri(import.meta.url);
+const dataFile = thisFile.get_parent().resolve_relative_path('data.txt');
+export const [, data] = dataFile.load_contents(null);
diff --git a/installed-tests/js/modules/importmeta.js b/installed-tests/js/modules/importmeta.js
new file mode 100644
index 00000000..f1c7e402
--- /dev/null
+++ b/installed-tests/js/modules/importmeta.js
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+
+if (typeof import.meta.importSync !== 'undefined')
+    throw new Error('internal import meta property should not be visible in userland');
+
+export default Object.keys(import.meta);
diff --git a/installed-tests/js/testESModules.js b/installed-tests/js/testESModules.js
new file mode 100644
index 00000000..ea551485
--- /dev/null
+++ b/installed-tests/js/testESModules.js
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+import gi from 'gi';
+import Gio from 'gi://Gio';
+
+import $ from 'resource:///org/gjs/jsunit/modules/exports.js';
+import {NamedExport, data} from 'resource:///org/gjs/jsunit/modules/exports.js';
+import metaProperties from 'resource:///org/gjs/jsunit/modules/importmeta.js';
+
+describe('ES module imports', function () {
+    it('default import', function () {
+        expect($).toEqual(5);
+    });
+
+    it('named import', function () {
+        expect(NamedExport).toEqual('Hello, World');
+    });
+
+    it('GObject introspection import', function () {
+        expect(gi.require('GObject').toString()).toEqual('[object GIRepositoryNamespace]');
+    });
+
+    it('import with version parameter', function () {
+        expect(gi.require('GObject', '2.0')).toBe(gi.require('GObject'));
+    });
+
+    it('import again with other version parameter', function () {
+        expect(() => gi.require('GObject', '1.75')).toThrow();
+    });
+
+    it('import for the first time with wrong version', function () {
+        expect(() => gi.require('Gtk', '1.75')).toThrow();
+    });
+
+    it('import nonexistent module', function () {
+        expect(() => gi.require('PLib')).toThrow();
+    });
+
+    it('GObject introspection import via URL scheme', function () {
+        expect(Gio.toString()).toEqual('[object GIRepositoryNamespace]');
+    });
+
+    it('import.meta.url', function () {
+        expect(import.meta.url).toMatch(/\/installed-tests\/js\/testESModules\.js$/);
+    });
+
+    it('finds files relative to import.meta.url', function () {
+        // this data is loaded inside exports.js relative to import.meta.url
+        expect(data).toEqual(Uint8Array.from('test data\n', c => c.codePointAt()));
+    });
+
+    it('does not expose internal import.meta properties to userland modules', function () {
+        expect(metaProperties).toEqual(['url']);
+    });
+});
diff --git a/installed-tests/minijasmine-module.test.in b/installed-tests/minijasmine-module.test.in
new file mode 100644
index 00000000..e9e6b700
--- /dev/null
+++ b/installed-tests/minijasmine-module.test.in
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+# SPDX-FileCopyrightText: 2020 Philip Chimento <philip chimento gmail com>
+
+[Test]
+Type=session
+Exec=@installed_tests_execdir@/minijasmine @installed_tests_execdir@/js/@name@ -m
+Output=TAP
diff --git a/installed-tests/scripts/testCommandLine.sh b/installed-tests/scripts/testCommandLine.sh
index 2d679e72..539e6929 100755
--- a/installed-tests/scripts/testCommandLine.sh
+++ b/installed-tests/scripts/testCommandLine.sh
@@ -59,6 +59,12 @@ async function bar() {
 bar();
 EOF
 
+# this JS script should fail to import a second version of the same namespace
+cat <<EOF >doublegi.js
+import 'gi://Gio?version=2.0';
+import 'gi://Gio?version=75.94';
+EOF
+
 total=0
 
 report () {
@@ -256,6 +262,9 @@ grep -q TN: coverage.lcov
 report "coverage prefix is treated as an absolute path"
 rm -f coverage.lcov
 
-rm -f exit.js help.js promise.js awaitcatch.js
+$gjs -m doublegi.js 2>&1 | grep -q 'already loaded'
+report "avoid statically importing two versions of the same module"
+
+rm -f exit.js help.js promise.js awaitcatch.js doublegi.js
 
 echo "1..$total"
diff --git a/js.gresource.xml b/js.gresource.xml
index bdb6b665..61193ab0 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -3,6 +3,12 @@
 <!-- SPDX-FileCopyrightText: 2014 Red Hat, Inc. -->
 <gresources>
   <gresource prefix="/org/gnome/gjs">
+    <!-- Internal modules -->
+    <file>modules/internal/internalLoader.js</file>
+    <file>modules/internal/loader.js</file>
+
+    <!-- ESM-based modules -->
+    <file>modules/esm/gi.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 b3ac224d..27a6cb9b 100644
--- a/meson.build
+++ b/meson.build
@@ -113,7 +113,7 @@ endif
 
 ### Check for required libraries ###############################################
 
-glib_required_version = '>= 2.58.0'
+glib_required_version = '>= 2.66.0'
 glib = dependency('glib-2.0', version: glib_required_version,
     fallback: ['glib', 'libglib_dep'])
 gthread = dependency('gthread-2.0', version: glib_required_version,
@@ -402,6 +402,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..d5d3ca16
--- /dev/null
+++ b/modules/esm/.eslintrc.yml
@@ -0,0 +1,7 @@
+---
+# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+# SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+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..3b2a5c4c
--- /dev/null
+++ b/modules/esm/gi.js
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+const gi = import.meta.importSync('gi');
+
+const Gi = {
+    require(namespace, version = undefined) {
+        if (namespace === 'versions')
+            throw new Error('Cannot import namespace "versions", use the version parameter of Gi.require to 
specify versions.');
+
+        if (version !== undefined) {
+            const alreadyLoadedVersion = gi.versions[namespace];
+            if (alreadyLoadedVersion !== undefined && version !== alreadyLoadedVersion) {
+                throw new Error(`Version ${alreadyLoadedVersion} of GI module ${
+                    namespace} already loaded, cannot load version ${version}`);
+            }
+            gi.versions[namespace] = version;
+        }
+
+        return gi[namespace];
+    },
+};
+Object.freeze(Gi);
+
+export default Gi;
diff --git a/modules/internal/.eslintrc.yml b/modules/internal/.eslintrc.yml
new file mode 100644
index 00000000..b06bc212
--- /dev/null
+++ b/modules/internal/.eslintrc.yml
@@ -0,0 +1,27 @@
+---
+# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+# SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+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
+  moduleGlobalThis: readonly
+  compileModule: readonly
+  compileInternalModule: readonly
+  loadResourceOrFile: readonly
+  parseURI: readonly
+  uriExists: readonly
+  resolveRelativeResourceOrFile: readonly
+  setGlobalModuleLoader: readonly
+  setModulePrivate: readonly
+  getRegistry: readonly
diff --git a/modules/internal/internalLoader.js b/modules/internal/internalLoader.js
new file mode 100644
index 00000000..b1023132
--- /dev/null
+++ b/modules/internal/internalLoader.js
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+// InternalModuleLoader is the mechanism present on the internal global object
+// which resolves (translates a module specifier such as './mymod.js' or 'gi' to
+// an actual file) and loads internal modules.
+// It is used to bootstrap the actual module loader in loader.js, and also
+// serves as its base class.
+
+/** @typedef {{ uri: string; scheme: string; host: string; path: string; query: Query }} Uri */
+
+/**
+ * Use '__internal: never' to prevent any object from being type compatible with Module
+ * because it is an internal type.
+ *
+ * @typedef {{__internal: never;}} Module
+ */
+/** @typedef {typeof moduleGlobalThis | typeof globalThis} Global */
+/** @typedef {{ load(uri: Uri): [contents: string, internal: boolean]; }} SchemeHandler */
+/** @typedef {{ [key: string]: string | undefined; }} Query */
+/** @typedef {(uri: string, contents: string) => Module} CompileFunc */
+
+/**
+ * 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';
+    }
+}
+
+/**
+ * ModulePrivate is the "private" object of every module.
+ */
+export class ModulePrivate {
+    /**
+     *
+     * @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. Note that this doesn't mean "relative
+    // path" in the GLib sense, as in "not absolute" — it means a relative path
+    // module specifier, which must start with a '.' or '..' path component.
+    return path.startsWith('./') || path.startsWith('../');
+}
+
+/**
+ * Handles resolving and loading URIs.
+ *
+ * @class
+ */
+export class InternalModuleLoader {
+    /**
+     * @param {typeof globalThis} global the global object to handle module
+     *   resolution
+     * @param {(string, string) => import("../types").Module} compileFunc the
+     *   function to compile a source into a module for a particular global
+     *   object. Should be compileInternalModule() for InternalModuleLoader,
+     *   but overridden in ModuleLoader
+     */
+    constructor(global, compileFunc) {
+        this.global = global;
+        this.compileFunc = compileFunc;
+    }
+
+    /**
+     * Loads a file or resource URI synchronously
+     *
+     * @param {Uri} uri the file or resource URI to load
+     * @returns {[contents: string, internal?: 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 it can't be parsed as a URI, try a relative path or return null.
+        }
+
+        if (isRelativePath(specifier)) {
+            if (!parentURI)
+                throw new ImportError('Cannot import 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} importingModuleURI the URI of the module triggering this
+     *   resolve
+     * @returns {Uri}
+     */
+    resolveRelativePath(relativePath, importingModuleURI) {
+        // Ensure the parent URI is valid.
+        parseURI(importingModuleURI);
+
+        // Handle relative imports from URI-based modules.
+        const relativeURI = resolveRelativeResourceOrFile(importingModuleURI, relativePath);
+        if (!relativeURI)
+            throw new ImportError('File does not have a valid parent!');
+        return parseURI(relativeURI);
+    }
+
+    /**
+     * Compiles a module source text with the module's URI
+     *
+     * @param {ModulePrivate} priv a module private object
+     * @param {string} text the module source text to compile
+     * @returns {Module}
+     */
+    compileModule(priv, text) {
+        const compiled = this.compileFunc(priv.uri, text);
+
+        setModulePrivate(compiled, priv);
+
+        return compiled;
+    }
+
+    /**
+     * @param {string} specifier the specifier (e.g. relative path, root package) to resolve
+     * @param {string | null} importingModuleURI the URI of the module
+     *   triggering this resolve
+     *
+     * @returns {Module | null}
+     */
+    resolveModule(specifier, importingModuleURI) {
+        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, importingModuleURI);
+        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 priv = new ModulePrivate(uri.uri, uri.uri, internal);
+            const compiled = this.compileModule(priv, text);
+
+            registry.set(uri.uri, compiled);
+            return compiled;
+        }
+
+        return null;
+    }
+
+    moduleResolveHook(importingModulePriv, specifier) {
+        const resolved = this.resolveModule(specifier, importingModulePriv.uri ?? null);
+        if (!resolved)
+            throw new ImportError(`Module not found: ${specifier}`);
+
+        return resolved;
+    }
+
+    moduleLoadHook(id, uri) {
+        const priv = new ModulePrivate(id, uri);
+
+        const result = this.loadURI(parseURI(uri));
+        // result can only be null if `this` is InternalModuleLoader. If `this`
+        // is ModuleLoader, then loadURI() will have thrown
+        if (!result)
+            throw new ImportError(`URI not found: ${uri}`);
+
+        const [text] = result;
+        const compiled = this.compileModule(priv, text);
+
+        const registry = getRegistry(this.global);
+        registry.set(id, compiled);
+
+        return compiled;
+    }
+}
+
+export const internalModuleLoader = new InternalModuleLoader(globalThis, compileInternalModule);
+setGlobalModuleLoader(globalThis, internalModuleLoader);
diff --git a/modules/internal/loader.js b/modules/internal/loader.js
new file mode 100644
index 00000000..fde96eb0
--- /dev/null
+++ b/modules/internal/loader.js
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
+
+import {ImportError, InternalModuleLoader, ModulePrivate} from './internalLoader.js';
+
+class ModuleLoader extends InternalModuleLoader {
+    /**
+     * @param {typeof moduleGlobalThis} global the global object to register modules with.
+     */
+    constructor(global) {
+        // Sets 'compileFunc' in InternalModuleLoader to be 'compileModule'
+        super(global, compileModule);
+
+        /**
+         * @type {Set<string>}
+         *
+         * The set of "module" URIs (the module search path)
+         * For example, having "resource:///org/gnome/gjs/modules/esm/" in this
+         * set allows import "system" if
+         * "resource:///org/gnome/gjs/modules/esm/system.js" exists.
+         */
+        this.moduleURIs = new Set([
+            'resource:///org/gnome/gjs/modules/esm/',
+        ]);
+
+        /**
+         * @type {Map<string, import("./internalLoader.js").SchemeHandler>}
+         *
+         * A map of handlers for URI schemes (e.g. gi://)
+         */
+        this.schemeHandlers = new Map();
+    }
+
+    /**
+     * @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("./internalLoader.js").SchemeHandler} handler a handler
+     */
+    registerScheme(scheme, handler) {
+        this.schemeHandlers.set(scheme, handler);
+    }
+
+    /**
+     * Overrides InternalModuleLoader.loadURI
+     *
+     * @param {import("./internalLoader.js").Uri} uri a Uri object to load
+     */
+    loadURI(uri) {
+        if (uri.scheme) {
+            const loader = this.schemeHandlers.get(uri.scheme);
+
+            if (loader)
+                return loader.load(uri);
+        }
+
+        const result = super.loadURI(uri);
+
+        if (result)
+            return result;
+
+        throw new ImportError(`Invalid module URI: ${uri.uri}`);
+    }
+
+    /**
+     * Resolves a bare specifier like 'system' against internal resources,
+     * erroring if no resource is found.
+     *
+     * @param {string} specifier the module specifier to resolve for an import
+     * @returns {import("./internalLoader").Module}
+     */
+    resolveBareSpecifier(specifier) {
+        // 2) Resolve internal imports.
+
+        const uri = this.buildInternalURIs(specifier).find(uriExists);
+
+        if (!uri)
+            throw new ImportError(`Unknown 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 ModulePrivate(specifier, uri, true);
+        const compiled = this.compileModule(priv, text);
+
+        const registry = getRegistry(this.global);
+        if (!registry.has(specifier))
+            registry.set(specifier, compiled);
+
+        return compiled;
+    }
+
+    /**
+     * Resolves a module import with optional handling for relative imports.
+     * Overrides InternalModuleLoader.moduleResolveHook
+     *
+     * @param {import("./internalLoader.js").ModulePrivate} importingModulePriv
+     *   the private object of the module initiating the import
+     * @param {string} specifier the module specifier to resolve for an import
+     * @returns {import("./internalLoader").Module}
+     */
+    moduleResolveHook(importingModulePriv, specifier) {
+        const module = this.resolveModule(specifier, importingModulePriv.uri);
+        if (module)
+            return module;
+
+        return this.resolveBareSpecifier(specifier);
+    }
+}
+
+const moduleLoader = new ModuleLoader(moduleGlobalThis);
+setGlobalModuleLoader(moduleGlobalThis, moduleLoader);
+
+const giVersionMap = new Map([
+    ['GLib', '2.0'],
+    ['Gio', '2.0'],
+    ['GObject', '2.0'],
+]);
+
+/**
+ * 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
+ */
+function generateGIModule(namespace, version) {
+    return `
+    import $$gi from 'gi';
+    export default $$gi.require('${namespace}'${version !== undefined ? `, '${version}'` : ''});
+    `;
+}
+
+moduleLoader.registerScheme('gi', {
+    /**
+     * @param {import("./internalLoader.js").Uri} uri the URI to load
+     */
+    load(uri) {
+        const namespace = uri.host;
+        const alreadyLoadedVersion = giVersionMap.get(namespace);
+        const version = uri.query.version ?? alreadyLoadedVersion;
+
+        if (version) {
+            if (alreadyLoadedVersion !== undefined && version !== alreadyLoadedVersion) {
+                throw new ImportError(`Version ${alreadyLoadedVersion} of GI module ${
+                    namespace} already loaded, cannot load version ${version}`);
+            }
+            giVersionMap.set(namespace, version);
+        }
+
+        return [generateGIModule(namespace, version), true];
+    },
+});
diff --git a/test/gjs-tests.cpp b/test/gjs-tests.cpp
index c7555013..cd79d100 100644
--- a/test/gjs-tests.cpp
+++ b/test/gjs-tests.cpp
@@ -150,6 +150,155 @@ gjstest_test_func_gjs_context_exit(void)
     g_object_unref(context);
 }
 
+static void gjstest_test_func_gjs_context_eval_module_file() {
+    GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+    uint8_t exit_status;
+    GError* error = nullptr;
+
+    bool ok = gjs_context_eval_module_file(
+        gjs, "resource:///org/gnome/gjs/mock/test/modules/default.js",
+        &exit_status, &error);
+
+    g_assert_true(ok);
+    g_assert_no_error(error);
+    // for modules, last executed statement is _not_ the exit code
+    g_assert_cmpuint(exit_status, ==, 0);
+}
+
+static void gjstest_test_func_gjs_context_eval_module_file_throw() {
+    GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+    uint8_t exit_status;
+    GError* error = nullptr;
+
+    g_test_expect_message("Gjs", G_LOG_LEVEL_CRITICAL, "*bad module*");
+
+    bool ok = gjs_context_eval_module_file(
+        gjs, "resource:///org/gnome/gjs/mock/test/modules/throws.js",
+        &exit_status, &error);
+
+    g_assert_false(ok);
+    g_assert_error(error, GJS_ERROR, GJS_ERROR_FAILED);
+    g_assert_cmpuint(exit_status, ==, 1);
+
+    g_test_assert_expected_messages();
+
+    g_clear_error(&error);
+}
+
+static void gjstest_test_func_gjs_context_eval_module_file_exit() {
+    GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+    GError* error = nullptr;
+    uint8_t exit_status;
+
+    bool ok = gjs_context_eval_module_file(
+        gjs, "resource:///org/gnome/gjs/mock/test/modules/exit0.js",
+        &exit_status, &error);
+
+    g_assert_false(ok);
+    g_assert_error(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT);
+    g_assert_cmpuint(exit_status, ==, 0);
+
+    g_clear_error(&error);
+
+    ok = gjs_context_eval_module_file(
+        gjs, "resource:///org/gnome/gjs/mock/test/modules/exit.js",
+        &exit_status, &error);
+
+    g_assert_false(ok);
+    g_assert_error(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT);
+    g_assert_cmpuint(exit_status, ==, 42);
+
+    g_clear_error(&error);
+}
+
+static void gjstest_test_func_gjs_context_eval_module_file_fail_instantiate() {
+    GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+    GError* error = nullptr;
+    uint8_t exit_status;
+
+    g_test_expect_message("Gjs", G_LOG_LEVEL_WARNING, "*foo*");
+
+    // evaluating this module without registering 'foo' first should make it
+    // fail ModuleInstantiate
+    bool ok = gjs_context_eval_module_file(
+        gjs, "resource:///org/gnome/gjs/mock/test/modules/import.js",
+        &exit_status, &error);
+
+    g_assert_false(ok);
+    g_assert_error(error, GJS_ERROR, GJS_ERROR_FAILED);
+    g_assert_cmpuint(exit_status, ==, 1);
+
+    g_test_assert_expected_messages();
+
+    g_clear_error(&error);
+}
+
+static void gjstest_test_func_gjs_context_register_module_eval_module() {
+    GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+    GError* error = nullptr;
+
+    bool ok = gjs_context_register_module(
+        gjs, "foo", "resource:///org/gnome/gjs/mock/test/modules/default.js",
+        &error);
+
+    g_assert_true(ok);
+    g_assert_no_error(error);
+
+    uint8_t exit_status;
+    ok = gjs_context_eval_module(gjs, "foo", &exit_status, &error);
+
+    g_assert_true(ok);
+    g_assert_no_error(error);
+    g_assert_cmpuint(exit_status, ==, 0);
+}
+
+static void gjstest_test_func_gjs_context_register_module_eval_module_file() {
+    GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+    GError* error = nullptr;
+
+    bool ok = gjs_context_register_module(
+        gjs, "foo", "resource:///org/gnome/gjs/mock/test/modules/default.js",
+        &error);
+
+    g_assert_true(ok);
+    g_assert_no_error(error);
+
+    uint8_t exit_status;
+    ok = gjs_context_eval_module_file(
+        gjs, "resource:///org/gnome/gjs/mock/test/modules/import.js",
+        &exit_status, &error);
+
+    g_assert_true(ok);
+    g_assert_no_error(error);
+    g_assert_cmpuint(exit_status, ==, 0);
+}
+
+static void gjstest_test_func_gjs_context_register_module_non_existent() {
+    GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+    GError* error = nullptr;
+
+    bool ok = gjs_context_register_module(gjs, "foo", "nonexist.js", &error);
+
+    g_assert_false(ok);
+    g_assert_error(error, GJS_ERROR, GJS_ERROR_FAILED);
+
+    g_clear_error(&error);
+}
+
+static void gjstest_test_func_gjs_context_eval_module_unregistered() {
+    GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+    GError* error = nullptr;
+    uint8_t exit_status;
+
+    bool ok = gjs_context_eval_module(gjs, "foo", &exit_status, &error);
+
+    g_assert_false(ok);
+    g_assert_error(error, GJS_ERROR, GJS_ERROR_FAILED);
+    g_assert_cmpuint(exit_status, ==, 1);
+
+    g_clear_error(&error);
+}
+
 #define JS_CLASS "\
 const GObject = imports.gi.GObject; \
 const FooBar = GObject.registerClass(class FooBar extends GObject.Object {}); \
@@ -619,6 +768,24 @@ main(int    argc,
     g_test_add_func("/gjs/context/eval/non-zero-terminated",
                     gjstest_test_func_gjs_context_eval_non_zero_terminated);
     g_test_add_func("/gjs/context/exit", gjstest_test_func_gjs_context_exit);
+    g_test_add_func("/gjs/context/eval-module-file",
+                    gjstest_test_func_gjs_context_eval_module_file);
+    g_test_add_func("/gjs/context/eval-module-file/throw",
+                    gjstest_test_func_gjs_context_eval_module_file_throw);
+    g_test_add_func("/gjs/context/eval-module-file/exit",
+                    gjstest_test_func_gjs_context_eval_module_file_exit);
+    g_test_add_func(
+        "/gjs/context/eval-module-file/fail-instantiate",
+        gjstest_test_func_gjs_context_eval_module_file_fail_instantiate);
+    g_test_add_func("/gjs/context/register-module/eval-module",
+                    gjstest_test_func_gjs_context_register_module_eval_module);
+    g_test_add_func(
+        "/gjs/context/register-module/eval-module-file",
+        gjstest_test_func_gjs_context_register_module_eval_module_file);
+    g_test_add_func("/gjs/context/register-module/non-existent",
+                    gjstest_test_func_gjs_context_register_module_non_existent);
+    g_test_add_func("/gjs/context/eval-module/unregistered",
+                    gjstest_test_func_gjs_context_eval_module_unregistered);
     g_test_add_func("/gjs/gobject/js_defined_type", gjstest_test_func_gjs_gobject_js_defined_type);
     g_test_add_func("/gjs/gobject/without_introspection",
                     gjstest_test_func_gjs_gobject_without_introspection);
diff --git a/test/mock-js-resources.gresource.xml b/test/mock-js-resources.gresource.xml
index 23172772..e706563b 100644
--- a/test/mock-js-resources.gresource.xml
+++ b/test/mock-js-resources.gresource.xml
@@ -4,5 +4,10 @@
 <gresources>
   <gresource prefix="/org/gnome/gjs/mock">
     <file>test/gjs-test-coverage/loadedJSFromResource.js</file>
+    <file>test/modules/default.js</file>
+    <file>test/modules/exit.js</file>
+    <file>test/modules/exit0.js</file>
+    <file>test/modules/import.js</file>
+    <file>test/modules/throws.js</file>
   </gresource>
 </gresources>
diff --git a/test/modules/default.js b/test/modules/default.js
new file mode 100644
index 00000000..de0c1faf
--- /dev/null
+++ b/test/modules/default.js
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+
+export default 77;
+77;
diff --git a/test/modules/exit.js b/test/modules/exit.js
new file mode 100644
index 00000000..e02c3b34
--- /dev/null
+++ b/test/modules/exit.js
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+
+import System from 'system';
+System.exit(42);
diff --git a/test/modules/exit0.js b/test/modules/exit0.js
new file mode 100644
index 00000000..bf16b3d1
--- /dev/null
+++ b/test/modules/exit0.js
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+
+import System from 'system';
+System.exit(0);
diff --git a/test/modules/import.js b/test/modules/import.js
new file mode 100644
index 00000000..4b52f9d0
--- /dev/null
+++ b/test/modules/import.js
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+
+import num from 'foo';
+if (num !== 77)
+    throw new Error('wrong number');
diff --git a/test/modules/throws.js b/test/modules/throws.js
new file mode 100644
index 00000000..2dce689d
--- /dev/null
+++ b/test/modules/throws.js
@@ -0,0 +1,4 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
+
+throw new Error('bad module');


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