[gjs/ewlsh/top-level-await] modules: Enable top-level await for modules




commit c43f4c19131154b0c6ff600985e4e3ee8ed66a94
Author: Evan Welsh <contact evanwelsh com>
Date:   Sun Jan 30 10:06:15 2022 -0800

    modules: Enable top-level await for modules

 gjs/context-private.h                             |   3 +
 gjs/context.cpp                                   | 119 +++++++++--
 gjs/engine.cpp                                    |   2 +-
 gjs/internal.cpp                                  |  11 +-
 gjs/mainloop.cpp                                  |  23 ++-
 gjs/mainloop.h                                    |  28 ++-
 gjs/module.cpp                                    |  27 ++-
 installed-tests/scripts/testCommandLineModules.sh |  18 +-
 js.gresource.xml                                  |   1 -
 modules/internal/internalLoader.js                | 229 ---------------------
 modules/internal/loader.js                        | 230 +++++++++++++++++++++-
 11 files changed, 402 insertions(+), 289 deletions(-)
---
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 350f646a0..9ef774b21 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -122,6 +122,7 @@ class GjsContextPrivate : public JS::JobQueue {
     bool m_draining_job_queue : 1;
     bool m_should_profile : 1;
     bool m_exec_as_module : 1;
+    bool m_unhandled_exception : 1;
     bool m_should_listen_sigusr2 : 1;
 
     // If profiling is enabled, we record the durations and reason for GC mark
@@ -182,6 +183,7 @@ class GjsContextPrivate : public JS::JobQueue {
     }
     void main_loop_hold() { m_main_loop.hold(); }
     void main_loop_release() { m_main_loop.release(); }
+
     [[nodiscard]] GjsProfiler* profiler() const { return m_profiler; }
     [[nodiscard]] const GjsAtoms& atoms() const { return *m_atoms; }
     [[nodiscard]] bool destroying() const { return m_destroying.load(); }
@@ -231,6 +233,7 @@ class GjsContextPrivate : public JS::JobQueue {
     void schedule_gc(void) { schedule_gc_internal(true); }
     void schedule_gc_if_needed(void);
 
+    void report_unhandled_exception() { m_unhandled_exception = true; }
     void exit(uint8_t exit_code);
     [[nodiscard]] bool should_exit(uint8_t* exit_code_p) const;
 
diff --git a/gjs/context.cpp b/gjs/context.cpp
index 3adc4d76e..2e570d229 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -523,6 +523,63 @@ gjs_context_constructed(GObject *object)
     setup_dump_heap();
 }
 
+static bool on_context_module_rejected_fallible(JSContext* cx, unsigned argc,
+                                                JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    JS::HandleValue error = args.get(0);
+
+    GjsContextPrivate* gjs_cx = GjsContextPrivate::from_cx(cx);
+    gjs_cx->report_unhandled_exception();
+
+    gjs_log_exception_full(cx, error, nullptr, G_LOG_LEVEL_CRITICAL);
+
+    gjs_cx->main_loop_release();
+
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool on_context_module_rejected_infallible(JSContext* cx, unsigned,
+                                                  JS::Value*) {
+    GjsContextPrivate::from_cx(cx)->main_loop_release();
+
+    g_error("Failed to load context module.");
+    return false;
+}
+
+static bool on_context_module_resolved(JSContext* cx, unsigned, JS::Value*) {
+    GjsContextPrivate::from_cx(cx)->main_loop_release();
+
+    return true;
+}
+
+static bool add_promise_reactions(JSContext* cx, JS::HandleValue promise,
+                                  JSNative resolve, JSNative reject,
+                                  const std::string& debug_tag) {
+    JS::RootedObject promiseObject(cx, promise.toObjectOrNull());
+    if (!promiseObject)
+        return false;
+
+    std::string resolved_tag = debug_tag + " async resolved";
+    std::string rejected_tag = debug_tag + " async rejected";
+
+    JS::RootedFunction onRejected(
+        cx,
+        js::NewFunctionWithReserved(cx, reject, 1, 0, resolved_tag.c_str()));
+    if (!onRejected)
+        return false;
+    JS::RootedFunction onResolved(
+        cx,
+        js::NewFunctionWithReserved(cx, resolve, 1, 0, rejected_tag.c_str()));
+    if (!onResolved)
+        return false;
+
+    JS::RootedObject resolved(cx, JS_GetFunctionObject(onResolved));
+    JS::RootedObject rejected(cx, JS_GetFunctionObject(onRejected));
+
+    return JS::AddPromiseReactions(cx, promiseObject, resolved, rejected);
+}
+
 static void load_context_module(JSContext* cx, const char* uri,
                                 const char* debug_identifier) {
     JS::RootedObject loader(cx, gjs_module_load(cx, uri, uri));
@@ -537,11 +594,21 @@ static void load_context_module(JSContext* cx, const char* uri,
         g_error("Failed to instantiate %s module.", debug_identifier);
     }
 
-    JS::RootedValue ignore(cx);
-    if (!JS::ModuleEvaluate(cx, loader, &ignore)) {
+    JS::RootedValue evaluation_promise(cx);
+    if (!JS::ModuleEvaluate(cx, loader, &evaluation_promise)) {
         gjs_log_exception(cx);
         g_error("Failed to evaluate %s module.", debug_identifier);
     }
+
+    GjsContextPrivate::from_cx(cx)->main_loop_hold();
+    bool ok = add_promise_reactions(
+        cx, evaluation_promise, on_context_module_resolved,
+        on_context_module_rejected_infallible, "context");
+
+    if (!ok) {
+        gjs_log_exception(cx);
+        g_error("Failed to load %s module.", debug_identifier);
+    }
 }
 
 GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
@@ -655,22 +722,22 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
         g_error("Failed to define module global in internal global.");
     }
 
-    if (!gjs_load_internal_module(cx, "internalLoader")) {
+    if (!gjs_load_internal_module(cx, "loader")) {
         gjs_log_exception(cx);
         g_error("Failed to load internal module loaders.");
     }
 
-    load_context_module(cx,
-                        "resource:///org/gnome/gjs/modules/internal/loader.js",
-                        "module loader");
-
     {
         JSAutoRealm ar(cx, global);
+
         load_context_module(
             cx, "resource:///org/gnome/gjs/modules/esm/_bootstrap/default.js",
             "ESM bootstrap");
     }
 
+    // This should never happen unless bootstrap calls system.exit()
+    g_assert(m_main_loop.spin(this));
+
     start_draining_job_queue();
 }
 
@@ -1258,6 +1325,13 @@ bool GjsContextPrivate::handle_exit_code(bool no_sync_error_pending,
         return false;
     }
 
+    if (m_unhandled_exception) {
+        g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED,
+                    "%s %s threw an exception", source_type, identifier);
+        *exit_code = 1;
+        return false;
+    }
+
     // Assume success if no error was thrown and should exit isn't
     // set
     if (no_sync_error_pending) {
@@ -1289,13 +1363,15 @@ bool GjsContextPrivate::eval(const char* script, size_t script_len,
     JS::RootedValue retval(m_cx);
     bool ok = eval_with_scope(nullptr, script, script_len, filename, &retval);
 
-    if (ok)
-        m_main_loop.spin(this);
-
     /* 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. */
-    {
+     * uncaught exceptions have been reported since draining runs callbacks.
+     *
+     * If the main loop returns false we cannot guarantee the state
+     * of our promise queue (a module promise could be pending) so
+     * instead of draining the queue we instead just exit.
+     */
+    if (!ok || m_main_loop.spin(this)) {
         JS::AutoSaveExceptionState saved_exc(m_cx);
         ok = run_jobs_fallible() && ok;
     }
@@ -1349,17 +1425,26 @@ bool GjsContextPrivate::eval_module(const char* identifier,
         return false;
     }
 
-    JS::RootedValue ignore(m_cx);
-    bool ok = JS::ModuleEvaluate(m_cx, obj, &ignore);
+    JS::RootedValue evaluation_promise(m_cx);
+    bool ok = JS::ModuleEvaluate(m_cx, obj, &evaluation_promise);
+
+    if (ok) {
+        GjsContextPrivate::from_cx(m_cx)->main_loop_hold();
 
-    if (ok)
-        m_main_loop.spin(this);
+        ok = add_promise_reactions(
+            m_cx, evaluation_promise, on_context_module_resolved,
+            on_context_module_rejected_fallible, "context");
+    }
 
     /* 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.
+     *
+     * If the main loop returns false we cannot guarantee the state
+     * of our promise queue (a module promise could be pending) so
+     * instead of draining the queue we instead just exit.
      */
-    {
+    if (!ok || m_main_loop.spin(this)) {
         JS::AutoSaveExceptionState saved_exc(m_cx);
         ok = run_jobs_fallible() && ok;
     }
diff --git a/gjs/engine.cpp b/gjs/engine.cpp
index 1da606b8d..9468947c3 100644
--- a/gjs/engine.cpp
+++ b/gjs/engine.cpp
@@ -169,7 +169,7 @@ JSContext* gjs_create_js_context(GjsContextPrivate* uninitialized_gjs) {
     }
     JS::ContextOptionsRef(cx)
         .setAsmJS(enable_jit)
-        .setTopLevelAwait(false)
+        .setTopLevelAwait(true)
         .setPrivateClassFields(true)
         .setPrivateClassMethods(true);
 
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
index 57f3d97b6..68dd071d4 100644
--- a/gjs/internal.cpp
+++ b/gjs/internal.cpp
@@ -88,15 +88,8 @@ bool gjs_load_internal_module(JSContext* cx, const char* identifier) {
     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));
-
-    JS::RootedValue ignore(cx);
-    if (!gjs_global_registry_set(cx, registry, key, module) ||
-        !JS::ModuleInstantiate(cx, module) ||
-        !JS::ModuleEvaluate(cx, module, &ignore)) {
+    JS::RootedValue ignored(cx);
+    if (!JS::Evaluate(cx, options, buf, &ignored)) {
         return false;
     }
 
diff --git a/gjs/mainloop.cpp b/gjs/mainloop.cpp
index 56f3db291..91109d61a 100644
--- a/gjs/mainloop.cpp
+++ b/gjs/mainloop.cpp
@@ -12,10 +12,17 @@
 
 namespace Gjs {
 
-void MainLoop::spin(GjsContextPrivate* gjs) {
+bool MainLoop::spin(GjsContextPrivate* gjs) {
+    if (m_exiting)
+        return false;
+
     // Check if System.exit() has been called.
-    if (gjs->should_exit(nullptr))
-        return;
+    if (gjs->should_exit(nullptr)) {
+        // Return false to indicate the loop is exiting due to an exit call,
+        // the queue is likely not empty
+        exit();
+        return false;
+    }
 
     GjsAutoPointer<GMainContext, GMainContext, g_main_context_unref>
         main_context(g_main_context_ref_thread_default());
@@ -26,12 +33,18 @@ void MainLoop::spin(GjsContextPrivate* gjs) {
         // Only run the loop if there are pending jobs.
         if (g_main_context_pending(main_context))
             g_main_context_iteration(main_context, blocking);
-    } while (
+
         // If System.exit() has not been called
-        !gjs->should_exit(nullptr) &&
+        if (gjs->should_exit(nullptr)) {
+            exit();
+            return false;
+        }
+    } while (
         // and there are pending sources or the job queue is not empty
         // continue spinning the event loop.
         (can_block() || !gjs->empty()));
+
+    return true;
 }
 
 };  // namespace Gjs
diff --git a/gjs/mainloop.h b/gjs/mainloop.h
index 748dc7e49..f374060c8 100644
--- a/gjs/mainloop.h
+++ b/gjs/mainloop.h
@@ -18,8 +18,13 @@ class MainLoop {
     // We nonetheless use grefcount here because it takes care of dealing with
     // integer overflow for us.
     grefcount m_hold_count;
+    bool m_exiting;
 
     [[nodiscard]] bool can_block() {
+        // Don't block if exiting
+        if (m_exiting)
+            return false;
+
         g_assert(!g_ref_count_compare(&m_hold_count, 0) &&
                  "main loop released too many times");
 
@@ -27,21 +32,38 @@ class MainLoop {
         return !g_ref_count_compare(&m_hold_count, 1);
     }
 
+    void exit() {
+        m_exiting = true;
+
+        // Reset the reference count to 1 to exit
+        g_ref_count_init(&m_hold_count);
+    }
+
  public:
-    MainLoop() { g_ref_count_init(&m_hold_count); }
+    MainLoop() : m_exiting(false) { g_ref_count_init(&m_hold_count); }
     ~MainLoop() {
         g_assert(g_ref_count_compare(&m_hold_count, 1) &&
                  "mismatched hold/release on main loop");
     }
 
-    void hold() { g_ref_count_inc(&m_hold_count); }
+    void hold() {
+        // Don't allow new holds after exit() is called
+        if (m_exiting)
+            return;
+
+        g_ref_count_inc(&m_hold_count);
+    }
 
     void release() {
+        // Ignore releases after exit(), exit() resets the refcount
+        if (m_exiting)
+            return;
+
         bool zero [[maybe_unused]] = g_ref_count_dec(&m_hold_count);
         g_assert(!zero && "main loop released too many times");
     }
 
-    void spin(GjsContextPrivate*);
+    [[nodiscard]] bool spin(GjsContextPrivate*);
 };
 
 };  // namespace Gjs
diff --git a/gjs/module.cpp b/gjs/module.cpp
index d1aee4941..95b0e1c34 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -540,7 +540,7 @@ JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importingModulePriv,
 // Can fail in JS::FinishDynamicModuleImport(), but will assert if anything
 // fails in fetching the stashed values, since that would be a serious GJS bug.
 GJS_JSAPI_RETURN_CONVENTION
-static bool finish_import(JSContext* cx, JS::DynamicImportStatus status,
+static bool finish_import(JSContext* cx, JS::HandleObject evaluation_promise,
                           const JS::CallArgs& args) {
     GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
     priv->main_loop_release();
@@ -566,20 +566,15 @@ static bool finish_import(JSContext* cx, JS::DynamicImportStatus status,
 
     args.rval().setUndefined();
 
-    return JS::FinishDynamicModuleImport_NoTLA(
-        cx, status, importing_module_priv, module_request, internal_promise);
+    return JS::FinishDynamicModuleImport(cx, evaluation_promise,
+                                         importing_module_priv, module_request,
+                                         internal_promise);
 }
 
 // Failing a JSAPI function may result either in an exception pending on the
 // context, in which case we must call JS::FinishDynamicModuleImport() to reject
 // the internal promise; or in an uncatchable exception such as OOM, in which
 // case we must not call JS::FinishDynamicModuleImport().
-GJS_JSAPI_RETURN_CONVENTION
-static bool fail_import(JSContext* cx, const JS::CallArgs& args) {
-    if (JS_IsExceptionPending(cx))
-        return finish_import(cx, JS::DynamicImportStatus::Failed, args);
-    return false;
-}
 
 GJS_JSAPI_RETURN_CONVENTION
 static bool import_rejected(JSContext* cx, unsigned argc, JS::Value* vp) {
@@ -591,14 +586,12 @@ static bool import_rejected(JSContext* cx, unsigned argc, JS::Value* vp) {
     // FinishDynamicModuleImport will reject the internal_promise with it.
     JS_SetPendingException(cx, args.get(0),
                            JS::ExceptionStackBehavior::DoNotCapture);
-
-    return finish_import(cx, JS::DynamicImportStatus::Failed, args);
+    return finish_import(cx, nullptr, args);
 }
 
 GJS_JSAPI_RETURN_CONVENTION
 static bool import_resolved(JSContext* cx, unsigned argc, JS::Value* vp) {
     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
-
     gjs_debug(GJS_DEBUG_IMPORTER, "Async import promise resolved");
 
     JS::RootedObject global(cx, gjs_get_import_global(cx));
@@ -607,12 +600,14 @@ static bool import_resolved(JSContext* cx, unsigned argc, JS::Value* vp) {
     g_assert(args[0].isObject());
     JS::RootedObject module(cx, &args[0].toObject());
 
-    JS::RootedValue ignore(cx);
+    JS::RootedValue evaluation_promise(cx);
     if (!JS::ModuleInstantiate(cx, module) ||
-        !JS::ModuleEvaluate(cx, module, &ignore))
-        return fail_import(cx, args);
+        !JS::ModuleEvaluate(cx, module, &evaluation_promise))
+        return finish_import(cx, nullptr, args);
 
-    return finish_import(cx, JS::DynamicImportStatus::Ok, args);
+    JS::RootedObject evaluation_promiseObject(
+        cx, evaluation_promise.toObjectOrNull());
+    return finish_import(cx, evaluation_promiseObject, args);
 }
 
 bool gjs_dynamic_module_resolve(JSContext* cx,
diff --git a/installed-tests/scripts/testCommandLineModules.sh 
b/installed-tests/scripts/testCommandLineModules.sh
index 6566dcb75..1965dcdc0 100755
--- a/installed-tests/scripts/testCommandLineModules.sh
+++ b/installed-tests/scripts/testCommandLineModules.sh
@@ -76,8 +76,24 @@ $gjs dynamicImplicitMainloop.js
 test $? -eq 21
 report "ensure dynamic imports resolve without an explicit mainloop"
 
+cat <<EOF >dynamicTopLevelAwaitImportee.js
+export const EXIT_CODE = 32;
+EOF
+
+cat <<EOF >dynamicTopLevelAwait.js
+const {EXIT_CODE} = await import("./dynamicTopLevelAwaitImportee.js")
+const system = await import('system');
+
+system.exit(EXIT_CODE);
+EOF
+
+$gjs -m dynamicTopLevelAwait.js
+test $? -eq 32
+report "ensure top level await can import modules"
+
 
 rm -f doubledynamic.js doubledynamicImportee.js \
-      dynamicImplicitMainloop.js dynamicImplicitMainloopImportee.js
+      dynamicImplicitMainloop.js dynamicImplicitMainloopImportee.js \
+      dynamicTopLevelAwait.js dynamicTopLevelAwaitImportee.js
 
 echo "1..$total"
diff --git a/js.gresource.xml b/js.gresource.xml
index 4d3fde355..e12dea125 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -4,7 +4,6 @@
 <gresources>
   <gresource prefix="/org/gnome/gjs">
     <!-- Internal modules -->
-    <file>modules/internal/internalLoader.js</file>
     <file>modules/internal/loader.js</file>
 
     <!-- ESM-based modules -->
diff --git a/modules/internal/loader.js b/modules/internal/loader.js
index 3e661496a..2f3f71d8e 100644
--- a/modules/internal/loader.js
+++ b/modules/internal/loader.js
@@ -1,7 +1,223 @@
 // 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';
+/** @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.
+ */
+class ImportError extends moduleGlobalThis.Error {
+    /**
+     * @param {string | undefined} message the import error message
+     */
+    constructor(message) {
+        super(message);
+
+        this.name = 'ImportError';
+    }
+}
+
+/**
+ * ModulePrivate is the "private" object of every module.
+ */
+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
+ */
+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;
+    }
+}
 
 class ModuleLoader extends InternalModuleLoader {
     /**
@@ -27,7 +243,7 @@ class ModuleLoader extends InternalModuleLoader {
         ]);
 
         /**
-         * @type {Map<string, import("./internalLoader.js").SchemeHandler>}
+         * @type {Map<string, SchemeHandler>}
          *
          * A map of handlers for URI schemes (e.g. gi://)
          */
@@ -52,7 +268,7 @@ class ModuleLoader extends InternalModuleLoader {
 
     /**
      * @param {string} scheme the URI scheme to register
-     * @param {import("./internalLoader.js").SchemeHandler} handler a handler
+     * @param {SchemeHandler} handler a handler
      */
     registerScheme(scheme, handler) {
         this.schemeHandlers.set(scheme, handler);
@@ -61,7 +277,7 @@ class ModuleLoader extends InternalModuleLoader {
     /**
      * Overrides InternalModuleLoader.loadURI
      *
-     * @param {import("./internalLoader.js").Uri} uri a Uri object to load
+     * @param {Uri} uri a Uri object to load
      */
     loadURI(uri) {
         if (uri.scheme) {
@@ -113,7 +329,7 @@ class ModuleLoader extends InternalModuleLoader {
      * Resolves a module import with optional handling for relative imports.
      * Overrides InternalModuleLoader.moduleResolveHook
      *
-     * @param {import("./internalLoader.js").ModulePrivate} importingModulePriv
+     * @param {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}
@@ -226,7 +442,7 @@ function generateGIModule(namespace, version) {
 
 moduleLoader.registerScheme('gi', {
     /**
-     * @param {import("./internalLoader.js").Uri} uri the URI to load
+     * @param {Uri} uri the URI to load
      */
     load(uri) {
         const namespace = uri.host;
@@ -235,7 +451,7 @@ moduleLoader.registerScheme('gi', {
         return [generateGIModule(namespace, version), true];
     },
     /**
-     * @param {import("./internalLoader.js").Uri} uri the URI to load asynchronously
+     * @param {Uri} uri the URI to load asynchronously
      */
     loadAsync(uri) {
         // gi: only does string manipulation, so it is safe to use the same code for sync and async.


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