mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
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:
parent
d26d9b9b2b
commit
252df0a1db
10 changed files with 206 additions and 8 deletions
|
|
@ -0,0 +1,6 @@
|
|||
export let value = 6;
|
||||
|
||||
export function mutate(newValue)
|
||||
{
|
||||
value = newValue;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
let value = 6;
|
||||
export default value;
|
||||
value = 36;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export default 36;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export default let value = 6;
|
||||
value = 36;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
let value = 6;
|
||||
export { value as default };
|
||||
value = 36;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "export.js";
|
||||
|
|
@ -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",
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue