diff options
-rw-r--r-- | gjs/context.cpp | 8 | ||||
-rw-r--r-- | gjs/module.cpp | 39 | ||||
-rw-r--r-- | gjs/module.h | 3 | ||||
-rw-r--r-- | installed-tests/js/jsunit.gresources.xml | 1 | ||||
-rw-r--r-- | installed-tests/js/modules/dynamic.js | 10 | ||||
-rw-r--r-- | installed-tests/js/testImporter.js | 13 | ||||
-rw-r--r-- | modules/internal/loader.js | 3 | ||||
-rw-r--r-- | test/gjs-tests.cpp | 81 |
8 files changed, 155 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 63efe59a..61bc4e9a 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 * @@ -235,6 +255,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 * @importer: the JS importer object, parent of the module to be imported diff --git a/gjs/module.h b/gjs/module.h index dfe6f18c..39d550b4 100644 --- a/gjs/module.h +++ b/gjs/module.h @@ -22,6 +22,9 @@ gjs_module_import(JSContext *cx, 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); GJS_JSAPI_RETURN_CONVENTION diff --git a/installed-tests/js/jsunit.gresources.xml b/installed-tests/js/jsunit.gresources.xml index 624ac738..b635c50c 100644 --- a/installed-tests/js/jsunit.gresources.xml +++ b/installed-tests/js/jsunit.gresources.xml @@ -13,6 +13,7 @@ <file>modules/badOverrides/Regress.js</file> <file>modules/badOverrides/WarnLib.js</file> <file>modules/data.txt</file> + <file>modules/dynamic.js</file> <file>modules/importmeta.js</file> <file>modules/exports.js</file> <file>modules/foobar.js</file> diff --git a/installed-tests/js/modules/dynamic.js b/installed-tests/js/modules/dynamic.js new file mode 100644 index 00000000..383ee7a0 --- /dev/null +++ b/installed-tests/js/modules/dynamic.js @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later +// SPDX-FileCopyrightText: 2021 Evan Welsh + +/* exported test */ + +async function test() { + const {say} = await import('./say.js'); + + return say('I did it!'); +} diff --git a/installed-tests/js/testImporter.js b/installed-tests/js/testImporter.js index b9c519c5..58e9e9c5 100644 --- a/installed-tests/js/testImporter.js +++ b/installed-tests/js/testImporter.js @@ -237,4 +237,17 @@ describe('Importer', function () { expect(imports[0]).not.toBeDefined(); expect(imports.foobar[0]).not.toBeDefined(); }); + + it('scripts support relative dynamic imports', async function () { + const {say} = await import('./modules/say.js'); + + expect(typeof say).toBe('function'); + expect(say('hello')).toBe('<( hello )'); + }); + + it('imported scripts support relative dynamic imports', async function () { + const response = await imports.dynamic.test(); + + expect(response).toBe('<( I did it! )'); + }); }); 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..d8a32102 100644 --- a/test/gjs-tests.cpp +++ b/test/gjs-tests.cpp @@ -112,6 +112,81 @@ 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(logError) + .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_relative() { + GjsAutoUnref<GjsContext> gjs = gjs_context_new(); + GError* error = NULL; + int status; + + bool ok = g_file_set_contents("num.js", "export default 77;", -1, &error); + + g_assert_true(ok); + g_assert_no_error(error); + + ok = gjs_context_eval(gjs, R"js( + let num; + import('./num.js') + .then(module => (num = module.default)) + .catch(logError) + .finally(() => imports.mainloop.quit()); + imports.mainloop.run(); + num; + )js", + -1, "<main>", &status, &error); + + g_assert_true(ok); + g_assert_no_error(error); + g_assert_cmpint(status, ==, 77); + + g_unlink("num.js"); +} + +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 +840,12 @@ 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/relative", + gjstest_test_func_gjs_context_eval_dynamic_import_relative); + 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); |