Return the namespace of a module

To make it easy for the engine to access the exported values the
namespace is returned from the future.
This commit is contained in:
phosit 2025-01-15 21:34:58 +01:00 committed by phosit
parent d26d9b9b2b
commit 252df0a1db
10 changed files with 206 additions and 8 deletions

View file

@ -0,0 +1,6 @@
export let value = 6;
export function mutate(newValue)
{
value = newValue;
}

View file

@ -0,0 +1,3 @@
let value = 6;
export default value;
value = 36;

View file

@ -0,0 +1 @@
export default 36;

View file

@ -0,0 +1,2 @@
export default let value = 6;
value = 36;

View file

@ -0,0 +1,3 @@
let value = 6;
export { value as default };
value = 36;

View file

@ -0,0 +1 @@
export * from "export.js";

View file

@ -15,6 +15,7 @@ const configIgnores = {
// This files deliberately contain errors
"binaries/data/mods/_test.scriptinterface/module/import_inside_function.js",
"binaries/data/mods/_test.scriptinterface/module/export_default/invalid.js",
],
};

View file

@ -108,7 +108,14 @@ bool Call(JSContext* cx, const unsigned argc, JS::Value* vp)
return true;
}
status = ModuleLoader::Future::Fulfilled{};
const auto evaluatingStatus{std::get_if<ModuleLoader::Future::Evaluating>(&status)};
if (!evaluatingStatus)
{
status = ModuleLoader::Future::Rejected{std::make_exception_ptr(std::runtime_error{
"Future is not Pending."})};
return true;
}
status = ModuleLoader::Future::Fulfilled{evaluatingStatus->moduleNamespace};
return true;
}
@ -144,15 +151,24 @@ ModuleLoader::CompiledModule::CompiledModule(const ScriptRequest& rq, const VfsP
}
ModuleLoader::Future::Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath):
m_Status{Evaluating{{rq.cx, JS_NewObject(rq.cx, &callbackClass<false>)},
m_Status{Evaluating{{rq.cx, nullptr}, {rq.cx, JS_NewObject(rq.cx, &callbackClass<false>)},
{rq.cx, JS_NewObject(rq.cx, &callbackClass<true>)}}}
{
// It's possible to access exported values before the complete module is evaluated (whenever
// something is `export`-ed before a top-level `await`).
// Those "partial" module namespaces are not exposed for the following reasons:
// - The use case for them is too limited.
// - JS developers are used to getting either a complete namespace or nothing.
// - Accessing values which are not yet exported results in an error. These errors might implicitly be
// dropped.
JS::RootedObject mod{rq.cx, CompileModule(rq, loader.m_Registry, modulePath)};
JS::RootedObject promise{rq.cx, Evaluate(rq, mod)};
Evaluating& evaluatingStatus{std::get<Evaluating>(m_Status)};
evaluatingStatus.moduleNamespace = JS::GetModuleNamespace(rq.cx, mod);
SetReservedSlot(JS::PrivateValue(static_cast<void*>(&m_Status)));
Evaluating& evaluatingStatus{std::get<Evaluating>(m_Status)};
if (!JS::AddPromiseReactions(rq.cx, promise, evaluatingStatus.fulfill, evaluatingStatus.reject))
throw std::runtime_error{"Failed adding promise reaction."};
}
@ -182,10 +198,10 @@ ModuleLoader::Future::~Future()
return std::holds_alternative<Fulfilled>(m_Status) || std::holds_alternative<Rejected>(m_Status);
}
void ModuleLoader::Future::Get()
[[nodiscard]] JSObject* ModuleLoader::Future::Get()
{
if (std::holds_alternative<Fulfilled>(m_Status))
return;
return std::get<Fulfilled>(std::exchange(m_Status, Invalid{})).moduleNamespace;
std::exception_ptr error{std::move(std::get<Rejected>(m_Status).error)};
m_Status = Invalid{};
std::rethrow_exception(std::move(error));

View file

@ -49,10 +49,14 @@ public:
public:
struct Evaluating
{
JS::PersistentRootedObject moduleNamespace;
JS::PersistentRootedObject fulfill;
JS::PersistentRootedObject reject;
};
struct Fulfilled {};
struct Fulfilled
{
JS::PersistentRootedObject moduleNamespace;
};
struct Rejected
{
std::exception_ptr error;
@ -72,8 +76,10 @@ public:
/**
* Throws if the evaluation of the module failed.
* @return The module namespace. All exported values are a property
* of this object. @c default is a property with name "default".
*/
void Get();
[[nodiscard]] JSObject* Get();
private:
// It's save to not require a `JS::HandleValue` here.

View file

@ -19,7 +19,9 @@
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "scriptinterface/FunctionWrapper.h"
#include "scriptinterface/ModuleLoader.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
@ -125,6 +127,7 @@ public:
TS_ASSERT(!future.IsDone());
g_ScriptContext->RunJobs();
TS_ASSERT(future.IsDone());
std::ignore = future.Get();
}
void test_TopLevelAwaitInfinite()
@ -136,6 +139,7 @@ public:
g_ScriptContext->RunJobs();
TS_ASSERT(!future.IsDone());
TS_ASSERT_THROWS_ANYTHING(std::ignore = future.Get());
}
void test_MoveFulfilledFuture()
@ -207,7 +211,162 @@ public:
g_ScriptContext->RunJobs();
TS_ASSERT(future.IsDone());
TS_ASSERT_THROWS_EQUALS(future.Get(), const std::runtime_error& e, e.what(),
TS_ASSERT_THROWS_EQUALS(std::ignore = future.Get(), const std::runtime_error& e, e.what(),
"Error: Test reason\n@top_level_throw.js:1:7\n");
}
void test_Export()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
auto future = script.GetModuleLoader().LoadModule(rq, "export.js");
g_ScriptContext->RunJobs();
JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
{
int value{0};
TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value));
TS_ASSERT_EQUALS(value, 6);
}
TS_ASSERT(ScriptFunction::CallVoid(rq, moduleValue, "mutate", 12));
{
int value{0};
TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value));
TS_ASSERT_EQUALS(value, 12);
}
}
void test_ExportSame()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
{
auto future = script.GetModuleLoader().LoadModule(rq, "export.js");
g_ScriptContext->RunJobs();
JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
TS_ASSERT(ScriptFunction::CallVoid(rq, moduleValue, "mutate", 12));
}
{
auto future = script.GetModuleLoader().LoadModule(rq, "include/../export.js");
g_ScriptContext->RunJobs();
JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
int value{0};
TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value));
TS_ASSERT_EQUALS(value, 12);
}
}
void test_ExportIndirect()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
{
auto future = script.GetModuleLoader().LoadModule(rq, "export.js");
g_ScriptContext->RunJobs();
JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
TS_ASSERT(ScriptFunction::CallVoid(rq, moduleValue, "mutate", 12));
}
{
auto future = script.GetModuleLoader().LoadModule(rq, "indirect.js");
g_ScriptContext->RunJobs();
JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
int value{0};
TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value));
TS_ASSERT_EQUALS(value, 12);
}
}
void test_ExportDefaultImmutable()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
auto future = script.GetModuleLoader().LoadModule(rq, "export_default/immutable.js");
g_ScriptContext->RunJobs();
JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
int value{0};
TS_ASSERT(Script::GetProperty(rq, moduleValue, "default", value));
TS_ASSERT_EQUALS(value, 36);
}
void test_ExportDefaultInvalid()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
TestLogger logger;
TS_ASSERT_THROWS(std::ignore = script.GetModuleLoader().LoadModule(rq,
"export_default/invalid.js"), const std::invalid_argument&);
const std::string log{logger.GetOutput()};
TS_ASSERT_STR_CONTAINS(log, "export_default/invalid.js line 1");
}
void test_ExportDefaultDoesNotWorkAround()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
auto future = script.GetModuleLoader().LoadModule(rq, "export_default/does_not_work_around.js");
g_ScriptContext->RunJobs();
JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
int value{0};
TS_ASSERT(Script::GetProperty(rq, moduleValue, "default", value));
TS_ASSERT_DIFFERS(value, 36);
TS_ASSERT_EQUALS(value, 6);
}
void test_ExportDefaultWorksAround()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
auto future = script.GetModuleLoader().LoadModule(rq, "export_default/works_around.js");
g_ScriptContext->RunJobs();
JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
int value{0};
TS_ASSERT(Script::GetProperty(rq, moduleValue, "default", value));
TS_ASSERT_EQUALS(value, 36);
}
void test_ReplaceEvaluatingFuture()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js");
future = script.GetModuleLoader().LoadModule(rq, "export.js");
g_ScriptContext->RunJobs();
JS::RootedObject ns{rq.cx, future.Get()};
JS::RootedValue moduleValue{rq.cx, JS::ObjectValue(*ns)};
int value{0};
TS_ASSERT(Script::GetProperty(rq, moduleValue, "value", value));
TS_ASSERT_EQUALS(value, 6);
}
};