LibJS: Cache entry modules before loading imports

Host-loaded modules are inserted into the VM module registry before
their dependencies are walked, but entry modules passed directly to
`VM::run()` were not. That made an entry module self-import parse a
second Module Record and observe the second copy after evaluation.

Register named entry modules in the same registry before dependency
loading runs, so self-imports and namespace access go through the
original Module Record. Add a test-js helper for running modules
directly and cover the self-import pre-evaluation binding state.
This commit is contained in:
Andreas Kling 2026-05-21 15:25:45 +02:00 committed by Andreas Kling
parent 410f0fdbb0
commit bdecc7f558
4 changed files with 54 additions and 0 deletions

View file

@ -627,6 +627,8 @@ VM::StoredModule* VM::get_stored_module(ImportedModuleReferrer const&, ByteStrin
return &(*end_or_module);
}
static ByteString resolve_module_filename(StringView filename, Utf16View const& module_type);
ThrowCompletionOr<void> VM::link_and_eval_module(SourceTextModule& module)
{
return link_and_eval_module(static_cast<CyclicModule&>(module));
@ -635,6 +637,19 @@ ThrowCompletionOr<void> VM::link_and_eval_module(SourceTextModule& module)
ThrowCompletionOr<void> VM::link_and_eval_module(CyclicModule& module)
{
auto filename = module.filename();
if (!filename.is_empty()) {
auto absolute_filename = resolve_module_filename(LexicalPath::absolute_path("."sv, filename), {});
if (!get_stored_module(GC::Ref { module }, absolute_filename, {})) {
// Register the entry module before loading dependencies so self-imports resolve to this Module Record.
m_loaded_modules.empend(
GC::Ref { module },
move(absolute_filename),
String {},
make_root(static_cast<Module&>(module)),
true);
}
}
auto& promise_capability = module.load_requested_modules(nullptr);
if (auto const& promise = as<Promise>(*promise_capability.promise()); promise.state() == Promise::State::Rejected)

View file

@ -259,6 +259,10 @@ describe("loops", () => {
expectModulePassed("./loop-self.mjs");
});
test("entry module can import itself before evaluation", () => {
evaluateModule("./modules/entry-self-import.mjs");
});
test("import something which imports a cycle", () => {
expectModulePassed("./loop-entry.mjs");
});

View file

@ -0,0 +1,23 @@
if (importedVarValue !== undefined) {
throw new Error(`Expected importedVarValue to be undefined before evaluation, got ${importedVarValue}`);
}
try {
importedVarValue = 0;
throw new Error("Expected assignment to importedVarValue to throw");
} catch (error) {
if (!(error instanceof TypeError)) throw error;
}
try {
namespace.localValue;
throw new Error("Expected namespace.localValue to be in the TDZ before evaluation");
} catch (error) {
if (!(error instanceof ReferenceError)) throw error;
}
import { varValue as importedVarValue } from "./entry-self-import.mjs";
import * as namespace from "./entry-self-import.mjs";
export var varValue = 1;
export let localValue = 2;

View file

@ -47,6 +47,18 @@ TESTJS_GLOBAL_FUNCTION(evaluate_source, evaluateSource)
return vm.run(script.value());
}
TESTJS_GLOBAL_FUNCTION(evaluate_module, evaluateModule)
{
auto& realm = *vm.current_realm();
auto path = TRY(vm.argument(0).to_string(vm));
auto module = Test::JS::parse_module(path.to_byte_string(), realm);
if (module.is_error())
return vm.throw_completion<JS::SyntaxError>(module.error().error.to_string());
return vm.run(module.value());
}
TESTJS_GLOBAL_FUNCTION(run_queued_promise_jobs, runQueuedPromiseJobs)
{
vm.run_queued_promise_jobs();