[gjs/esm/dynamic-imports: 290/290] Implement dynamic imports
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/esm/dynamic-imports: 290/290] Implement dynamic imports
- Date: Sat, 14 Nov 2020 20:36:51 +0000 (UTC)
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]