LibJS+LibWeb: More bringing module loading closer to spec

In particular, this patch removes three host hooks on JS::VM in favor
of the new JS-side module loading stuff.
This commit is contained in:
Andreas Kling 2023-12-02 22:56:47 +01:00
parent 07f567cd9f
commit 8b7d27b349
Notes: sideshowbarker 2024-07-16 23:57:20 +09:00
15 changed files with 234 additions and 319 deletions

View File

@ -305,7 +305,7 @@ extern "C" int initialize_repl(char const* time_zone)
setenv("TZ", time_zone, 1);
g_vm = MUST(JS::VM::create());
g_vm->enable_default_host_import_module_dynamically_hook();
g_vm->set_dynamic_imports_allowed(true);
// NOTE: These will print out both warnings when using something like Promise.reject().catch(...) -
// which is, as far as I can tell, correct - a promise is created, rejected without handler, and a

View File

@ -214,7 +214,7 @@ static Result<void, TestError> run_test(StringView source, StringView filepath,
}
auto vm = MUST(JS::VM::create());
vm->enable_default_host_import_module_dynamically_hook();
vm->set_dynamic_imports_allowed(true);
JS::GCPtr<JS::Realm> realm;
JS::GCPtr<JS::Test262::GlobalObject> global_object;

View File

@ -38,7 +38,7 @@ Workbook::Workbook(Vector<NonnullRefPtr<Sheet>>&& sheets, GUI::Window& parent_wi
m_main_execution_context->realm = &realm;
m_main_execution_context->is_strict_mode = true;
m_vm->push_execution_context(*m_main_execution_context);
m_vm->enable_default_host_import_module_dynamically_hook();
m_vm->set_dynamic_imports_allowed(true);
}
bool Workbook::set_filename(DeprecatedString const& filename)

View File

@ -45,13 +45,13 @@ void GraphLoadingState::visit_edges(Cell::Visitor& visitor)
}
// 16.2.1.5.1 LoadRequestedModules ( [ hostDefined ] ), https://tc39.es/ecma262/#sec-LoadRequestedModules
PromiseCapability& CyclicModule::load_requested_modules(JS::Realm& realm, GCPtr<GraphLoadingState::HostDefined> host_defined)
PromiseCapability& CyclicModule::load_requested_modules(GCPtr<GraphLoadingState::HostDefined> host_defined)
{
// 1. If hostDefined is not present, let hostDefined be EMPTY.
// NOTE: The empty state is handled by hostDefined being an optional without value.
// 2. Let pc be ! NewPromiseCapability(%Promise%).
auto promise_capability = MUST(new_promise_capability(realm.vm(), realm.intrinsics().promise_constructor()));
auto promise_capability = MUST(new_promise_capability(vm(), vm().current_realm()->intrinsics().promise_constructor()));
// 3. Let state be the GraphLoadingState Record { [[IsLoading]]: true, [[PendingModulesCount]]: 1, [[Visited]]: « », [[PromiseCapability]]: pc, [[HostDefined]]: hostDefined }.
auto state = heap().allocate_without_realm<GraphLoadingState>(promise_capability, true, 1, HashTable<CyclicModule*> {}, move(host_defined));
@ -101,7 +101,7 @@ void CyclicModule::inner_module_loading(JS::GraphLoadingState& state)
// ii. Else,
if (!found_record_in_loaded_modules) {
// 1. Perform HostLoadImportedModule(module, required, state.[[HostDefined]], state).
vm().host_load_imported_module(realm(), NonnullGCPtr<CyclicModule>(*this), required, state.host_defined, state);
vm().host_load_imported_module(NonnullGCPtr<CyclicModule> { *this }, required, state.host_defined, NonnullGCPtr<GraphLoadingState> { state });
// 2. NOTE: HostLoadImportedModule will call FinishLoadingImportedModule, which re-enters the graph loading process through ContinueModuleLoading.
}
@ -138,7 +138,7 @@ void CyclicModule::inner_module_loading(JS::GraphLoadingState& state)
}
// 16.2.1.5.1.2 ContinueModuleLoading ( state, moduleCompletion ), https://tc39.es/ecma262/#sec-ContinueModuleLoading
void continue_module_loading(Realm& realm, GraphLoadingState& state, ThrowCompletionOr<Module*> const& module_completion)
void continue_module_loading(GraphLoadingState& state, ThrowCompletionOr<NonnullGCPtr<Module>> const& module_completion)
{
// 1. If state.[[IsLoading]] is false, return UNUSED.
if (state.is_loading)
@ -146,10 +146,10 @@ void continue_module_loading(Realm& realm, GraphLoadingState& state, ThrowComple
// 2. If moduleCompletion is a normal completion, then
if (!module_completion.is_error()) {
auto* module = const_cast<Module*>(module_completion.value());
auto module = module_completion.value();
// a. Perform InnerModuleLoading(state, moduleCompletion.[[Value]]).
static_cast<CyclicModule*>(module)->inner_module_loading(state);
verify_cast<CyclicModule>(*module).inner_module_loading(state);
}
// 3. Else,
else {
@ -159,7 +159,7 @@ void continue_module_loading(Realm& realm, GraphLoadingState& state, ThrowComple
auto value = module_completion.throw_completion().value();
// b. Perform ! Call(state.[[PromiseCapability]].[[Reject]], undefined, « moduleCompletion.[[Value]] »).
MUST(call(realm.vm(), *state.promise_capability->reject(), js_undefined(), *value));
MUST(call(state.vm(), *state.promise_capability->reject(), js_undefined(), *value));
}
// 4. Return UNUSED.
@ -318,14 +318,14 @@ ThrowCompletionOr<u32> CyclicModule::inner_module_linking(VM& vm, Vector<Module*
return index;
}
// 16.2.1.5.2 Evaluate ( ), https://tc39.es/ecma262/#sec-moduleevaluation
// 16.2.1.5.3 Evaluate ( ), https://tc39.es/ecma262/#sec-moduleevaluation
ThrowCompletionOr<Promise*> CyclicModule::evaluate(VM& vm)
{
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] evaluate[{}](vm)", this);
// 1. Assert: This call to Evaluate is not happening at the same time as another call to Evaluate within the surrounding agent.
// FIXME: Verify this somehow
// 2. Assert: module.[[Status]] is linked, evaluating-async, or evaluated.
// 2. Assert: module.[[Status]] is one of linked, evaluating-async, or evaluated.
VERIFY(m_status == ModuleStatus::Linked || m_status == ModuleStatus::EvaluatingAsync || m_status == ModuleStatus::Evaluated);
// NOTE: The spec does not catch the case where evaluate is called twice on a script which failed
@ -335,7 +335,7 @@ ThrowCompletionOr<Promise*> CyclicModule::evaluate(VM& vm)
if (m_top_level_capability != nullptr)
return verify_cast<Promise>(m_top_level_capability->promise().ptr());
// 3. If module.[[Status]] is evaluating-async or evaluated, set module to module.[[CycleRoot]].
// 3. If module.[[Status]] is either evaluating-async or evaluated, set module to module.[[CycleRoot]].
if (m_status == ModuleStatus::EvaluatingAsync || m_status == ModuleStatus::Evaluated) {
// Note: This will continue this function with module.[[CycleRoot]]
VERIFY(m_cycle_root);
@ -396,7 +396,7 @@ ThrowCompletionOr<Promise*> CyclicModule::evaluate(VM& vm)
}
// 10. Else,
else {
// a. Assert: module.[[Status]] is evaluating-async or evaluated.
// a. Assert: module.[[Status]] is either evaluating-async or evaluated.
VERIFY(m_status == ModuleStatus::EvaluatingAsync || m_status == ModuleStatus::Evaluated);
// b. Assert: module.[[EvaluationError]] is empty.
VERIFY(!m_evaluation_error.is_error());
@ -830,4 +830,90 @@ NonnullGCPtr<Module> CyclicModule::get_imported_module(ModuleRequest const& requ
VERIFY_NOT_REACHED();
}
// 13.3.10.1.1 ContinueDynamicImport ( promiseCapability, moduleCompletion ), https://tc39.es/ecma262/#sec-ContinueDynamicImport
void continue_dynamic_import(NonnullGCPtr<PromiseCapability> promise_capability, ThrowCompletionOr<NonnullGCPtr<Module>> const& module_completion)
{
auto& vm = promise_capability->vm();
// 1. If moduleCompletion is an abrupt completion, then
if (module_completion.is_throw_completion()) {
// a. Perform ! Call(promiseCapability.[[Reject]], undefined, « moduleCompletion.[[Value]] »).
MUST(call(vm, *promise_capability->reject(), js_undefined(), *module_completion.throw_completion().value()));
// b. Return unused.
return;
}
// 2. Let module be moduleCompletion.[[Value]].
auto& module = *module_completion.value();
// 3. Let loadPromise be module.LoadRequestedModules().
auto& load_promise = verify_cast<CyclicModule>(module).load_requested_modules({});
// 4. Let rejectedClosure be a new Abstract Closure with parameters (reason) that captures promiseCapability and performs the
// following steps when called:
auto reject_closure = [promise_capability](VM& vm) -> ThrowCompletionOr<Value> {
auto reason = vm.argument(0);
// a. Perform ! Call(promiseCapability.[[Reject]], undefined, « reason »).
MUST(call(vm, *promise_capability->reject(), js_undefined(), reason));
// b. Return unused.
return js_undefined();
};
// 5. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »).
auto on_rejected = NativeFunction::create(*vm.current_realm(), move(reject_closure), 1, "");
// 6. Let linkAndEvaluateClosure be a new Abstract Closure with no parameters that captures module, promiseCapability,
// and onRejected and performs the following steps when called:
auto link_and_evaluate_closure = [&module, promise_capability, on_rejected](VM& vm) -> ThrowCompletionOr<Value> {
// a. Let link be Completion(module.Link()).
auto link = module.link(vm);
// b. If link is an abrupt completion, then
if (link.is_throw_completion()) {
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « link.[[Value]] »).
MUST(call(vm, *promise_capability->reject(), js_undefined(), *link.throw_completion().value()));
// ii. Return unused.
return js_undefined();
}
// c. Let evaluatePromise be module.Evaluate().
auto evaluate_promise = module.evaluate(vm);
// d. Let fulfilledClosure be a new Abstract Closure with no parameters that captures module and
// promiseCapability and performs the following steps when called:
auto fulfilled_closure = [&module, promise_capability](VM& vm) -> ThrowCompletionOr<Value> {
// i. Let namespace be GetModuleNamespace(module).
auto namespace_ = module.get_module_namespace(vm);
// ii. Perform ! Call(promiseCapability.[[Resolve]], undefined, « namespace »).
MUST(call(vm, *promise_capability->resolve(), js_undefined(), namespace_.value()));
// iii. Return unused.
return js_undefined();
};
// e. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 0, "", « »).
auto on_fulfilled = NativeFunction::create(*vm.current_realm(), move(fulfilled_closure), 0, "");
// f. Perform PerformPromiseThen(evaluatePromise, onFulfilled, onRejected).
evaluate_promise.value()->perform_then(on_fulfilled, on_rejected, {});
// g. Return unused.
return js_undefined();
};
// 7. Let linkAndEvaluate be CreateBuiltinFunction(linkAndEvaluateClosure, 0, "", « »).
auto link_and_evaluate = NativeFunction::create(*vm.current_realm(), move(link_and_evaluate_closure), 0, "");
// 8. Perform PerformPromiseThen(loadPromise, linkAndEvaluate, onRejected).
// FIXME: This is likely a spec bug, see load_requested_modules.
verify_cast<Promise>(*load_promise.promise()).perform_then(link_and_evaluate, on_rejected, {});
// 9. Return unused.
}
}

View File

@ -65,7 +65,7 @@ public:
virtual ThrowCompletionOr<void> link(VM& vm) override final;
virtual ThrowCompletionOr<Promise*> evaluate(VM& vm) override final;
virtual PromiseCapability& load_requested_modules(Realm&, GCPtr<GraphLoadingState::HostDefined>);
virtual PromiseCapability& load_requested_modules(GCPtr<GraphLoadingState::HostDefined>);
virtual void inner_module_loading(GraphLoadingState& state);
Vector<ModuleRequest> const& requested_modules() const { return m_requested_modules; }
@ -103,6 +103,7 @@ protected:
Optional<u32> m_pending_async_dependencies; // [[PendingAsyncDependencies]]
};
void continue_module_loading(Realm&, GraphLoadingState& state, ThrowCompletionOr<Module*> const&);
void continue_module_loading(GraphLoadingState&, ThrowCompletionOr<NonnullGCPtr<Module>> const&);
void continue_dynamic_import(NonnullGCPtr<PromiseCapability>, ThrowCompletionOr<NonnullGCPtr<Module>> const& module_completion);
}

View File

@ -67,46 +67,52 @@ ThrowCompletionOr<u32> Module::inner_module_evaluation(VM& vm, Vector<Module*>&,
}
// 16.2.1.9 FinishLoadingImportedModule ( referrer, specifier, payload, result ), https://tc39.es/ecma262/#sec-FinishLoadingImportedModule
// FIXME: We currently implement an outdated version of https://tc39.es/proposal-import-attributes, as such it is not possible to
// use the exact steps from https://tc39.es/proposal-import-attributes/#sec-HostLoadImportedModule here.
// FIXME: Support Realm for referrer.
void finish_loading_imported_module(Realm& realm, Variant<JS::NonnullGCPtr<JS::Script>, JS::NonnullGCPtr<JS::CyclicModule>> referrer, ModuleRequest const& module_request, GraphLoadingState& payload, ThrowCompletionOr<Module*> const& result)
void finish_loading_imported_module(ImportedModuleReferrer referrer, ModuleRequest const& module_request, ImportedModulePayload payload, ThrowCompletionOr<NonnullGCPtr<Module>> const& result)
{
// 1. If result is a normal completion, then
if (!result.is_error()) {
auto loaded_modules = referrer.visit(
[](JS::NonnullGCPtr<JS::Script> script) -> Vector<ModuleWithSpecifier> { return script->loaded_modules(); },
[](JS::NonnullGCPtr<JS::CyclicModule> module) -> Vector<ModuleWithSpecifier> { return module->loaded_modules(); });
// NOTE: Only Script and CyclicModule referrers have the [[LoadedModules]] internal slot.
if (referrer.has<NonnullGCPtr<Script>>() || referrer.has<NonnullGCPtr<CyclicModule>>()) {
auto loaded_modules = referrer.visit(
[](JS::NonnullGCPtr<JS::Script> script) -> Vector<ModuleWithSpecifier> { return script->loaded_modules(); },
[](JS::NonnullGCPtr<JS::CyclicModule> module) -> Vector<ModuleWithSpecifier> { return module->loaded_modules(); },
[](auto&) {
VERIFY_NOT_REACHED();
return Vector<ModuleWithSpecifier> {};
});
bool found_record = false;
bool found_record = false;
// a.a. If referrer.[[LoadedModules]] contains a Record whose [[Specifier]] is specifier, then
for (auto const& record : loaded_modules) {
if (record.specifier == module_request.module_specifier) {
// i. Assert: That Record's [[Module]] is result.[[Value]].
VERIFY(record.module == result.value());
found_record = true;
// a. If referrer.[[LoadedModules]] contains a Record whose [[Specifier]] is specifier, then
for (auto const& record : loaded_modules) {
if (record.specifier == module_request.module_specifier) {
// i. Assert: That Record's [[Module]] is result.[[Value]].
VERIFY(record.module == result.value());
found_record = true;
}
}
}
// b. Else,
if (!found_record) {
auto* module = const_cast<Module*>(result.value());
// b. Else,
if (!found_record) {
auto module = result.value();
// i. Append the Record { [[Specifier]]: specifier, [[Module]]: result.[[Value]] } to referrer.[[LoadedModules]].
loaded_modules.append(ModuleWithSpecifier {
.specifier = module_request.module_specifier,
.module = NonnullGCPtr<Module>(*module) });
// i. Append the Record { [[Specifier]]: specifier, [[Module]]: result.[[Value]] } to referrer.[[LoadedModules]].
loaded_modules.append(ModuleWithSpecifier {
.specifier = module_request.module_specifier,
.module = NonnullGCPtr<Module>(*module) });
}
}
}
// FIXME: 2. If payload is a GraphLoadingState Record, then
// a. Perform ContinueModuleLoading(payload, result)
continue_module_loading(realm, payload, result);
// FIXME: Else,
// FIXME: a. Perform ContinueDynamicImport(payload, result).
if (payload.has<NonnullGCPtr<GraphLoadingState>>()) {
// a. Perform ContinueModuleLoading(payload, result)
continue_module_loading(payload.get<NonnullGCPtr<GraphLoadingState>>(), result);
}
// Else,
else {
// a. Perform ContinueDynamicImport(payload, result).
continue_dynamic_import(payload.get<NonnullGCPtr<PromiseCapability>>(), result);
}
// 4. Return unused.
}

View File

@ -9,6 +9,7 @@
#include <AK/DeprecatedFlyString.h>
#include <LibJS/Heap/GCPtr.h>
#include <LibJS/ModuleLoading.h>
#include <LibJS/Runtime/Environment.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Script.h>
@ -113,6 +114,6 @@ private:
class CyclicModule;
struct GraphLoadingState;
void finish_loading_imported_module(Realm&, Variant<NonnullGCPtr<Script>, NonnullGCPtr<CyclicModule>>, ModuleRequest const&, GraphLoadingState&, ThrowCompletionOr<Module*> const&);
void finish_loading_imported_module(ImportedModuleReferrer, ModuleRequest const&, ImportedModulePayload, ThrowCompletionOr<NonnullGCPtr<Module>> const&);
}

View File

@ -1590,7 +1590,7 @@ ThrowCompletionOr<Value> perform_import_call(VM& vm, Value specifier, Value opti
ModuleRequest request { specifier_string, attributes };
// 13. Perform HostLoadImportedModule(referrer, moduleRequest, empty, promiseCapability).
MUST_OR_THROW_OOM(vm.host_import_module_dynamically(referrer, move(request), promise_capability));
vm.host_load_imported_module(referrer, move(request), nullptr, promise_capability);
// 13. Return promiseCapability.[[Promise]].
return Value { promise_capability->promise() };

View File

@ -234,8 +234,9 @@ ThrowCompletionOr<Value> shadow_realm_import_value(VM& vm, DeprecatedString spec
TRY(vm.push_execution_context(eval_context, {}));
// 6. Let referrer be the Realm component of evalContext.
// 7. Perform HostImportModuleDynamically(referrer, specifierString, innerCapability).
MUST_OR_THROW_OOM(vm.host_import_module_dynamically(NonnullGCPtr { *eval_context.realm }, ModuleRequest { move(specifier_string) }, inner_capability));
auto referrer = JS::NonnullGCPtr { *eval_context.realm };
// 7. Perform HostLoadImportedModule(referrer, specifierString, empty, innerCapability).
vm.host_load_imported_module(referrer, ModuleRequest { specifier_string }, nullptr, inner_capability);
// 7. Suspend evalContext and remove it from the execution context stack.
// NOTE: We don't support this concept yet.

View File

@ -94,41 +94,8 @@ VM::VM(OwnPtr<CustomData> custom_data, ErrorMessages error_messages)
return make_job_callback(function_object);
};
host_resolve_imported_module = [&](ImportedModuleReferrer referrer, ModuleRequest const& specifier) {
return resolve_imported_module(referrer, specifier);
};
host_import_module_dynamically = [&](ImportedModuleReferrer, ModuleRequest const&, PromiseCapability const& promise_capability) -> ThrowCompletionOr<void> {
// By default, we throw on dynamic imports this is to prevent arbitrary file access by scripts.
VERIFY(current_realm());
auto& realm = *current_realm();
auto promise = Promise::create(realm);
// If you are here because you want to enable dynamic module importing make sure it won't be a security problem
// by checking the default implementation of HostImportModuleDynamically and creating your own hook or calling
// vm.enable_default_host_import_module_dynamically_hook().
promise->reject(Error::create(realm, ErrorType::DynamicImportNotAllowed.message()));
promise->perform_then(
NativeFunction::create(realm, "", [](auto&) -> ThrowCompletionOr<Value> {
VERIFY_NOT_REACHED();
}),
NativeFunction::create(realm, "", [&promise_capability](auto& vm) -> ThrowCompletionOr<Value> {
auto error = vm.argument(0);
// a. Perform ! Call(promiseCapability.[[Reject]], undefined, « error »).
MUST(call(vm, *promise_capability.reject(), js_undefined(), error));
// b. Return undefined.
return js_undefined();
}),
{});
return {};
};
host_finish_dynamic_import = [&](ImportedModuleReferrer referrer, ModuleRequest const& specifier, PromiseCapability const& promise_capability, Promise* promise) {
return finish_dynamic_import(referrer, specifier, promise_capability, promise);
host_load_imported_module = [this](ImportedModuleReferrer referrer, ModuleRequest const& module_request, GCPtr<GraphLoadingState::HostDefined> load_state, ImportedModulePayload payload) -> void {
return load_imported_module(referrer, module_request, load_state, move(payload));
};
host_get_import_meta_properties = [&](SourceTextModule const&) -> HashMap<PropertyKey, Value> {
@ -181,13 +148,6 @@ String const& VM::error_message(ErrorMessage type) const
return message;
}
void VM::enable_default_host_import_module_dynamically_hook()
{
host_import_module_dynamically = [&](ImportedModuleReferrer referrer, ModuleRequest const& specifier, PromiseCapability const& promise_capability) {
return import_module_dynamically(referrer, specifier, promise_capability);
};
}
Bytecode::Interpreter& VM::bytecode_interpreter()
{
return *m_bytecode_interpreter;
@ -865,22 +825,32 @@ static DeprecatedString resolve_module_filename(StringView filename, StringView
return filename;
}
// 16.2.1.7 HostResolveImportedModule ( referencingScriptOrModule, specifier ), https://tc39.es/ecma262/#sec-hostresolveimportedmodule
ThrowCompletionOr<NonnullGCPtr<Module>> VM::resolve_imported_module(ImportedModuleReferrer referrer, ModuleRequest const& module_request)
// 16.2.1.8 HostLoadImportedModule ( referrer, specifier, hostDefined, payload ), https://tc39.es/ecma262/#sec-HostLoadImportedModule
void VM::load_imported_module(ImportedModuleReferrer referrer, ModuleRequest const& module_request, GCPtr<GraphLoadingState::HostDefined>, ImportedModulePayload payload)
{
// An implementation of HostResolveImportedModule must conform to the following requirements:
// - If it completes normally, the [[Value]] slot of the completion must contain an instance of a concrete subclass of Module Record.
// - If a Module Record corresponding to the pair referencingScriptOrModule, moduleRequest does not exist or cannot be created, an exception must be thrown.
// - Each time this operation is called with a specific referencingScriptOrModule, moduleRequest.[[Specifier]], moduleRequest.[[Assertions]] triple
// as arguments it must return the same Module Record instance if it completes normally.
// * It is recommended but not required that implementations additionally conform to the following stronger constraint:
// each time this operation is called with a specific referencingScriptOrModule, moduleRequest.[[Specifier]] pair as arguments it must return the same Module Record instance if it completes normally.
// - moduleRequest.[[Assertions]] must not influence the interpretation of the module or the module specifier;
// instead, it may be used to determine whether the algorithm completes normally or with an abrupt completion.
// An implementation of HostLoadImportedModule must conform to the following requirements:
//
// - The host environment must perform FinishLoadingImportedModule(referrer, specifier, payload, result),
// where result is either a normal completion containing the loaded Module Record or a throw completion,
// either synchronously or asynchronously.
// - If this operation is called multiple times with the same (referrer, specifier) pair and it performs
// FinishLoadingImportedModule(referrer, specifier, payload, result) where result is a normal completion,
// then it must perform FinishLoadingImportedModule(referrer, specifier, payload, result) with the same result each time.
// - The operation must treat payload as an opaque value to be passed through to FinishLoadingImportedModule.
//
// The actual process performed is host-defined, but typically consists of performing whatever I/O operations are necessary to
// load the appropriate Module Record. Multiple different (referrer, specifier) pairs may map to the same Module Record instance.
// The actual mapping semantics is host-defined but typically a normalization process is applied to specifier as part of the
// mapping process. A typical normalization process would include actions such as expansion of relative and abbreviated path specifiers.
// Multiple different referencingScriptOrModule, moduleRequest.[[Specifier]] pairs may map to the same Module Record instance.
// The actual mapping semantic is host-defined but typically a normalization process is applied to specifier as part of the mapping process.
// A typical normalization process would include actions such as alphabetic case folding and expansion of relative and abbreviated path specifiers.
// Here we check, against the spec, if payload is a promise capability, meaning that this was called for a dynamic import
if (payload.has<NonnullGCPtr<PromiseCapability>>() && !m_dynamic_imports_allowed) {
// If you are here because you want to enable dynamic module importing make sure it won't be a security problem
// by checking the default implementation of HostImportModuleDynamically and creating your own hook or calling
// vm.allow_dynamic_imports().
finish_loading_imported_module(referrer, module_request, payload, throw_completion<InternalError>(ErrorType::DynamicImportNotAllowed, module_request.module_specifier));
return;
}
DeprecatedString module_type;
for (auto& attribute : module_request.attributes) {
@ -894,13 +864,18 @@ ThrowCompletionOr<NonnullGCPtr<Module>> VM::resolve_imported_module(ImportedModu
StringView const base_filename = referrer.visit(
[&](NonnullGCPtr<Realm> const&) {
return "."sv;
// Generally within ECMA262 we always get a referencing_script_or_module. However, ShadowRealm gives an explicit null.
// To get around this is we attempt to get the active script_or_module otherwise we might start loading "random" files from the working directory.
return get_active_script_or_module().visit(
[](Empty) {
return "."sv;
},
[](auto const& script_or_module) {
return script_or_module->filename();
});
},
[&](NonnullGCPtr<Script> const& script) {
return script->filename();
},
[&](NonnullGCPtr<Module> const& module) {
return module->filename();
[&](auto const& script_or_module) {
return script_or_module->filename();
});
LexicalPath base_path { base_filename };
@ -925,14 +900,15 @@ ThrowCompletionOr<NonnullGCPtr<Module>> VM::resolve_imported_module(ImportedModu
return DeprecatedString::formatted("Module @ {}", script_or_module.ptr());
});
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] resolve_imported_module({}, {})", referencing_module_string, filename);
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] load_imported_module({}, {})", referencing_module_string, filename);
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] resolved {} + {} -> {}", base_path, module_request.module_specifier, filename);
#endif
auto* loaded_module_or_end = get_stored_module(referrer, filename, module_type);
if (loaded_module_or_end != nullptr) {
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] resolve_imported_module({}) already loaded at {}", filename, loaded_module_or_end->module.ptr());
return NonnullGCPtr(*loaded_module_or_end->module);
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] load_imported_module({}) already loaded at {}", filename, loaded_module_or_end->module.ptr());
finish_loading_imported_module(referrer, module_request, payload, *loaded_module_or_end->module);
return;
}
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] reading and parsing module {}", filename);
@ -940,21 +916,25 @@ ThrowCompletionOr<NonnullGCPtr<Module>> VM::resolve_imported_module(ImportedModu
auto file_or_error = Core::File::open(filename, Core::File::OpenMode::Read);
if (file_or_error.is_error()) {
return throw_completion<SyntaxError>(ErrorType::ModuleNotFound, module_request.module_specifier);
finish_loading_imported_module(referrer, module_request, payload, throw_completion<SyntaxError>(ErrorType::ModuleNotFound, module_request.module_specifier));
return;
}
// FIXME: Don't read the file in one go.
auto file_content_or_error = file_or_error.value()->read_until_eof();
if (file_content_or_error.is_error()) {
if (file_content_or_error.error().code() == ENOMEM)
return throw_completion<JS::InternalError>(error_message(::JS::VM::ErrorMessage::OutOfMemory));
return throw_completion<SyntaxError>(ErrorType::ModuleNotFound, module_request.module_specifier);
if (file_content_or_error.error().code() == ENOMEM) {
finish_loading_imported_module(referrer, module_request, payload, throw_completion<JS::InternalError>(error_message(::JS::VM::ErrorMessage::OutOfMemory)));
return;
}
finish_loading_imported_module(referrer, module_request, payload, throw_completion<SyntaxError>(ErrorType::ModuleNotFound, module_request.module_specifier));
return;
}
StringView const content_view { file_content_or_error.value().bytes() };
auto module = TRY([&]() -> ThrowCompletionOr<NonnullGCPtr<Module>> {
auto module = [&]() -> ThrowCompletionOr<NonnullGCPtr<Module>> {
// If assertions has an entry entry such that entry.[[Key]] is "type", let type be entry.[[Value]]. The following requirements apply:
// If type is "json", then this algorithm must either invoke ParseJSONModule and return the resulting Completion Record, or throw an exception.
if (module_type == "json"sv) {
@ -971,124 +951,11 @@ ThrowCompletionOr<NonnullGCPtr<Module>> VM::resolve_imported_module(ImportedModu
return throw_completion<SyntaxError>(module_or_errors.error().first().to_deprecated_string());
}
return module_or_errors.release_value();
}());
}();
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] resolve_imported_module(...) parsed {} to {}", filename, module.ptr());
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] load_imported_module(...) parsed {} to {}", filename, module);
// We have to set it here already in case it references itself.
m_loaded_modules.empend(
referrer,
filename,
module_type,
*module,
false);
return module;
}
// 16.2.1.8 HostImportModuleDynamically ( referencingScriptOrModule, specifier, promiseCapability ), https://tc39.es/ecma262/#sec-hostimportmoduledynamically
ThrowCompletionOr<void> VM::import_module_dynamically(ImportedModuleReferrer referrer, ModuleRequest module_request, PromiseCapability const& promise_capability)
{
auto& realm = *current_realm();
// Success path:
// - At some future time, the host environment must perform FinishDynamicImport(referencingScriptOrModule, moduleRequest, promiseCapability, promise),
// where promise is a Promise resolved with undefined.
// - Any subsequent call to HostResolveImportedModule after FinishDynamicImport has completed,
// given the arguments referencingScriptOrModule and specifier, must return a normal completion
// containing a module which has already been evaluated, i.e. whose Evaluate concrete method has
// already been called and returned a normal completion.
// Failure path:
// - At some future time, the host environment must perform
// FinishDynamicImport(referencingScriptOrModule, moduleRequest, promiseCapability, promise),
// where promise is a Promise rejected with an error representing the cause of failure.
auto promise = Promise::create(realm);
ScopeGuard finish_dynamic_import = [&] {
host_finish_dynamic_import(referrer, module_request, promise_capability, promise);
};
// Note: If host_resolve_imported_module returns a module it has been loaded successfully and the next call in finish_dynamic_import will retrieve it again.
auto module_or_error = host_resolve_imported_module(referrer, module_request);
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] HostImportModuleDynamically(..., {}) -> {}", module_request.module_specifier, module_or_error.is_error() ? "failed" : "passed");
if (module_or_error.is_throw_completion()) {
promise->reject(*module_or_error.throw_completion().value());
} else {
auto module = module_or_error.release_value();
auto& source_text_module = static_cast<Module&>(*module);
auto evaluated_or_error = link_and_eval_module(source_text_module);
if (evaluated_or_error.is_throw_completion()) {
promise->reject(*evaluated_or_error.throw_completion().value());
} else {
promise->fulfill(js_undefined());
}
}
// It must return unused.
// Note: Just return void always since the resulting value cannot be accessed by user code.
return {};
}
// 16.2.1.9 FinishDynamicImport ( referencingScriptOrModule, specifier, promiseCapability, innerPromise ), https://tc39.es/ecma262/#sec-finishdynamicimport
void VM::finish_dynamic_import(ImportedModuleReferrer referrer, ModuleRequest module_request, PromiseCapability const& promise_capability, Promise* inner_promise)
{
dbgln_if(JS_MODULE_DEBUG, "[JS MODULE] finish_dynamic_import on {}", module_request.module_specifier);
auto& realm = *current_realm();
// 1. Let fulfilledClosure be a new Abstract Closure with parameters (result) that captures referencingScriptOrModule, specifier, and promiseCapability and performs the following steps when called:
auto fulfilled_closure = [referrer = move(referrer), module_request = move(module_request), &promise_capability](VM& vm) -> ThrowCompletionOr<Value> {
auto result = vm.argument(0);
// a. Assert: result is undefined.
VERIFY(result.is_undefined());
// b. Let moduleRecord be ! HostResolveImportedModule(referencingScriptOrModule, specifier).
auto module_record = MUST(vm.host_resolve_imported_module(referrer, module_request));
// c. Assert: Evaluate has already been invoked on moduleRecord and successfully completed.
// Note: If HostResolveImportedModule returns a module evaluate will have been called on it.
// d. Let namespace be Completion(GetModuleNamespace(moduleRecord)).
auto namespace_ = module_record->get_module_namespace(vm);
// e. If namespace is an abrupt completion, then
if (namespace_.is_throw_completion()) {
// i. Perform ! Call(promiseCapability.[[Reject]], undefined, « namespace.[[Value]] »).
MUST(call(vm, *promise_capability.reject(), js_undefined(), *namespace_.throw_completion().value()));
}
// f. Else,
else {
// i. Perform ! Call(promiseCapability.[[Resolve]], undefined, « namespace.[[Value]] »).
MUST(call(vm, *promise_capability.resolve(), js_undefined(), namespace_.release_value()));
}
// g. Return unused.
// NOTE: We don't support returning an empty/optional/unused value here.
return js_undefined();
};
// 2. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 0, "", « »).
auto on_fulfilled = NativeFunction::create(realm, move(fulfilled_closure), 0, "");
// 3. Let rejectedClosure be a new Abstract Closure with parameters (error) that captures promiseCapability and performs the following steps when called:
auto rejected_closure = [&promise_capability](VM& vm) -> ThrowCompletionOr<Value> {
auto error = vm.argument(0);
// a. Perform ! Call(promiseCapability.[[Reject]], undefined, « error »).
MUST(call(vm, *promise_capability.reject(), js_undefined(), error));
// b. Return unused.
// NOTE: We don't support returning an empty/optional/unused value here.
return js_undefined();
};
// 4. Let onRejected be CreateBuiltinFunction(rejectedClosure, 0, "", « »).
auto on_rejected = NativeFunction::create(realm, move(rejected_closure), 0, "");
// 5. Perform PerformPromiseThen(innerPromise, onFulfilled, onRejected).
inner_promise->perform_then(on_fulfilled, on_rejected, {});
// 6. Return unused.
finish_loading_imported_module(referrer, module_request, payload, module);
}
void VM::push_execution_context(ExecutionContext& context)

View File

@ -229,18 +229,14 @@ public:
// Our implementation of this proposal is outdated however, as such we try to adapt the proposal and living standard
// to match our implementation for now.
// 16.2.1.8 HostLoadImportedModule ( referrer, moduleRequest, hostDefined, payload ), https://tc39.es/proposal-import-attributes/#sec-HostLoadImportedModule
Function<void(Realm&, Variant<NonnullGCPtr<Script>, NonnullGCPtr<CyclicModule>>, ModuleRequest const&, GCPtr<GraphLoadingState::HostDefined>, GraphLoadingState&)> host_load_imported_module;
Function<ThrowCompletionOr<NonnullGCPtr<Module>>(ImportedModuleReferrer, ModuleRequest const&)> host_resolve_imported_module;
Function<ThrowCompletionOr<void>(ImportedModuleReferrer, ModuleRequest, PromiseCapability const&)> host_import_module_dynamically;
Function<void(ImportedModuleReferrer, ModuleRequest const&, PromiseCapability const&, Promise*)> host_finish_dynamic_import;
Function<void(ImportedModuleReferrer, ModuleRequest const&, GCPtr<GraphLoadingState::HostDefined>, ImportedModulePayload)> host_load_imported_module;
Function<HashMap<PropertyKey, Value>(SourceTextModule&)> host_get_import_meta_properties;
Function<void(Object*, SourceTextModule const&)> host_finalize_import_meta;
Function<Vector<DeprecatedString>()> host_get_supported_import_attributes;
void enable_default_host_import_module_dynamically_hook();
void set_dynamic_imports_allowed(bool value) { m_dynamic_imports_allowed = value; }
Function<void(Promise&, Promise::RejectionOperation)> host_promise_rejection_tracker;
Function<ThrowCompletionOr<Value>(JobCallback&, Value, ReadonlySpan<Value>)> host_call_job_callback;
@ -271,12 +267,9 @@ private:
ThrowCompletionOr<void> property_binding_initialization(BindingPattern const& binding, Value value, Environment* environment);
ThrowCompletionOr<void> iterator_binding_initialization(BindingPattern const& binding, IteratorRecord& iterator_record, Environment* environment);
ThrowCompletionOr<NonnullGCPtr<Module>> resolve_imported_module(ImportedModuleReferrer, ModuleRequest const& module_request);
void load_imported_module(ImportedModuleReferrer, ModuleRequest const&, GCPtr<GraphLoadingState::HostDefined>, ImportedModulePayload);
ThrowCompletionOr<void> link_and_eval_module(Module& module);
ThrowCompletionOr<void> import_module_dynamically(ImportedModuleReferrer, ModuleRequest module_request, PromiseCapability const& promise_capability);
void finish_dynamic_import(ImportedModuleReferrer, ModuleRequest module_request, PromiseCapability const& promise_capability, Promise* inner_promise);
void set_well_known_symbols(WellKnownSymbols well_known_symbols) { m_well_known_symbols = move(well_known_symbols); }
Vector<FlatPtr> get_native_stack_trace() const;
@ -322,6 +315,8 @@ private:
OwnPtr<CustomData> m_custom_data;
OwnPtr<Bytecode::Interpreter> m_bytecode_interpreter;
bool m_dynamic_imports_allowed { false };
};
template<typename GlobalObjectType, typename... Args>

View File

@ -194,7 +194,7 @@ int main(int argc, char** argv)
if (!g_vm) {
g_vm = MUST(JS::VM::create());
g_vm->enable_default_host_import_module_dynamically_hook();
g_vm->set_dynamic_imports_allowed(true);
}
Test::JS::TestRunner test_runner(test_root, common_path, print_times, print_progress, print_json, per_file);

View File

@ -404,7 +404,10 @@ ErrorOr<void> initialize_main_thread_vm()
};
// 8.1.6.5.3 HostLoadImportedModule(referrer, moduleRequest, loadState, payload), https://html.spec.whatwg.org/multipage/webappapis.html#hostloadimportedmodule
s_main_thread_vm->host_load_imported_module = [](JS::Realm& realm, Variant<JS::NonnullGCPtr<JS::Script>, JS::NonnullGCPtr<JS::CyclicModule>> referrer, JS::ModuleRequest const& module_request, JS::GCPtr<JS::GraphLoadingState::HostDefined> load_state, JS::NonnullGCPtr<JS::GraphLoadingState> payload) -> void {
s_main_thread_vm->host_load_imported_module = [](JS::ImportedModuleReferrer referrer, JS::ModuleRequest const& module_request, JS::GCPtr<JS::GraphLoadingState::HostDefined> load_state, JS::ImportedModulePayload payload) -> void {
auto& vm = *s_main_thread_vm;
auto& realm = *vm.current_realm();
// 1. Let settingsObject be the current settings object.
Optional<HTML::EnvironmentSettingsObject&> settings_object = HTML::current_settings_object();
@ -447,7 +450,7 @@ ErrorOr<void> initialize_main_thread_vm()
auto completion = dom_exception_to_throw_completion(main_thread_vm(), url.exception());
// 2. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, completion).
JS::finish_loading_imported_module(realm, referrer, module_request, payload, completion);
JS::finish_loading_imported_module(referrer, module_request, payload, completion);
// 3. Return.
return;
@ -475,37 +478,37 @@ ErrorOr<void> initialize_main_thread_vm()
// 1. Let completion be null.
// NOTE: Our JS::Completion does not support non JS::Value types for its [[Value]], a such we
// use JS::ThrowCompletionOr here.
auto completion = JS::ThrowCompletionOr<JS::Module*> { {} };
auto completion = [&]() -> JS::ThrowCompletionOr<JS::NonnullGCPtr<JS::Module>> {
// 2. If moduleScript is null, then set completion to Completion Record { [[Type]]: throw, [[Value]]: a new TypeError, [[Target]]: empty }.
if (!module_script) {
return JS::throw_completion(JS::TypeError::create(realm, DeprecatedString::formatted("Loading imported module '{}' failed.", module_request.module_specifier)));
}
// 3. Otherwise, if moduleScript's parse error is not null, then:
else if (!module_script->parse_error().is_empty()) {
// 1. Let parseError be moduleScript's parse error.
auto parse_error = module_script->parse_error();
// 2. If moduleScript is null, then set completion to Completion Record { [[Type]]: throw, [[Value]]: a new TypeError, [[Target]]: empty }.
if (!module_script) {
completion = JS::throw_completion(JS::TypeError::create(realm, DeprecatedString::formatted("Loading imported module '{}' failed.", module_request.module_specifier)));
}
// 3. Otherwise, if moduleScript's parse error is not null, then:
else if (!module_script->parse_error().is_empty()) {
// 1. Let parseError be moduleScript's parse error.
auto parse_error = module_script->parse_error();
// 2. Set completion to Completion Record { [[Type]]: throw, [[Value]]: parseError, [[Target]]: empty }.
return JS::throw_completion(parse_error);
// 2. Set completion to Completion Record { [[Type]]: throw, [[Value]]: parseError, [[Target]]: empty }.
completion = JS::throw_completion(parse_error);
// 3. If loadState is not undefined and loadState.[[ParseError]] is null, set loadState.[[ParseError]] to parseError.
if (load_state) {
auto& load_state_as_fetch_context = static_cast<HTML::FetchContext&>(*load_state);
if (load_state_as_fetch_context.parse_error.is_null()) {
load_state_as_fetch_context.parse_error = parse_error;
// 3. If loadState is not undefined and loadState.[[ParseError]] is null, set loadState.[[ParseError]] to parseError.
if (load_state) {
auto& load_state_as_fetch_context = static_cast<HTML::FetchContext&>(*load_state);
if (load_state_as_fetch_context.parse_error.is_null()) {
load_state_as_fetch_context.parse_error = parse_error;
}
}
}
}
// 4. Otherwise, set completion to Completion Record { [[Type]]: normal, [[Value]]: result's record, [[Target]]: empty }.
else {
auto* record = static_cast<HTML::JavaScriptModuleScript&>(*module_script).record();
// 4. Otherwise, set completion to Completion Record { [[Type]]: normal, [[Value]]: result's record, [[Target]]: empty }.
else {
auto* record = static_cast<HTML::JavaScriptModuleScript&>(*module_script).record();
completion = JS::ThrowCompletionOr<JS::Module*>(record);
}
return JS::ThrowCompletionOr<JS::NonnullGCPtr<JS::Module>>(*record);
}
}();
// 5. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, completion).
JS::finish_loading_imported_module(realm, referrer, module_request, payload, completion);
JS::finish_loading_imported_module(referrer, module_request, payload, completion);
});
// 13. Fetch a single imported module script given url, fetchClient, destination, fetchOptions, settingsObject, fetchReferrer,
@ -514,51 +517,6 @@ ErrorOr<void> initialize_main_thread_vm()
HTML::fetch_single_imported_module_script(realm, url.release_value(), *fetch_client, destination, fetch_options, *settings_object, fetch_referrer, module_request, on_single_fetch_complete);
};
// 8.1.6.5.3 HostResolveImportedModule(referencingScriptOrModule, moduleRequest), https://html.spec.whatwg.org/multipage/webappapis.html#hostresolveimportedmodule(referencingscriptormodule,-modulerequest)
s_main_thread_vm->host_resolve_imported_module = [](JS::ImportedModuleReferrer referrer, JS::ModuleRequest const& module_request) -> JS::ThrowCompletionOr<JS::NonnullGCPtr<JS::Module>> {
// 1. Let moduleMap and referencingScript be null.
Optional<HTML::ModuleMap&> module_map;
Optional<HTML::Script&> referencing_script;
if (referrer.has<JS::NonnullGCPtr<JS::Script>>() || referrer.has<JS::NonnullGCPtr<JS::CyclicModule>>()) {
// 1. Set referencingScript to referencingScriptOrModule.[[HostDefined]].
referencing_script = verify_cast<HTML::Script>(referrer.has<JS::NonnullGCPtr<JS::Script>>() ? *referrer.get<JS::NonnullGCPtr<JS::Script>>()->host_defined() : *referrer.get<JS::NonnullGCPtr<JS::CyclicModule>>()->host_defined());
// 2. Set moduleMap to referencingScript's settings object's module map.
module_map = referencing_script->settings_object().module_map();
}
// 3. Otherwise:
else {
// 1. Assert: there is a current settings object.
// NOTE: This is handled by the HTML::current_settings_object() accessor.
// 2. Set moduleMap to the current settings object's module map.
module_map = HTML::current_settings_object().module_map();
}
// 4. Let url be the result of resolving a module specifier given referencingScript and moduleRequest.[[Specifier]].
auto url = MUST(HTML::resolve_module_specifier(referencing_script, module_request.module_specifier));
// 5. Assert: the previous step never throws an exception, because resolving a module specifier must have been previously successful
// with these same two arguments (either while creating the corresponding module script, or in fetch an import() module script graph).
// NOTE: Handled by MUST above.
// 6. Let moduleType be the result of running the module type from module request steps given moduleRequest.
auto module_type = HTML::module_type_from_module_request(module_request);
// 7. Let resolvedModuleScript be moduleMap[(url, moduleType)]. (This entry must exist for us to have gotten to this point.)
auto resolved_module_script = module_map->get(url, module_type).value();
// 8. Assert: resolvedModuleScript is a module script (i.e., is not null or "fetching").
VERIFY(resolved_module_script.type == HTML::ModuleMap::EntryType::ModuleScript);
// 9. Assert: resolvedModuleScript's record is not null.
VERIFY(resolved_module_script.module_script->record());
// 10. Return resolvedModuleScript's record.
return JS::NonnullGCPtr(*resolved_module_script.module_script->record());
};
return {};
}

View File

@ -775,7 +775,7 @@ void fetch_descendants_of_and_link_a_module_script(JS::Realm& realm,
fetch_client.prepare_to_run_callback();
// 5. Let loadingPromise be record.LoadRequestedModules(state).
auto& loading_promise = record->load_requested_modules(realm, state);
auto& loading_promise = record->load_requested_modules(state);
// 6. Upon fulfillment of loadingPromise, run the following steps:
WebIDL::upon_fulfillment(loading_promise, [&realm, record, &module_script, on_complete](auto const&) -> WebIDL::ExceptionOr<JS::Value> {

View File

@ -562,7 +562,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
s_history_path = TRY(String::formatted("{}/.js-history", Core::StandardPaths::home_directory()));
g_vm = TRY(JS::VM::create());
g_vm->enable_default_host_import_module_dynamically_hook();
g_vm->set_dynamic_imports_allowed(true);
if (!disable_debug_printing) {
// NOTE: These will print out both warnings when using something like Promise.reject().catch(...) -