[gjs/esm/dynamic-imports: 187/192] Implement dynamic imports




commit 653ec55707fe709e642b455fa4af347c55172f64
Author: Evan Welsh <noreply evanwelsh com>
Date:   Sat Nov 14 14:36:27 2020 -0600

    Implement dynamic imports

 gjs/context.cpp                    |  1 +
 gjs/global.cpp                     |  3 ++
 gjs/internal.cpp                   | 77 ++++++++++++++++++++++++++++++++++++++
 gjs/internal.h                     |  5 +++
 gjs/module.cpp                     | 27 +++++++++++++
 gjs/module.h                       |  5 +++
 modules/internal/internalLoader.js | 69 ++++++++++++++++++++++++++++++++++
 modules/internal/loader.js         | 59 +++++++++++++++++++++++++++++
 8 files changed, 246 insertions(+)
---
diff --git a/gjs/context.cpp b/gjs/context.cpp
index ab3da86e..79a86ce2 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -549,6 +549,7 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
     }
 
     JS::SetModuleResolveHook(rt, gjs_module_resolve);
+    JS::SetModuleDynamicImportHook(rt, gjs_dynamic_module_resolve);
     JS::SetModuleMetadataHook(rt, gjs_populate_module_meta);
 
     if (!JS_DefineProperty(m_cx, internal_global, "moduleGlobalThis", global,
diff --git a/gjs/global.cpp b/gjs/global.cpp
index 6e0dac3b..10151e42 100644
--- a/gjs/global.cpp
+++ b/gjs/global.cpp
@@ -268,13 +268,16 @@ class GjsInternalGlobal : GjsBaseGlobal {
         JS_FN("compileModule", gjs_internal_compile_module, 2, 0),
         JS_FN("compileInternalModule", gjs_internal_compile_internal_module, 2,
               0),
+        JS_FN("finishDynamicModuleImport", FinishDynamicModuleImport, 3, 0),
         JS_FN("getRegistry", gjs_internal_get_registry, 1, 0),
+        JS_FN("initAndEval", InitAndEval, 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("setModuleDynamicImportHook", SetModuleDynamicImportHook, 2, 0),
         JS_FN("setModulePrivate", gjs_internal_set_module_private, 2, 0),
         JS_FN("uriExists", gjs_internal_uri_exists, 1, 0),
         JS_FS_END};
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
index 779e0864..e13c59f9 100644
--- a/gjs/internal.cpp
+++ b/gjs/internal.cpp
@@ -105,6 +105,59 @@ bool gjs_load_internal_module(JSContext* cx, const char* identifier) {
     return true;
 }
 
+bool SetModuleDynamicImportHook(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args.requireAtLeast(cx, "setModuleDynamicImportHook", 2)) {
+        return false;
+    }
+
+    JS::RootedValue gv(cx, args[0]);
+    JS::RootedValue mv(cx, args[1]);
+
+    g_assert(gv.isObject());
+
+    // The hook is stored in the internal global.
+    JS::RootedObject global(cx, &gv.toObject());
+
+    // The dynamic hook is stored in the internal global.
+
+    gjs_set_global_slot(global, GjsGlobalSlot::DYNAMIC_IMPORT_HOOK, mv);
+
+    args.rval().setUndefined();
+    return true;
+}
+
+bool FinishDynamicModuleImport(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!args.requireAtLeast(cx, "finishDynamicModuleImport", 3)) {
+        return false;
+    }
+
+    if (JS_IsExceptionPending(cx)) {
+        gjs_log_exception_uncaught(cx);
+        gjs_throw(cx, "Uncaught exception in module!");
+        return false;
+    }
+
+    JS::RootedString specifier(cx, args[1].toString());
+    JS::RootedObject promise(cx, &args[2].toObject());
+
+    auto priv = GjsContextPrivate::from_cx(cx);
+    // gjs_module_resolve is called within whatever realm the dynamic import is
+    // finished in.
+    {
+        JSAutoRealm ar(cx, priv->global());
+
+        if (!JS_WrapObject(cx, &promise)) {
+            gjs_throw(cx, "Failed to wrap dynamic imports' promise.");
+            return false;
+        }
+
+        return JS::FinishDynamicModuleImport(cx, args[0], specifier, promise);
+    }
+}
+
 /**
  * gjs_internal_set_global_module_loader:
  *
@@ -248,6 +301,30 @@ bool gjs_internal_set_module_private(JSContext* cx, unsigned argc,
     return true;
 }
 
+bool InitAndEval(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!args.requireAtLeast(cx, "initAndEval", 1)) {
+        return false;
+    }
+    JS::RootedObject global(cx, gjs_get_import_global(cx));
+    JSAutoRealm ar(cx, global);
+
+    JS::RootedObject new_module(cx, &args[0].toObject());
+
+    if (!JS::ModuleInstantiate(cx, new_module)) {
+        gjs_log_exception(cx);
+        return false;
+    }
+    if (!JS::ModuleEvaluate(cx, new_module)) {
+        gjs_log_exception(cx);
+        return false;
+    }
+
+    args.rval().setUndefined();
+    return true;
+}
+
 /**
  * gjs_internal_get_registry:
  *
diff --git a/gjs/internal.h b/gjs/internal.h
index 212e5458..bf742652 100644
--- a/gjs/internal.h
+++ b/gjs/internal.h
@@ -40,6 +40,11 @@ bool gjs_internal_resolve_relative_resource_or_file(JSContext* cx,
                                                     unsigned argc,
                                                     JS::Value* vp);
 
+bool InitAndEval(JSContext* cx, unsigned argc, JS::Value* vp);
+bool SetModuleDynamicImportHook(JSContext* cx, unsigned argc, JS::Value* vp);
+
+bool FinishDynamicModuleImport(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);
diff --git a/gjs/module.cpp b/gjs/module.cpp
index 0a9effab..bf5a41c6 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -474,3 +474,30 @@ JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importingModulePriv,
     g_assert(result.isObject() && "resolve hook failed to return an object!");
     return &result.toObject();
 }
+
+bool gjs_dynamic_module_resolve(JSContext* cx,
+                                JS::Handle<JS::Value> aReferencingPrivate,
+                                JS::Handle<JSString*> aSpecifier,
+                                JS::Handle<JSObject*> aPromise) {
+    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.");
+
+    GjsContextPrivate* gjs_cx = GjsContextPrivate::from_cx(cx);
+
+    JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
+
+    JSAutoRealm ar(cx, global);
+
+    JS::RootedValue hookValue(
+        cx, gjs_get_global_slot(global, GjsGlobalSlot::DYNAMIC_IMPORT_HOOK));
+
+    JS::RootedValueArray<3> args(cx);
+    args[0].set(aReferencingPrivate);
+    args[1].setString(aSpecifier);
+    args[2].setObject(*aPromise);
+
+    JS::RootedValue result(cx);
+    return JS_CallFunctionValue(cx, nullptr, hookValue, args, &result);
+}
diff --git a/gjs/module.h b/gjs/module.h
index c754b114..d303530f 100644
--- a/gjs/module.h
+++ b/gjs/module.h
@@ -39,4 +39,9 @@ GJS_JSAPI_RETURN_CONVENTION
 bool gjs_populate_module_meta(JSContext* cx, JS::HandleValue private_ref,
                               JS::HandleObject meta_object);
 
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_dynamic_module_resolve(JSContext* cx,
+                                JS::Handle<JS::Value> aReferencingPrivate,
+                                JS::Handle<JSString*> aSpecifier,
+                                JS::Handle<JSObject*> aPromise);
 #endif  // GJS_MODULE_H_
diff --git a/modules/internal/internalLoader.js b/modules/internal/internalLoader.js
index 09acc16c..bd9933e6 100644
--- a/modules/internal/internalLoader.js
+++ b/modules/internal/internalLoader.js
@@ -84,6 +84,21 @@ export class InternalModuleLoader {
 
         return null;
     }
+    
+    /**
+     * Loads a file or resource URI synchronously
+     *
+     * @param {Uri} uri the file or resource URI to load
+     * @returns {Promise<[string] | [string, boolean] | null>}
+     */
+    async loadURIAsync(uri) {
+        if (uri.scheme === 'file' || uri.scheme === 'resource') {
+            const result = await loadResourceOrFileAsync(uri.uri);
+            return [result];
+        }
+
+        return null;
+    }
 
     /**
      * Resolves an import specifier given an optional parent importer.
@@ -191,6 +206,60 @@ export class InternalModuleLoader {
         return null;
     }
 
+    /**
+     * @param {string} specifier the specifier (e.g. relative path, root package) to resolve
+     * @param {string | null} parentURI the URI of the module triggering this resolve
+     *
+     * @returns {Promise<import("../types").Module | null>}
+     */
+    async resolveModuleAsync(specifier, parentURI) {
+        const registry = getRegistry(this.global);
+
+
+        // Check if the module has already been loaded
+
+        let module = registry.get(specifier);
+
+        if (module)
+            return module;
+
+
+        // 1) Resolve path and URI-based imports.
+
+        const uri = this.resolveSpecifier(specifier, parentURI);
+
+        if (uri) {
+            module = registry.get(uri.uri);
+            //
+            // Check if module is already loaded (relative handling)
+            if (module)
+                return module;
+
+
+            const result = await this.loadURIAsync(uri);
+
+            if (!result)
+                return null;
+
+
+            const [text, internal = false] = result;
+
+            const esmodule = new ESModule(uri.uri, uri.uri, internal);
+
+            const compiled = this.compileModule(esmodule, text);
+
+            if (!compiled)
+                throw new ImportError(`Failed to register module: ${uri}`);
+
+
+            registry.set(uri.uri, compiled);
+            return compiled;
+        }
+
+        return null;
+    }
+
+
     moduleResolveHook(importingModulePriv, specifier) {
         const resolved = this.resolveModule(specifier, importingModulePriv.uri ?? null);
         if (!resolved)
diff --git a/modules/internal/loader.js b/modules/internal/loader.js
index a44e8127..dad19b58 100644
--- a/modules/internal/loader.js
+++ b/modules/internal/loader.js
@@ -117,6 +117,53 @@ class ModuleLoader extends InternalModuleLoader {
 
         return compiled;
     }
+
+      /**
+     * Resolves a module import with optional handling for relative imports asynchronously.
+     *
+     * @param {string} specifier the module specifier to resolve for an import
+     * @param {string | null} moduleURI the importing module's URI or null if importing from the entry point
+     * @returns {Promise<import("../types").Module>}
+     */
+    async resolveModuleAsync(specifier, moduleURI) {
+        const module = await super.resolveModuleAsync(specifier, moduleURI);
+
+        if (module)
+            return module;
+
+
+        // 2) Resolve internal imports.
+
+        const uri = this.buildInternalURIs(specifier).find(u => {
+            let file = Gio.File.new_for_uri(u);
+
+            return file && file.query_exists(null);
+        });
+
+        if (!uri)
+            throw new ImportError(`Attempted to load unregistered global module: ${specifier}`);
+
+        const parsed = parseURI(uri);
+
+        if (parsed.scheme !== 'file' && parsed.scheme !== 'resource')
+            throw new ImportError('Only file:// and resource:// URIs are currently supported.');
+
+        const text = loadResourceOrFile(parsed.uri);
+
+        const priv = new ESModule(specifier, uri, true);
+
+        const compiled = this.compileModule(priv, text);
+
+        if (!compiled)
+            throw new ImportError(`Failed to register module: ${uri}`);
+
+        const registry = getRegistry(this.global);
+
+        if (!registry.has(specifier))
+            registry.set(specifier, compiled);
+
+        return compiled;
+    }
 }
 
 const moduleLoader = new ModuleLoader(moduleGlobalThis);
@@ -163,3 +210,15 @@ moduleLoader.registerScheme('gi', {
         return [generateGIModule(namespace, version), true];
     },
 });
+
+setModuleDynamicImportHook(moduleGlobalThis, (module, specifier, promise) => {
+    moduleLoader.resolveModuleAsync(specifier, module.uri).then((m) => {
+        initAndEval(m);
+
+        finishDynamicModuleImport(module, specifier, promise);
+    }).catch(err => {
+        debug(err);
+        debug(err.stack);
+        throw new Error(`Dynamic module import failed: ${err}`);
+    });
+});


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