[gjs/esm/dynamic-imports] esm: Enable dynamic imports.



commit b6d8ada44b1aa4b147eea9cf5c94d4fe11f564a0
Author: Evan Welsh <noreply evanwelsh com>
Date:   Sun Apr 12 18:50:32 2020 -0500

    esm: Enable dynamic imports.

 gjs/global.cpp                      |   3 +
 gjs/internal.cpp                    |  41 ++++++++++++
 gjs/internal.h                      |   6 ++
 gjs/internal/module.js              | 127 +++++++++++++++++++++++++++++++-----
 gjs/internal/module/loaders/file.js |  29 +++++++-
 gjs/module.cpp                      |  25 +++++++
 gjs/module.h                        |   5 ++
 7 files changed, 218 insertions(+), 18 deletions(-)
---
diff --git a/gjs/global.cpp b/gjs/global.cpp
index 2604dd84..314619a2 100644
--- a/gjs/global.cpp
+++ b/gjs/global.cpp
@@ -320,6 +320,7 @@ class GjsGlobal {
         JSRuntime* rt = JS_GetRuntime(cx);
 
         JS::SetModuleResolveHook(rt, gjs_module_resolve);
+        JS::SetModuleDynamicImportHook(rt, gjs_dynamic_module_resolve);
         JS::SetModuleMetadataHook(rt, gjs_populate_module_meta);
     }
 
@@ -443,10 +444,12 @@ class GjsInternalGlobal {
         JS_FN("getModuleURI", GetModuleURI, 1, 0),
         JS_FN("compileAndEvalModule", CompileAndEvalModule, 1, 0),
         JS_FN("debug", Debug, 1, 0),
+        JS_FN("finishDynamicModuleImport", FinishDynamicModuleImport, 3, 0),
         JS_FN("lookupInternalModule", LookupInternalModule, 1, 0),
         JS_FN("lookupModule", LookupModule, 1, 0),
         JS_FN("registerModule", RegisterModule, 5, 0),
         JS_FN("registerInternalModule", RegisterInternalModule, 5, 0),
+        JS_FN("setModuleDynamicImportHook", SetModuleDynamicImportHook, 1, 0),
         JS_FN("setModuleResolveHook", SetModuleResolveHook, 1, 0),
         JS_FS_END};
 
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
index 6863978c..e6dcbd56 100644
--- a/gjs/internal.cpp
+++ b/gjs/internal.cpp
@@ -169,6 +169,47 @@ bool SetModuleResolveHook(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", 1)) {
+        return false;
+    }
+
+    JS::RootedValue mv(cx, args[0]);
+    GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
+    // The dynamic hook is stored in the internal global.
+    JS::RootedObject global(cx, priv->internal_global());
+    gjs_set_global_slot(global, GjsInternalGlobalSlot::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;
+    }
+
+    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);
+    }
+}
+
 bool CompileAndEvalModule(JSContext* cx, unsigned argc, JS::Value* vp) {
     JS::CallArgs args = CallArgsFromVp(argc, vp);
 
diff --git a/gjs/internal.h b/gjs/internal.h
index 475b6ce2..148f3f70 100644
--- a/gjs/internal.h
+++ b/gjs/internal.h
@@ -40,9 +40,15 @@ bool gjs_load_internal_script(JSContext* cx, const char* identifier);
 // setModuleResolveHook
 bool SetModuleResolveHook(JSContext* cx, unsigned argc, JS::Value* vp);
 
+// setModuleDynamicImportHook()
+bool SetModuleDynamicImportHook(JSContext* cx, unsigned argc, JS::Value* vp);
+
 // compileAndEvalModule(id: string)
 bool CompileAndEvalModule(JSContext* cx, unsigned argc, JS::Value* vp);
 
+// finishDynamicModuleImport()
+bool FinishDynamicModuleImport(JSContext* cx, unsigned argc, JS::Value* vp);
+
 // registerModule(id: string, path: string, text: string, length: number, ?:
 // boolean)
 bool RegisterModule(JSContext* cx, unsigned argc, JS::Value* vp);
diff --git a/gjs/internal/module.js b/gjs/internal/module.js
index 6681e8a7..95f7cf7d 100644
--- a/gjs/internal/module.js
+++ b/gjs/internal/module.js
@@ -36,6 +36,7 @@ const allowedRelatives = ['file', 'resource'];
 
 const relativeResolvers = new Map();
 const loaders = new Map();
+const asyncLoaders = new Map();
 
 function registerScheme(...schemes) {
     function forEach(fn, ...args) {
@@ -56,6 +57,13 @@ function registerScheme(...schemes) {
                 loaders.set(scheme, handler);
             });
 
+            return schemeBuilder;
+        },
+        asyncLoader(handler) {
+            forEach(scheme => {
+                asyncLoaders.set(scheme, handler);
+            });
+
             return schemeBuilder;
         },
     };
@@ -153,33 +161,38 @@ function loadURI(uri) {
     }
 }
 
+async function loadURIAsync(uri) {
+    debug(`URI: ${uri.raw}`);
+
+    if (uri.scheme) {
+        const loader = asyncLoaders.get(uri.scheme);
+
+        if (loader)
+            return await loader(uri);
+        else
+            throw new ImportError(`No resolver found for URI: ${uri.raw || uri}`);
+    } else {
+        throw new ImportError(`Unable to load module, module has invalid URI: ${uri.raw || uri}`);
+    }
+}
+
 function resolveSpecifier(specifier, moduleURI = null) {
     // If a module has a path, we'll have stored it in the host field
-    let output = null;
-    let uri = null;
     let parsedURI = null;
 
     if (isRelativePath(specifier)) {
         let resolved = resolveRelativePath(moduleURI, specifier);
 
         parsedURI = parseURI(resolved);
-        uri = resolved;
     } else {
         const parsed = parseURI(specifier);
 
         if (parsed) {
-            uri = parsed.raw;
             parsedURI = parsed;
         }
     }
 
-    if (parsedURI)
-        output = loadURI(parsedURI);
-
-    if (!output)
-        return null;
-
-    return {output, uri};
+    return parsedURI;
 }
 
 function resolveModule(specifier, moduleURI) {
@@ -203,10 +216,10 @@ function resolveModule(specifier, moduleURI) {
 
     // 1) Resolve path and URI-based imports.
 
-    const resolved = resolveSpecifier(specifier, moduleURI);
+    const parsedURI = resolveSpecifier(specifier, moduleURI);
 
-    if (resolved) {
-        const {output, uri} = resolved;
+    if (parsedURI) {
+        const uri = parsedURI.raw;
 
         debug(`Full path found: ${uri}`);
 
@@ -216,7 +229,10 @@ function resolveModule(specifier, moduleURI) {
         if (lookup_module)
             return lookup_module;
 
-        const text = output;
+        let text = loadURI(parsedURI);
+
+        if (!text)
+            return null;
 
         if (!registerModule(uri, uri, text, text.length, false))
             throw new ImportError(`Failed to register module: ${uri}`);
@@ -243,6 +259,87 @@ function resolveModule(specifier, moduleURI) {
     return lookupInternalModule(specifier);
 }
 
+async function resolveModuleAsync(specifier, moduleURI) {
+    debug(`Resolving (asynchronously): ${specifier}...`);
+
+    // Check if the module has already been loaded (absolute imports)
+    if (lookupModule(specifier)) {
+        // Resolve if found.
+        return;
+    }
+
+    if (lookupInternalModule(specifier)) {
+        return;
+    }
+
+    const parsedURI = resolveSpecifier(specifier, moduleURI);
+
+    if (parsedURI) {
+        const uri = parsedURI.raw;
+
+        if (lookupModule(uri)) {
+            return;
+        }
+
+        let text = await loadURIAsync(parsedURI);
+
+        if (!text)
+            return;
+
+        if (!registerModule(uri, uri, text, text.length)) {
+            throw new ImportError(`Failed to register module: ${uri}`);
+        }
+
+        const registered = lookupModule(uri);
+
+        if (registered) {
+            if (compileAndEvalModule(uri)) {
+                return;
+            } else {
+                throw new ImportError(`Failed to compile and evaluate module ${uri}.`);
+            }
+        }
+
+        // Fail by default.
+        throw new ImportError('Unknown dynamic import error occured.');
+    } else {
+        for (const uri of buildInternalPaths(specifier)) {
+            try {
+                const text = await loadURIAsync(uri);
+
+                if (!registerInternalModule(specifier, uri, text, text.length)) {
+                    throw new ImportError(`Failed to register internal module: ${specifier} at ${uri}.`);
+                }
+
+                if (lookupInternalModule(specifier)) {
+                    return;
+                }
+            } catch (err) {
+                debug(`Failed to load ${uri}.`);
+            }
+        }
+
+        throw new ImportError(`Attempted to load unregistered global module: ${specifier}`);
+    }
+}
+
+setModuleDynamicImportHook((referencingInfo, specifier, promise) => {
+    debug('Starting dynamic import...');
+    const uri = getModuleUri(referencingInfo);
+    
+    if (uri)
+        debug(`Found base URI: ${uri}`);
+
+    resolveModuleAsync(specifier, uri).then(() => {
+        debug('Successfully imported module!');
+        finishDynamicModuleImport(referencingInfo, specifier, promise);
+    }).catch(err => {
+        debug(err);
+        debug(err.stack);
+        throw new Error(`Dynamic module import failed: ${err}`);
+    });
+});
+
 setModuleResolveHook((referencingInfo, specifier) => {
     debug('Starting module import...');
     const uri = getModuleURI(referencingInfo);
diff --git a/gjs/internal/module/loaders/file.js b/gjs/internal/module/loaders/file.js
index e36b1135..1e12db67 100644
--- a/gjs/internal/module/loaders/file.js
+++ b/gjs/internal/module/loaders/file.js
@@ -26,15 +26,34 @@ function fromBytes(bytes) {
     return ByteUtils.toString(bytes, 'utf-8');
 }
 
-function loadFileSync(output, full_path) {
+function loadFileSync(output) {
     try {
         const [, bytes] = output.load_contents(null);
         return fromBytes(bytes);
     } catch (error) {
-        throw new Error(`Unable to load file from: ${full_path}`);
+        throw new Error(`Unable to load file from: ${output && output.get_uri()}`);
     }
 }
 
+function loadFileAsync(output) {
+    return new Promise((resolve, reject) => {
+        output.load_contents_async(
+            null,
+            (file, res) => {
+                debug('Contents loaded!');
+
+                try {
+                    const [, bytes] = file.load_contents_finish(res);
+                    const text = fromBytes(bytes);
+
+                    resolve(text);
+                } catch (error) {
+                    reject(error);
+                }
+            });
+    });
+}
+
 registerScheme('file', 'resource')
     .relativeResolver((moduleURI, relativePath) => {
         let module_file = Gio.File.new_for_uri(moduleURI.raw);
@@ -43,8 +62,12 @@ registerScheme('file', 'resource')
         let output = module_parent_file.resolve_relative_path(relativePath);
 
         return output.get_uri();
+    }).asyncLoader(uri => {
+        const file = Gio.File.new_for_uri(uri.raw);
+
+        return loadFileAsync(file);
     }).loader(uri => {
         const file = Gio.File.new_for_uri(uri.raw);
 
-        return loadFileSync(file, file.get_uri());
+        return loadFileSync(file);
     });
diff --git a/gjs/module.cpp b/gjs/module.cpp
index 79dbb59e..3899c272 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -401,6 +401,31 @@ 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_dynamic_module_resolve can only be called on the default "
+             "global.");
+
+    GjsContextPrivate* gjs_cx = GjsContextPrivate::from_cx(cx);
+
+    JS::RootedObject global(cx, gjs_cx->internal_global());
+    JSAutoRealm ar(cx, global);
+
+    JS::RootedValue hookValue(
+        cx, gjs_get_global_slot(global, GjsInternalGlobalSlot::IMPORT_HOOK));
+
+    JS::AutoValueArray<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);
+}
+
 bool gjs_populate_module_meta(JSContext* m_cx,
                               JS::Handle<JS::Value> private_ref,
                               JS::Handle<JSObject*> meta_object) {
diff --git a/gjs/module.h b/gjs/module.h
index d4ecf3dc..433d5d73 100644
--- a/gjs/module.h
+++ b/gjs/module.h
@@ -122,6 +122,11 @@ GJS_JSAPI_RETURN_CONVENTION
 JSObject* gjs_module_resolve(JSContext* cx, JS::HandleValue mod_val,
                              JS::HandleString specifier);
 
+bool gjs_dynamic_module_resolve(JSContext* cx,
+                                JS::Handle<JS::Value> aReferencingPrivate,
+                                JS::Handle<JSString*> aSpecifier,
+                                JS::Handle<JSObject*> aPromise);
+
 bool gjs_populate_module_meta(JSContext* m_cx,
                               JS::Handle<JS::Value> private_ref,
                               JS::Handle<JSObject*> meta_object);


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