[gjs/esm/dynamic-imports: 4/7] Add dynamic import support to scripts




commit 4a20e8b159b36a2af6f4a9492a837222b2cceeed
Author: Evan Welsh <contact evanwelsh com>
Date:   Fri Feb 5 21:51:41 2021 -0800

    Add dynamic import support to scripts
    
    To allow import() from scripts we must set script privates
    with a object similar to the ModulePrivate class.

 gjs/context.cpp            |  8 ++++++++
 gjs/module.cpp             | 39 +++++++++++++++++++++++++++++++++--
 gjs/module.h               |  3 +++
 modules/internal/loader.js |  3 ++-
 test/gjs-tests.cpp         | 51 ++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 101 insertions(+), 3 deletions(-)
---
diff --git a/gjs/context.cpp b/gjs/context.cpp
index 64ef9605..379f623d 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -1253,6 +1253,14 @@ bool GjsContextPrivate::eval_with_scope(JS::HandleObject scope_object,
     JS::CompileOptions options(m_cx);
     options.setFileAndLine(filename, 1);
 
+    GjsAutoUnref<GFile> file = g_file_new_for_commandline_arg(filename);
+    GjsAutoChar uri = g_file_get_uri(file);
+    JS::RootedObject priv(m_cx, gjs_script_module_build_private(m_cx, uri));
+    if (!priv)
+        return false;
+
+    options.setPrivateValue(JS::ObjectValue(*priv));
+
     if (!JS::Evaluate(m_cx, scope_chain, options, buf, retval))
         return false;
 
diff --git a/gjs/module.cpp b/gjs/module.cpp
index 264d83ce..e047ef58 100644
--- a/gjs/module.cpp
+++ b/gjs/module.cpp
@@ -94,7 +94,7 @@ class GjsScriptModule {
     GJS_JSAPI_RETURN_CONVENTION
     bool evaluate_import(JSContext* cx, JS::HandleObject module,
                          const char* script, ssize_t script_len,
-                         const char* filename) {
+                         const char* filename, const char* uri) {
         std::u16string utf16_string =
             gjs_utf8_script_to_utf16(script, script_len);
         // COMPAT: This could use JS::SourceText<mozilla::Utf8Unit> directly,
@@ -114,6 +114,9 @@ class GjsScriptModule {
         JS::CompileOptions options(cx);
         options.setFileAndLine(filename, 1);
 
+        JS::RootedObject priv(cx, build_private(cx, uri));
+        options.setPrivateValue(JS::ObjectValue(*priv));
+
         JS::RootedValue ignored_retval(cx);
         if (!JS::Evaluate(cx, scope_chain, options, buf, &ignored_retval))
             return false;
@@ -145,7 +148,8 @@ class GjsScriptModule {
         g_assert(script);
 
         GjsAutoChar full_path = g_file_get_parse_name(file);
-        return evaluate_import(cx, module, script, script_len, full_path);
+        GjsAutoChar uri = g_file_get_uri(file);
+        return evaluate_import(cx, module, script, script_len, full_path, uri);
     }
 
     /* JSClass operations */
@@ -215,6 +219,22 @@ class GjsScriptModule {
     };
 
  public:
+    /*
+     * Creates a JS object to pass to JS::CompileOptions as a script's private.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static JSObject* build_private(JSContext* cx, const char* script_uri) {
+        JS::RootedObject priv(cx, JS_NewPlainObject(cx));
+        const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+
+        JS::RootedValue val(cx);
+        if (!gjs_string_from_utf8(cx, script_uri, &val) ||
+            !JS_SetPropertyById(cx, priv, atoms.uri(), val))
+            return nullptr;
+
+        return priv;
+    }
+
     /* Carries out the import operation */
     GJS_JSAPI_RETURN_CONVENTION
     static JSObject *
@@ -234,6 +254,21 @@ class GjsScriptModule {
     }
 };
 
+/**
+ * gjs_script_module_build_private:
+ * @param cx the #JSContext
+ * @param uri the URI this script module is loaded from
+ *
+ * @brief To support dynamic imports from scripts, we need to provide private
+ * data when we compile scripts which is compatible with our module resolution
+ * hooks in modules/internal/loader.js
+ *
+ * @returns a JSObject which can be used for a JSScript's private data.
+ */
+JSObject* gjs_script_module_build_private(JSContext* cx, const char* uri) {
+    return GjsScriptModule::build_private(cx, uri);
+}
+
 /**
  * gjs_module_import:
  * @cx: the JS context
diff --git a/gjs/module.h b/gjs/module.h
index dfe6f18c..39d550b4 100644
--- a/gjs/module.h
+++ b/gjs/module.h
@@ -21,6 +21,9 @@ gjs_module_import(JSContext       *cx,
                   const char      *name,
                   GFile           *file);
 
+GJS_JSAPI_RETURN_CONVENTION
+JSObject* gjs_script_module_build_private(JSContext* cx, const char* uri);
+
 GJS_JSAPI_RETURN_CONVENTION
 JSObject* gjs_get_native_registry(JSObject* global);
 
diff --git a/modules/internal/loader.js b/modules/internal/loader.js
index f28814c9..63a1c405 100644
--- a/modules/internal/loader.js
+++ b/modules/internal/loader.js
@@ -124,7 +124,8 @@ class ModuleLoader extends InternalModuleLoader {
     }
 
     moduleResolveAsyncHook(importingModulePriv, specifier) {
-        // importingModulePriv may be falsy in the case of gjs_context_eval()
+        // importingModulePriv should never be missing. If it is then a JSScript
+        // is missing a private object
         if (!importingModulePriv || !importingModulePriv.uri)
             throw new ImportError('Cannot resolve relative imports from an unknown file.');
 
diff --git a/test/gjs-tests.cpp b/test/gjs-tests.cpp
index cd79d100..71cc8f34 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"js(
+        import('system')
+            .catch(err => logError(err))
+            .finally(() => imports.mainloop.quit());
+        imports.mainloop.run();
+    )js",
+                               -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"js(
+        let isBad = false;
+        import('badmodule')
+            .catch(err => {
+                logError(err);
+                isBad = true;
+            })
+            .finally(() => imports.mainloop.quit());
+        imports.mainloop.run();
+
+        if (isBad) imports.system.exit(10);
+    )js",
+                               -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]