[gjs/esm/dynamic-imports: 1/6] Implement dynamic imports




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

    Implement dynamic imports

 gjs/context.cpp            |   1 +
 gjs/module.cpp             | 123 +++++++++++++++++++++++++++++++++++++++++++++
 gjs/module.h               |   5 ++
 modules/internal/loader.js |  79 +++++++++++++++++++++++++++++
 test/gjs-tests.cpp         |  51 +++++++++++++++++++
 5 files changed, 259 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/module.cpp b/gjs/module.cpp
index c810f7c9..9645e134 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -21,6 +21,7 @@
 #include <js/Conversions.h>
 #include <js/GCVector.h>  // for RootedVector
 #include <js/Id.h>
+#include <js/Modules.h>
 #include <js/PropertyDescriptor.h>
 #include <js/RootingAPI.h>
 #include <js/SourceText.h>
@@ -484,3 +485,125 @@ JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importingModulePriv,
     g_assert(result.isObject() && "resolve hook failed to return an object!");
     return &result.toObject();
 }
+
+inline bool gjs_dynamic_module_get_meta(
+    JSContext* cx, JS::HandleObject meta,
+    JS::MutableHandleValue importingModulePriv,
+    JS::MutableHandleString specifier,
+    JS::MutableHandleObject internalPromise) {
+    JS::RootedValue v_specifier(cx);
+    JS::RootedValue v_internalPromise(cx);
+    if (!JS_GetProperty(cx, meta, "priv", importingModulePriv) ||
+        !JS_GetProperty(cx, meta, "promise", &v_internalPromise) ||
+        !JS_GetProperty(cx, meta, "specifier", &v_specifier))
+        return false;
+
+    g_assert(v_specifier.isString());
+    g_assert(v_internalPromise.isObject());
+
+    specifier.set(v_specifier.toString());
+    internalPromise.set(&v_internalPromise.toObject());
+
+    return true;
+}
+
+bool gjs_dynamic_module_rejected(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    JS::Value priv_value = js::GetFunctionNativeReserved(&args.callee(), 0);
+    g_assert(!priv_value.isNull() && "Dynamic module rejection called twice");
+    JS::RootedObject meta(cx, &priv_value.toObject());
+    g_assert(meta && "Dynamic module rejection called twice");
+    js::SetFunctionNativeReserved(&args.callee(), 0, JS::NullValue());
+
+    JS::RootedValue importingModulePriv(cx);
+    JS::RootedString specifier(cx);
+    JS::RootedObject internalPromise(cx);
+    if (!gjs_dynamic_module_get_meta(cx, meta, &importingModulePriv, &specifier,
+                                     &internalPromise))
+        return false;
+
+    return JS::FinishDynamicModuleImport(cx, importingModulePriv, specifier,
+                                         internalPromise);
+}
+
+bool gjs_dynamic_module_resolved(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    JS::Value priv_value = js::GetFunctionNativeReserved(&args.callee(), 0);
+    g_assert(!priv_value.isNull() && "Dynamic module resolution called twice");
+    JS::RootedObject meta(cx, &priv_value.toObject());
+    g_assert(meta && "Dynamic module resolution called twice");
+    js::SetFunctionNativeReserved(&args.callee(), 0, JS::NullValue());
+
+    JS::RootedValue importingModulePriv(cx);
+    JS::RootedString specifier(cx);
+    JS::RootedObject internalPromise(cx);
+    if (!gjs_dynamic_module_get_meta(cx, meta, &importingModulePriv, &specifier,
+                                     &internalPromise))
+        return false;
+
+    JS::RootedObject global(cx, gjs_get_import_global(cx));
+    JSAutoRealm ar(cx, global);
+
+    g_assert(args[0].isObject());
+    JS::RootedObject module(cx, &args[0].toObject());
+
+    if (!JS::ModuleInstantiate(cx, module) || !JS::ModuleEvaluate(cx, module))
+        return JS::FinishDynamicModuleImport(cx, importingModulePriv, specifier,
+                                             internalPromise);
+
+    return JS::FinishDynamicModuleImport(cx, importingModulePriv, specifier,
+                                         internalPromise);
+}
+
+bool gjs_dynamic_module_resolve(JSContext* cx,
+                                JS::Handle<JS::Value> importingModulePriv,
+                                JS::Handle<JSString*> specifier,
+                                JS::Handle<JSObject*> internalPromise) {
+    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::RootedObject meta(cx, JS_NewPlainObject(cx));
+    if (!JS_DefineProperty(cx, meta, "specifier", specifier,
+                           JSPROP_PERMANENT) ||
+        !JS_DefineProperty(cx, meta, "promise", internalPromise,
+                           JSPROP_PERMANENT) ||
+        !JS_DefineProperty(cx, meta, "priv", importingModulePriv,
+                           JSPROP_PERMANENT))
+        return false;
+
+    JS::RootedValueArray<2> args(cx);
+    args[0].set(importingModulePriv);
+    args[1].setString(specifier);
+
+    JS::RootedValue result(cx);
+    if (!JS::Call(cx, loader, "moduleResolveAsyncHook", args, &result))
+        return JS::FinishDynamicModuleImport(cx, importingModulePriv, specifier,
+                                             internalPromise);
+
+    JS::RootedObject resolved(
+        cx, JS_GetFunctionObject(js::NewFunctionWithReserved(
+                cx, gjs_dynamic_module_resolved, 2, 0, "import resolved")));
+    JS::RootedObject rejected(
+        cx, JS_GetFunctionObject(js::NewFunctionWithReserved(
+                cx, gjs_dynamic_module_rejected, 2, 0, "import rejected")));
+    js::SetFunctionNativeReserved(resolved, 0, JS::ObjectValue(*meta));
+    js::SetFunctionNativeReserved(rejected, 0, JS::ObjectValue(*meta));
+
+    JS::RootedObject promise(cx, &result.toObject());
+
+    if (!JS::AddPromiseReactions(cx, promise, resolved, rejected))
+        return false;
+
+    return true;
+}
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..bf2b8b33 100644
--- a/modules/internal/loader.js
+++ b/modules/internal/loader.js
@@ -122,6 +122,78 @@ class ModuleLoader extends InternalModuleLoader {
 
         return this.resolveBareSpecifier(specifier);
     }
+
+    moduleResolveAsyncHook(importingModulePriv, specifier) {
+        if (!importingModulePriv || !importingModulePriv.uri)
+            throw new ImportError('Cannot resolve relative imports from an unknown file.');
+
+        return this.resolveModuleAsync(specifier, importingModulePriv.uri);
+    }
+
+    /**
+     * 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 +239,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);
+    }
 });
diff --git a/test/gjs-tests.cpp b/test/gjs-tests.cpp
index cd79d100..5d44e583 100644
--- a/test/gjs-tests.cpp
+++ b/test/gjs-tests.cpp
@@ -112,6 +112,53 @@ gjstest_test_func_gjs_context_construct_eval(void)
     g_object_unref (context);
 }
 
+static void gjstest_test_func_gjs_context_eval_dynamic_import() {
+    GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+    GError* error = NULL;
+    int status;
+
+    bool ok = gjs_context_eval(gjs, R"(
+        import('system')
+            .catch(err => logError(err))
+            .finally(() => imports.mainloop.quit());
+        imports.mainloop.run();
+    )",
+                               -1, "<main>", &status, &error);
+
+    g_assert_true(ok);
+    g_assert_no_error(error);
+}
+
+static void gjstest_test_func_gjs_context_eval_dynamic_import_bad() {
+    GjsAutoUnref<GjsContext> gjs = gjs_context_new();
+    GError* error = NULL;
+    int status;
+
+    g_test_expect_message("Gjs", G_LOG_LEVEL_WARNING,
+                          "*Unknown module: 'badmodule'*");
+
+    bool ok = gjs_context_eval(gjs, R"(
+        let isBad = false;
+        import('badmodule')
+            .catch(err => {
+                logError(err);
+                isBad = true;
+            })
+            .finally(() => imports.mainloop.quit());
+        imports.mainloop.run();
+
+        if (isBad) imports.system.exit(10);
+    )",
+                               -1, "<main>", &status, &error);
+
+    g_assert_false(ok);
+    g_assert_cmpuint(status, ==, 10);
+
+    g_test_assert_expected_messages();
+
+    g_clear_error(&error);
+}
+
 static void gjstest_test_func_gjs_context_eval_non_zero_terminated(void) {
     GjsAutoUnref<GjsContext> gjs = gjs_context_new();
     GError* error = NULL;
@@ -765,6 +812,10 @@ main(int    argc,
 
     g_test_add_func("/gjs/context/construct/destroy", gjstest_test_func_gjs_context_construct_destroy);
     g_test_add_func("/gjs/context/construct/eval", gjstest_test_func_gjs_context_construct_eval);
+    g_test_add_func("/gjs/context/eval/dynamic-import",
+                    gjstest_test_func_gjs_context_eval_dynamic_import);
+    g_test_add_func("/gjs/context/eval/dynamic-import/bad",
+                    gjstest_test_func_gjs_context_eval_dynamic_import_bad);
     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);


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