diff --git a/binaries/data/mods/_test.scriptinterface/module/dynamic_import.js b/binaries/data/mods/_test.scriptinterface/module/dynamic_import.js new file mode 100644 index 0000000000..887e2f02c9 --- /dev/null +++ b/binaries/data/mods/_test.scriptinterface/module/dynamic_import.js @@ -0,0 +1,4 @@ +export default function() +{ + return import("include/pi.js"); +} diff --git a/source/scriptinterface/ModuleLoader.cpp b/source/scriptinterface/ModuleLoader.cpp index 0d80bd038e..65f4653038 100644 --- a/source/scriptinterface/ModuleLoader.cpp +++ b/source/scriptinterface/ModuleLoader.cpp @@ -65,6 +65,18 @@ namespace return std::get<1>(*std::get<0>(insertResult)).m_ModuleObject; } +[[nodiscard]] JSObject* Resolve(const ScriptRequest& rq, + ModuleLoader::RegistryType& registry, JS::HandleObject moduleRequest) +{ + std::string includeString; + const JS::RootedValue pathValue{rq.cx, + JS::StringValue(JS::GetModuleRequestSpecifier(rq.cx, moduleRequest))}; + if (!Script::FromJSVal(rq, pathValue, includeString)) + throw std::logic_error{"The module-name to import isn't a string."}; + + return CompileModule(rq, registry, includeString); +} + [[nodiscard]] JSObject* Evaluate(const ScriptRequest& rq, JS::HandleObject mod) { if (!JS::ModuleLink(rq.cx, mod)) @@ -230,13 +242,7 @@ void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept try { const ScriptRequest rq{cx}; - std::string includeString; - const JS::RootedValue pathValue{rq.cx, - JS::StringValue(JS::GetModuleRequestSpecifier(rq.cx, moduleRequest))}; - if (!Script::FromJSVal(rq, pathValue, includeString)) - throw std::logic_error{"The module-name to import isn't a string."}; - - return CompileModule(rq, rq.GetScriptInterface().GetModuleLoader().m_Registry, includeString); + return Resolve(rq, rq.GetScriptInterface().GetModuleLoader().m_Registry, moduleRequest); } catch (const std::exception& e) { @@ -249,4 +255,29 @@ void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept return nullptr; } } + +[[nodiscard]] bool ModuleLoader::DynamicImportHook(JSContext* cx, JS::HandleValue referencingPrivate, + JS::HandleObject moduleRequest, JS::HandleObject promise) noexcept +{ + const ScriptRequest rq{cx}; + try + { + JS::RootedObject mod{rq.cx, Resolve(rq, rq.GetScriptInterface().GetModuleLoader().m_Registry, + moduleRequest)}; + JS::RootedObject evaluationPromise{rq.cx, Evaluate(rq, mod)}; + return JS::FinishDynamicModuleImport(rq.cx, evaluationPromise, referencingPrivate, + moduleRequest, promise); + } + catch (const std::exception& e) + { + LOGERROR("%s", e.what()); + return JS::FinishDynamicModuleImport(rq.cx, nullptr, referencingPrivate, moduleRequest, + promise); + } + catch (...) + { + return JS::FinishDynamicModuleImport(rq.cx, nullptr, referencingPrivate, moduleRequest, + promise); + } +} } // namespace Script diff --git a/source/scriptinterface/ModuleLoader.h b/source/scriptinterface/ModuleLoader.h index 19a9ac10cd..6ffd3a946e 100644 --- a/source/scriptinterface/ModuleLoader.h +++ b/source/scriptinterface/ModuleLoader.h @@ -102,6 +102,8 @@ private: // Functions used by the `ScriptContext`. [[nodiscard]] static JSObject* ResolveHook(JSContext* cx, JS::HandleValue, JS::HandleObject moduleRequest) noexcept; + [[nodiscard]] static bool DynamicImportHook(JSContext* cx, JS::HandleValue referencingPrivate, + JS::HandleObject moduleRequest, JS::HandleObject promise) noexcept; RegistryType m_Registry; }; diff --git a/source/scriptinterface/ScriptContext.cpp b/source/scriptinterface/ScriptContext.cpp index a3eec88f26..1ff3304c05 100644 --- a/source/scriptinterface/ScriptContext.cpp +++ b/source/scriptinterface/ScriptContext.cpp @@ -154,14 +154,18 @@ ScriptContext::ScriptContext(int contextSize, uint32_t heapGrowthBytesGCTrigger) JS::SetJobQueue(m_cx, m_JobQueue.get()); JS::SetPromiseRejectionTrackerCallback(m_cx, &Script::UnhandledRejectedPromise); - JS::SetModuleResolveHook(JS_GetRuntime(m_cx), &Script::ModuleLoader::ResolveHook); + JSRuntime* runtime{JS_GetRuntime(m_cx)}; + JS::SetModuleResolveHook(runtime, &Script::ModuleLoader::ResolveHook); + JS::SetModuleDynamicImportHook(runtime, &Script::ModuleLoader::DynamicImportHook); } ScriptContext::~ScriptContext() { ENSURE(ScriptEngine::IsInitialised() && "The ScriptEngine must be active (initialized and not yet shut down) when destroying a ScriptContext!"); - JS::SetModuleResolveHook(JS_GetRuntime(m_cx), nullptr); + JSRuntime* runtime{JS_GetRuntime(m_cx)}; + JS::SetModuleDynamicImportHook(runtime, nullptr); + JS::SetModuleResolveHook(runtime, nullptr); // Switch back to normal performance mode to avoid assertion in debug mode. js::gc::SetPerformanceHint(m_cx, js::gc::PerformanceHint::Normal); diff --git a/source/scriptinterface/tests/test_Module.h b/source/scriptinterface/tests/test_Module.h index 42b1a32c61..a0241972cf 100644 --- a/source/scriptinterface/tests/test_Module.h +++ b/source/scriptinterface/tests/test_Module.h @@ -22,6 +22,7 @@ #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ModuleLoader.h" #include "scriptinterface/Object.h" +#include "scriptinterface/Promises.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" @@ -369,4 +370,35 @@ public: TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value)); TS_ASSERT_EQUALS(value, 6); } + + void test_DynamicImport() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + + auto future = script.GetModuleLoader().LoadModule(rq, "dynamic_import.js"); + + g_ScriptContext->RunJobs(); + + JS::RootedObject ns{rq.cx, future.Get()}; + JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)}; + + JS::RootedValue promise{rq.cx}; + TS_ASSERT(ScriptFunction::Call(rq, moduleValue, "default", &promise)); + TS_ASSERT(promise.isObject()); + JS::RootedObject promiseObject{rq.cx, &promise.toObject()}; + TS_ASSERT(JS::IsPromiseObject(promiseObject)); + + TS_ASSERT_EQUALS(JS::GetPromiseState(promiseObject), JS::PromiseState::Pending); + g_ScriptContext->RunJobs(); + TS_ASSERT_EQUALS(JS::GetPromiseState(promiseObject), JS::PromiseState::Fulfilled); + + JS::RootedValue piModule{rq.cx, JS::GetPromiseResult(promiseObject)}; + + double pi{0.0}; + TS_ASSERT(Script::FromJSProperty(rq, piModule, "default", pi)); + + TS_ASSERT_LESS_THAN(pi, 3.1416); + TS_ASSERT_LESS_THAN(3.1415, pi); + } };