[gjs/esm/dynamic-imports: 9/13] Implement dynamic imports




commit eeaf3790de37d3a1bce163051799a86d270a24d1
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             |  2 ++
 gjs/internal.cpp           | 55 +++++++++++++++++++++++++++++++
 gjs/internal.h             |  3 ++
 gjs/module.cpp             | 25 ++++++++++++++
 gjs/module.h               |  5 +++
 modules/internal/loader.js | 82 ++++++++++++++++++++++++++++++++++++++++++++++
 7 files changed, 173 insertions(+)
---
diff --git a/gjs/context.cpp b/gjs/context.cpp
index c3810d97..1a85f6c6 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..1dcb613a 100644
--- a/gjs/global.cpp
+++ b/gjs/global.cpp
@@ -268,7 +268,9 @@ 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",
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
index 184f2457..ef620d33 100644
--- a/gjs/internal.cpp
+++ b/gjs/internal.cpp
@@ -108,6 +108,37 @@ bool gjs_load_internal_module(JSContext* cx, const char* identifier) {
     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:
  *
@@ -251,6 +282,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..6b9d525a 100644
--- a/gjs/internal.h
+++ b/gjs/internal.h
@@ -40,6 +40,9 @@ 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 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 c810f7c9..2d4a7e06 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -484,3 +484,28 @@ 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_dynamic_module_resolve can only be called from the default "
+             "global.");
+
+    JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
+    JSAutoRealm ar(cx, global);
+
+    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<3> args(cx);
+    args[0].set(aReferencingPrivate);
+    args[1].setString(aSpecifier);
+    args[2].setObject(*aPromise);
+
+    JS::RootedValue result(cx);
+    return JS::Call(cx, loader, "moduleDynamicImportHook", 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/loader.js b/modules/internal/loader.js
index fde96eb0..464a1034 100644
--- a/modules/internal/loader.js
+++ b/modules/internal/loader.js
@@ -76,6 +76,16 @@ class ModuleLoader extends InternalModuleLoader {
         throw new ImportError(`Invalid module URI: ${uri.uri}`);
     }
 
+    moduleDynamicImportHook(module, specifier, promise) {
+        this.resolveModuleAsync(specifier, module.uri).then((m) => {
+            initAndEval(m);
+    
+            finishDynamicModuleImport(module, specifier, promise);
+        }).catch(err => {
+            throw new ImportError(err.message);
+        });
+    }
+
     /**
      * Resolves a bare specifier like 'system' against internal resources,
      * erroring if no resource is found.
@@ -122,6 +132,71 @@ class ModuleLoader extends InternalModuleLoader {
 
         return this.resolveBareSpecifier(specifier);
     }
+
+    /**
+     * Resolves a module import with optional handling for relative imports asynchronously.
+     *
+     * @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("../types").Module}
+     */
+    async resolveModuleAsync(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 = await this.loadURIAsync(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;
+        }
+
+        // 2) Resolve internal imports.
+
+        return this.resolveBareSpecifier(specifier);
+    }
+
+    /**
+     * Loads a file or resource URI asynchronously
+     *
+     * @param {Uri} uri the file or resource URI to load
+     * @returns {Promise<[string] | [string, boolean] | null>}
+     */
+    async loadURIAsync(uri) {
+        if (uri.scheme) {
+            const loader = this.schemeHandlers.get(uri.scheme);
+
+            if (loader)
+                return loader.loadAsync(uri);
+        }
+
+        if (uri.scheme === 'file' || uri.scheme === 'resource') {
+            const result = await loadResourceOrFileAsync(uri.uri);
+            return [result];
+        }
+
+        return null;
+    }
 }
 
 const moduleLoader = new ModuleLoader(moduleGlobalThis);
@@ -167,4 +242,11 @@ moduleLoader.registerScheme('gi', {
 
         return [generateGIModule(namespace, version), true];
     },
+    /**
+     * @param {import("./internalLoader.js").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.
+        return this.load(uri);
+    }
 });


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