[gjs/esm/dynamic-imports: 290/290] Implement dynamic imports




commit 3d4b652653ac7f5bd629534bab2487f5195d9971
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/global.h            |   2 +
 gjs/internal.cpp        |  79 ++++++++++++++++++++++++++++++++++-
 gjs/internal.h          |   6 +++
 gjs/module.cpp          |  27 ++++++++++++
 gjs/module.h            |   6 +++
 lib/bootstrap/module.js | 109 +++++++++++++++++++++++++++++++++++++++++++++++-
 lib/modules/esm.js      |  58 ++++++++++++++++++++++++++
 lib/types.d.ts          |   8 +++-
 package.json            |   2 +-
 yarn.lock               |  20 +++++++++
 12 files changed, 317 insertions(+), 4 deletions(-)
---
diff --git a/gjs/context.cpp b/gjs/context.cpp
index 5805c2aa..8be00cce 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -552,6 +552,7 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
     JS::LeaveRealm(m_cx, realm);
 
     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 ac8de60f..460ee0d3 100644
--- a/gjs/global.cpp
+++ b/gjs/global.cpp
@@ -284,11 +284,14 @@ class GjsInternalGlobal : GjsBaseGlobal {
     static constexpr JSFunctionSpec static_funcs[] = {
         JS_FN("compileModule", CompileModule, 2, 0),
         JS_FN("compileInternalModule", CompileInternalModule, 2, 0),
+        JS_FN("finishDynamicModuleImport", FinishDynamicModuleImport, 3, 0),
         JS_FN("getRegistry", GetRegistry, 1, 0),
         JS_FN("importSync", ImportSync, 1, 0),
+        JS_FN("initAndEval", InitAndEval, 1, 0),
         JS_FN("setModuleLoadHook", SetModuleLoadHook, 3, 0),
         JS_FN("setModuleMetaHook", SetModuleMetaHook, 2, 0),
         JS_FN("setModulePrivate", SetModulePrivate, 2, 0),
+        JS_FN("setModuleDynamicImportHook", SetModuleDynamicImportHook, 2, 0),
         JS_FN("setModuleResolveHook", SetModuleResolveHook, 2, 0),
         JS_FS_END};
 
diff --git a/gjs/global.h b/gjs/global.h
index 18b7a185..e877dec0 100644
--- a/gjs/global.h
+++ b/gjs/global.h
@@ -35,6 +35,8 @@ enum class GjsGlobalSlot : uint32_t {
     IMPORTS = static_cast<uint32_t>(GjsBaseGlobalSlot::LAST),
     // Stores the import resolution hook
     IMPORT_HOOK,
+    // Stores the import resolution hook
+    DYNAMIC_IMPORT_HOOK,
     // Stores the module creation hook
     MODULE_HOOK,
     // Stores the metadata population hook
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
index a92fa732..b696474f 100644
--- a/gjs/internal.cpp
+++ b/gjs/internal.cpp
@@ -160,6 +160,59 @@ bool SetModuleLoadHook(JSContext* cx, unsigned argc, JS::Value* vp) {
     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);
+    }
+}
+
 /**
  * SetModuleResolveHook:
  *
@@ -247,7 +300,7 @@ static bool compile_module(JSContext* cx, JS::CallArgs args) {
 
     JS::UniqueChars uri = JS_EncodeStringToUTF8(cx, s1);
     JS::UniqueChars text = JS_EncodeStringToUTF8(cx, s2);
-    size_t text_len = JS_GetStringLength(s2);
+    size_t text_len = strlen(text.get());
 
     JS::CompileOptions options(cx);
     options.setFileAndLine(uri.get(), 1).setSourceIsLazy(false);
@@ -349,6 +402,30 @@ bool SetModulePrivate(JSContext* cx, unsigned argc, JS::Value* vp) {
     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;
+}
+
 /**
  * ImportSync:
  *
diff --git a/gjs/internal.h b/gjs/internal.h
index 91e47f59..82bb0d58 100644
--- a/gjs/internal.h
+++ b/gjs/internal.h
@@ -15,10 +15,16 @@ bool CompileModule(JSContext* cx, unsigned argc, JS::Value* vp);
 
 bool CompileInternalModule(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);
+
 bool GetRegistry(JSContext* cx, unsigned argc, JS::Value* vp);
 
 bool ImportSync(JSContext* cx, unsigned argc, JS::Value* vp);
 
+bool InitAndEval(JSContext* cx, unsigned argc, JS::Value* vp);
+
 bool SetModuleLoadHook(JSContext* cx, unsigned argc, JS::Value* vp);
 
 bool SetModuleMetaHook(JSContext* cx, unsigned argc, JS::Value* vp);
diff --git a/gjs/module.cpp b/gjs/module.cpp
index 953d91fe..c9bddd93 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -420,3 +420,30 @@ JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue importer,
 
     return module;
 }
+
+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 b2638933..ca6d08b7 100644
--- a/gjs/module.h
+++ b/gjs/module.h
@@ -36,6 +36,12 @@ GJS_JSAPI_RETURN_CONVENTION
 JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue mod_val,
                              JS::HandleString specifier);
 
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_dynamic_module_resolve(JSContext* cx,
+                                JS::Handle<JS::Value> aReferencingPrivate,
+                                JS::Handle<JSString*> aSpecifier,
+                                JS::Handle<JSObject*> aPromise);
+
 GJS_JSAPI_RETURN_CONVENTION
 bool gjs_populate_module_meta(JSContext* m_cx,
                               JS::Handle<JS::Value> private_ref,
diff --git a/lib/bootstrap/module.js b/lib/bootstrap/module.js
index 0c5fda23..62a74188 100644
--- a/lib/bootstrap/module.js
+++ b/lib/bootstrap/module.js
@@ -60,6 +60,43 @@ function fromBytes(bytes) {
     return ByteUtils.toString(bytes, 'utf-8');
 }
 
+/**
+ * @param {import("gio").File} file the Gio.File to load from.
+ * @returns {Promise<string>}
+ */
+function loadFileAsync(file) {
+    return new Promise((resolve, reject) => {
+        file.load_contents_async(
+            null,
+            (file, res) => {
+                if (!file) {
+                    return reject(new ImportError(`File not found!`));
+                }
+
+                try {
+                    const [, bytes] = file.load_contents_finish(res);
+                    const text = fromBytes(bytes);
+
+                    resolve(text);
+                } catch (error) {
+                    reject(error);
+                }
+            });
+    });
+}
+
+/**
+ * Synchronously loads a file's text from a URI.
+ *
+ * @param {string} uri the URI to load
+ * @returns {Promise<string>}
+ */
+function loadResourceOrFileAsync(uri) {
+    let output = Gio.File.new_for_uri(uri);
+
+    return loadFileAsync(output);
+}
+
 /**
  * @param {import("gio").File} file the Gio.File to load from.
  * @returns {string}
@@ -68,7 +105,9 @@ function loadFileSync(file) {
     try {
         const [, bytes] = file.load_contents(null);
 
-        return fromBytes(bytes);
+      let text = fromBytes(bytes);
+
+      return text;
     } catch (error) {
         throw new ImportError(`Unable to load file from: ${file.get_uri()}`);
     }
@@ -159,6 +198,21 @@ export class ModuleLoader {
 
         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.
@@ -270,6 +324,59 @@ export class ModuleLoader {
             return compiled;
         }
 
+        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;
     }
 }
diff --git a/lib/modules/esm.js b/lib/modules/esm.js
index 7b7d2676..831d3b21 100644
--- a/lib/modules/esm.js
+++ b/lib/modules/esm.js
@@ -148,6 +148,53 @@ export class ESModuleLoader extends ModuleLoader {
 
         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;
+    }
 }
 
 export const moduleLoader = new ESModuleLoader(moduleGlobalThis);
@@ -217,3 +264,14 @@ setModuleResolveHook(moduleGlobalThis, (module, specifier) => {
     return moduleLoader.resolveModule(specifier, module.uri);
 });
 
+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}`);
+    });
+});
\ No newline at end of file
diff --git a/lib/types.d.ts b/lib/types.d.ts
index 935cddc5..5ce347d3 100644
--- a/lib/types.d.ts
+++ b/lib/types.d.ts
@@ -57,12 +57,18 @@ declare global {
     export function setModulePrivate(module: Module, private: ESModule);
 
     /**
-     * 
      * @param global 
      * @param hook 
      */
     export function setModuleLoadHook(global: typeof globalThis, hook: (id: string, uri: string) => Module);
+    
+    /** 
+     * @param global 
+     * @param hook 
+     */
+    export function setModuleDynamicImportHook(global: typeof globalThis, hook: (module: ESModule, 
specifier: string, promise: unknown) => void);
 
+    export function finishDynamicModuleImport(module: ESModule, specifier: string, promise: unknown): void;
     /**
      * 
      * @param global 
diff --git a/package.json b/package.json
index 5c293e60..4b48e1fd 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
     "typecheck:lib": "tsc -p lib/jsconfig.json"
   },
   "devDependencies": {
-    "@gi-types/gio": "^2.66.3",
+    "@gi-types/gio": "^2.66.5",
     "@gi-types/glib": "^2.66.3",
     "@gi-types/gobject": "^2.66.3",
     "@gi-types/gtk": "^3.24.3",
diff --git a/yarn.lock b/yarn.lock
index a5829c6a..9e35addd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -77,11 +77,24 @@
   dependencies:
     "@gi-types/gobject" "^2.66.3"
 
+"@gi-types/gio@^2.66.5":
+  version "2.66.5"
+  resolved 
"https://registry.yarnpkg.com/@gi-types/gio/-/gio-2.66.5.tgz#6ef8755e9f1ec818f69b397b5464b4a70e8033fe";
+  integrity sha512-i1aTbsOT3p4bl6/MWbXT/GNxM9XCZ1eZ0oEbb65V14eoM60KtKycZ/T/WyPA4fLHTATUJJN0MESpFzMVuBzrtA==
+  dependencies:
+    "@gi-types/glib" "^2.66.5"
+    "@gi-types/gobject" "^2.66.5"
+
 "@gi-types/glib@^2.66.2", "@gi-types/glib@^2.66.3":
   version "2.66.3"
   resolved 
"https://registry.yarnpkg.com/@gi-types/glib/-/glib-2.66.3.tgz#795e6566f25de5dddaab72fa30c16136334c80c7";
   integrity sha512-EoT/w9ImW5QyMicZ/I9yh3CzgZWcTPrm96siyC5qApNzblLsNixt3MzPQvnFoKqdLNCHrbAcgXTHZKzyqVztww==
 
+"@gi-types/glib@^2.66.5":
+  version "2.66.5"
+  resolved 
"https://registry.yarnpkg.com/@gi-types/glib/-/glib-2.66.5.tgz#a396e941366d672538b43a10d9909aabb7f5d3c7";
+  integrity sha512-5S/L0fOtVii9djkEqw/CaIVnsd3uF8VwK2bRqKAf2HiZ8WcVCh5SOW7spCbOD42En42oB0VTYAVKKPBxceP9ig==
+
 "@gi-types/gobject@^2.66.2", "@gi-types/gobject@^2.66.3":
   version "2.66.3"
   resolved 
"https://registry.yarnpkg.com/@gi-types/gobject/-/gobject-2.66.3.tgz#41bc8b141a5bb78bcbce6252646d7ef71fa8b275";
@@ -89,6 +102,13 @@
   dependencies:
     "@gi-types/glib" "^2.66.2"
 
+"@gi-types/gobject@^2.66.5":
+  version "2.66.5"
+  resolved 
"https://registry.yarnpkg.com/@gi-types/gobject/-/gobject-2.66.5.tgz#a1f193bffde7753f90c8d6c147a6ec424d067e76";
+  integrity sha512-3qX2bw+ITRDhkTLU+tAgggzV+W80hjYh6gQaUjbVyAJEIO0oFQ8EjfyipgawU/akxv377iNgFx/OZcNSP++1Ow==
+  dependencies:
+    "@gi-types/glib" "^2.66.5"
+
 "@gi-types/gtk@^3.24.3":
   version "3.24.3"
   resolved 
"https://registry.yarnpkg.com/@gi-types/gtk/-/gtk-3.24.3.tgz#a5d5c12edf27313c54f22d8def69625e933eb0c7";


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