diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js index 3e3473b000c5..8c977b541e00 100644 --- a/lib/internal/bootstrap/realm.js +++ b/lib/internal/bootstrap/realm.js @@ -183,7 +183,10 @@ const { setInternalLoaders, } = internalBinding('builtins'); -const { ModuleWrap } = internalBinding('module_wrap'); +const { + ModuleWrap, + kEvaluationPhase, +} = internalBinding('module_wrap'); ObjectSetPrototypeOf(ModuleWrap.prototype, null); const getOwn = (target, property, receiver) => { @@ -402,7 +405,7 @@ class BuiltinModule { }); // Ensure immediate sync execution to capture exports now this.module.instantiate(); - this.module.evaluate(-1, false); + this.module.evaluate(-1, false, kEvaluationPhase); return this.module; } diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 489d8ff91e35..f3b8d8b1b396 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -692,7 +692,7 @@ class ModuleLoader { return module.getModuleSourceObject(); } const { module } = await moduleJob.run(isEntryPoint); - return module.getNamespace(); + return module.getNamespace(phase); }, { __proto__: null, parentURL, diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index c8506b42ff3f..567d3c30880f 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -329,7 +329,7 @@ class ModuleJobBase { debug(`ModuleJobBase.syncLink() ${this.url} -> ${request.specifier}`, job); assert(!isPromise(job)); assert(job.module instanceof ModuleWrap); - if (this.shouldRunModule(request.phase)) { + if (this.shouldEvaluateModule(request.phase)) { ArrayPrototypePush(evaluationDepJobs, job); } modules[idx] = job.module; @@ -383,7 +383,7 @@ class ModuleJobBase { shouldLinkModule(phase) { return phase >= kDeferPhase; } - shouldRunModule(phase) { + shouldEvaluateModule(phase) { return phase === kEvaluationPhase; } } @@ -466,7 +466,7 @@ class ModuleJob extends ModuleJobBase { const dependencyJobPromise = this.loader.getOrCreateModuleJob(this.url, request, requestType); const modulePromise = PromisePrototypeThen(dependencyJobPromise, (job) => { debug(`ModuleJob.asyncLink() ${this.url} -> ${request.specifier}`, job); - if (this.shouldRunModule(request.phase)) { + if (this.shouldEvaluateModule(request.phase)) { ArrayPrototypePush(evaluationDepJobs, job); } return job.modulePromise; @@ -521,7 +521,7 @@ class ModuleJob extends ModuleJobBase { } runSync(parent) { - assert(this.shouldRunModule(this.phase)); + assert(this.shouldEvaluateModule(this.phase)); assert(this.module instanceof ModuleWrap); let status = this.module.getStatus(); @@ -561,17 +561,17 @@ class ModuleJob extends ModuleJobBase { } async run(isEntryPoint = false) { - debug('ModuleJob.run()', this.module); - assert(this.shouldRunModule(this.phase)); + debug('ModuleJob.run(); phase: ', this.phase, this.module); await this.#instantiate(); if (isEntryPoint) { globalThis[entry_point_module_private_symbol] = this.module; } + const timeout = -1; const breakOnSigint = false; setHasStartedUserESMExecution(); try { - await this.module.evaluate(timeout, breakOnSigint); + await this.module.evaluate(timeout, breakOnSigint, this.phase); } catch (e) { explainCommonJSGlobalLikeNotDefinedError(e, this.module.url, this.module.hasTopLevelAwait); throw e; @@ -641,7 +641,8 @@ class ModuleJobSync extends ModuleJobBase { } async run(isEntryPoint = false) { - assert(this.shouldRunModule(this.phase)); + assert(this.shouldEvaluateModule(this.phase)); + // This path is hit by a require'd module that is imported again. const status = this.module.getStatus(); debug('ModuleJobSync.run()', status, this.module); // If the module was previously required and errored, reject from import() again. @@ -676,7 +677,7 @@ class ModuleJobSync extends ModuleJobBase { const { callAndPauseOnStart } = internalBinding('inspector'); this.evaluationPromise = callAndPauseOnStart(this.module.evaluate, this.module, timeout, breakOnSigint); } else { - this.evaluationPromise = this.module.evaluate(timeout, breakOnSigint); + this.evaluationPromise = this.module.evaluate(timeout, breakOnSigint, this.phase); } await this.evaluationPromise; this.evaluationPromise = undefined; @@ -704,7 +705,7 @@ class ModuleJobSync extends ModuleJobBase { runSync(parent) { debug('ModuleJobSync.runSync()', this.module); - assert(this.shouldRunModule(this.phase)); + assert(this.shouldEvaluateModule(this.phase)); // TODO(joyeecheung): Reject graphs with top-level await _before_ instantiation, so that the // async graph error supersedes instantiation (mismatch export) errors in the graph. this.instantiateSync(); diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 9e27a2db1ebc..d4d3baf9d986 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -25,6 +25,7 @@ const { const { ModuleWrap, + kEvaluationPhase, setImportModuleDynamicallyCallback, setInitializeImportMetaObjectCallback, } = internalBinding('module_wrap'); @@ -383,7 +384,7 @@ function compileSourceTextModule(url, source, type, context = kEmptyObject) { const modules = requests.map(({ specifier }) => getBuiltinModuleWrapForEmbedder(specifier)); wrap.link(modules); wrap.instantiate(); - wrap.evaluate(-1, false); + wrap.evaluate(-1, false, kEvaluationPhase); } return wrap; } diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 9892f38d96bd..6505d11c219d 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -564,6 +564,17 @@ ModulePhase to_phase_constant(ModuleImportPhase phase) { } } +ModuleImportPhase to_module_import_phase(ModulePhase phase) { + switch (phase) { + case kEvaluationPhase: + return ModuleImportPhase::kEvaluation; + case kDeferPhase: + return ModuleImportPhase::kDefer; + default: + UNREACHABLE(); + } +} + static Local createImportAttributesContainer( Realm* realm, Isolate* isolate, @@ -774,8 +785,8 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo& args) { if (contextify_context != nullptr) microtask_queue = contextify_context->microtask_queue(); - // module.evaluate(timeout, breakOnSigint) - CHECK_EQ(args.Length(), 2); + // module.evaluate(timeout, breakOnSigint, phase) + CHECK_EQ(args.Length(), 3); CHECK(args[0]->IsNumber()); int64_t timeout; @@ -786,6 +797,9 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo& args) { CHECK(args[1]->IsBoolean()); bool break_on_sigint = args[1]->IsTrue(); + CHECK(args[2]->IsNumber()); + ModulePhase phase = static_cast(args[2].As()->Value()); + ShouldNotAbortOnUncaughtScope no_abort_scope(realm->env()); TryCatchScope try_catch(realm->env()); @@ -800,7 +814,13 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo& args) { isolate, &received_signal) : std::nullopt; - result = module->Evaluate(context); + CHECK_NE(phase, kSourcePhase); + if (phase == kDeferPhase) { + result = module->EvaluateForImportDefer(context); + } else { + CHECK_EQ(phase, kEvaluationPhase); + result = module->Evaluate(context); + } Local res; if (result.ToLocal(&res) && microtask_queue) { @@ -954,12 +974,19 @@ void ModuleWrap::GetNamespace(const FunctionCallbackInfo& args) { ModuleWrap* obj; ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); + ModulePhase phase = kEvaluationPhase; + if (args.Length() > 0) { + CHECK(args[0]->IsInt32()); + phase = static_cast(args[0].As()->Value()); + } + Local module = obj->module_.Get(isolate); if (module->GetStatus() < Module::kInstantiated) { return THROW_ERR_MODULE_NOT_INSTANTIATED(realm->env()); } - Local result = module->GetModuleNamespace(); + Local result = + module->GetModuleNamespace(to_module_import_phase(phase)); args.GetReturnValue().Set(result); } diff --git a/test/es-module/test-defer-dynamic-import.mjs b/test/es-module/test-defer-dynamic-import.mjs new file mode 100644 index 000000000000..06d54179196d --- /dev/null +++ b/test/es-module/test-defer-dynamic-import.mjs @@ -0,0 +1,22 @@ +// Flags: --js-defer-import-eval + +// Tests that defer import of a dynamically imported module +// defers the execution of the module until any of its properties +// is accessed for the first time. + +import * as common from '../common/index.mjs'; +import * as assert from 'assert'; + +globalThis.eval_list ||= []; + +console.log('main module loading...'); + +await import.defer('../fixtures/es-modules/module-deferred-eval.mjs').then((module) => { + // Check that the module has been evaluated exactly once. + assert.strictEqual(globalThis.eval_list.length, 1); + assert.strictEqual(module.foo, 42); + assert.partialDeepStrictEqual(['defer-1'], globalThis.eval_list); + + // Clean-up + delete globalThis.eval_list; +}).then(common.mustCall()); diff --git a/test/parallel/test-internal-module-wrap.js b/test/parallel/test-internal-module-wrap.js index 2b7bcf009d14..424d8ee87d5b 100644 --- a/test/parallel/test-internal-module-wrap.js +++ b/test/parallel/test-internal-module-wrap.js @@ -4,7 +4,10 @@ const common = require('../common'); const assert = require('assert'); const { internalBinding } = require('internal/test/binding'); -const { ModuleWrap } = internalBinding('module_wrap'); +const { + ModuleWrap, + kEvaluationPhase, +} = internalBinding('module_wrap'); async function testModuleWrap() { const unlinked = new ModuleWrap('unlinked', undefined, 'export * from "bar";', 0, 0); @@ -43,7 +46,7 @@ async function testModuleWrap() { assert.strictEqual(bar.hasAsyncGraph, false); assert.strictEqual(foo.hasAsyncGraph, false); - assert.strictEqual(await foo.evaluate(-1, false), undefined); + assert.strictEqual(await foo.evaluate(-1, false, kEvaluationPhase), undefined); assert.strictEqual(foo.getNamespace().five, 5); // Check that the module requests are the same after linking, instantiate, and evaluation. @@ -68,7 +71,7 @@ async function testAsyncGraph() { assert.strictEqual(bar.hasAsyncGraph, true); assert.strictEqual(foo.hasAsyncGraph, true); - const evalPromise = foo.evaluate(-1, false); + const evalPromise = foo.evaluate(-1, false, kEvaluationPhase); assert.throws(() => foo.getNamespace().five, ReferenceError); assert.strictEqual(await evalPromise, undefined); assert.strictEqual(foo.getNamespace().five, 5);