Skip to content

Commit 503ed1f

Browse files
committed
modules: implement deferred import for dynamic modules
This PR adds support for deferring the import of modules in additional cases, such as importing a dynamic module. It also adds a test for this case. Refs: https://github.com/tc39/proposal-defer-import-eval Signed-off-by: Maya Lekova maya@igalia.com
1 parent 9abe615 commit 503ed1f

8 files changed

Lines changed: 81 additions & 22 deletions

File tree

‎lib/internal/bootstrap/realm.js‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,10 @@ const {
183183
setInternalLoaders,
184184
} = internalBinding('builtins');
185185

186-
const { ModuleWrap } = internalBinding('module_wrap');
186+
const {
187+
ModuleWrap,
188+
kEvaluationPhase,
189+
} = internalBinding('module_wrap');
187190
ObjectSetPrototypeOf(ModuleWrap.prototype, null);
188191

189192
const getOwn = (target, property, receiver) => {
@@ -402,7 +405,7 @@ class BuiltinModule {
402405
});
403406
// Ensure immediate sync execution to capture exports now
404407
this.module.instantiate();
405-
this.module.evaluate(-1, false);
408+
this.module.evaluate(-1, false, kEvaluationPhase);
406409
return this.module;
407410
}
408411

‎lib/internal/modules/esm/loader.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ class ModuleLoader {
692692
return module.getModuleSourceObject();
693693
}
694694
const { module } = await moduleJob.run(isEntryPoint);
695-
return module.getNamespace();
695+
return module.getNamespace(phase);
696696
}, {
697697
__proto__: null,
698698
parentURL,

‎lib/internal/modules/esm/module_job.js‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ class ModuleJobBase {
329329
debug(`ModuleJobBase.syncLink() ${this.url} -> ${request.specifier}`, job);
330330
assert(!isPromise(job));
331331
assert(job.module instanceof ModuleWrap);
332-
if (this.shouldRunModule(request.phase)) {
332+
if (this.shouldEvaluateModule(request.phase)) {
333333
ArrayPrototypePush(evaluationDepJobs, job);
334334
}
335335
modules[idx] = job.module;
@@ -383,7 +383,7 @@ class ModuleJobBase {
383383
shouldLinkModule(phase) {
384384
return phase >= kDeferPhase;
385385
}
386-
shouldRunModule(phase) {
386+
shouldEvaluateModule(phase) {
387387
return phase === kEvaluationPhase;
388388
}
389389
}
@@ -466,7 +466,7 @@ class ModuleJob extends ModuleJobBase {
466466
const dependencyJobPromise = this.loader.getOrCreateModuleJob(this.url, request, requestType);
467467
const modulePromise = PromisePrototypeThen(dependencyJobPromise, (job) => {
468468
debug(`ModuleJob.asyncLink() ${this.url} -> ${request.specifier}`, job);
469-
if (this.shouldRunModule(request.phase)) {
469+
if (this.shouldEvaluateModule(request.phase)) {
470470
ArrayPrototypePush(evaluationDepJobs, job);
471471
}
472472
return job.modulePromise;
@@ -521,7 +521,7 @@ class ModuleJob extends ModuleJobBase {
521521
}
522522

523523
runSync(parent) {
524-
assert(this.shouldRunModule(this.phase));
524+
assert(this.shouldEvaluateModule(this.phase));
525525
assert(this.module instanceof ModuleWrap);
526526
let status = this.module.getStatus();
527527

@@ -561,17 +561,17 @@ class ModuleJob extends ModuleJobBase {
561561
}
562562

563563
async run(isEntryPoint = false) {
564-
debug('ModuleJob.run()', this.module);
565-
assert(this.shouldRunModule(this.phase));
564+
debug('ModuleJob.run(); phase: ', this.phase, this.module);
566565
await this.#instantiate();
567566
if (isEntryPoint) {
568567
globalThis[entry_point_module_private_symbol] = this.module;
569568
}
569+
570570
const timeout = -1;
571571
const breakOnSigint = false;
572572
setHasStartedUserESMExecution();
573573
try {
574-
await this.module.evaluate(timeout, breakOnSigint);
574+
await this.module.evaluate(timeout, breakOnSigint, this.phase);
575575
} catch (e) {
576576
explainCommonJSGlobalLikeNotDefinedError(e, this.module.url, this.module.hasTopLevelAwait);
577577
throw e;
@@ -641,7 +641,8 @@ class ModuleJobSync extends ModuleJobBase {
641641
}
642642

643643
async run(isEntryPoint = false) {
644-
assert(this.shouldRunModule(this.phase));
644+
assert(this.shouldEvaluateModule(this.phase));
645+
// This path is hit by a require'd module that is imported again.
645646
const status = this.module.getStatus();
646647
debug('ModuleJobSync.run()', status, this.module);
647648
// If the module was previously required and errored, reject from import() again.
@@ -676,7 +677,7 @@ class ModuleJobSync extends ModuleJobBase {
676677
const { callAndPauseOnStart } = internalBinding('inspector');
677678
this.evaluationPromise = callAndPauseOnStart(this.module.evaluate, this.module, timeout, breakOnSigint);
678679
} else {
679-
this.evaluationPromise = this.module.evaluate(timeout, breakOnSigint);
680+
this.evaluationPromise = this.module.evaluate(timeout, breakOnSigint, this.phase);
680681
}
681682
await this.evaluationPromise;
682683
this.evaluationPromise = undefined;
@@ -704,7 +705,7 @@ class ModuleJobSync extends ModuleJobBase {
704705

705706
runSync(parent) {
706707
debug('ModuleJobSync.runSync()', this.module);
707-
assert(this.shouldRunModule(this.phase));
708+
assert(this.shouldEvaluateModule(this.phase));
708709
// TODO(joyeecheung): Reject graphs with top-level await _before_ instantiation, so that the
709710
// async graph error supersedes instantiation (mismatch export) errors in the graph.
710711
this.instantiateSync();

‎lib/internal/modules/esm/utils.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525

2626
const {
2727
ModuleWrap,
28+
kEvaluationPhase,
2829
setImportModuleDynamicallyCallback,
2930
setInitializeImportMetaObjectCallback,
3031
} = internalBinding('module_wrap');
@@ -383,7 +384,7 @@ function compileSourceTextModule(url, source, type, context = kEmptyObject) {
383384
const modules = requests.map(({ specifier }) => getBuiltinModuleWrapForEmbedder(specifier));
384385
wrap.link(modules);
385386
wrap.instantiate();
386-
wrap.evaluate(-1, false);
387+
wrap.evaluate(-1, false, kEvaluationPhase);
387388
}
388389
return wrap;
389390
}

‎src/module_wrap.cc‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,17 @@ ModulePhase to_phase_constant(ModuleImportPhase phase) {
564564
}
565565
}
566566

567+
ModuleImportPhase to_module_import_phase(ModulePhase phase) {
568+
switch (phase) {
569+
case kEvaluationPhase:
570+
return ModuleImportPhase::kEvaluation;
571+
case kDeferPhase:
572+
return ModuleImportPhase::kDefer;
573+
default:
574+
UNREACHABLE();
575+
}
576+
}
577+
567578
static Local<Object> createImportAttributesContainer(
568579
Realm* realm,
569580
Isolate* isolate,
@@ -774,8 +785,8 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
774785
if (contextify_context != nullptr)
775786
microtask_queue = contextify_context->microtask_queue();
776787

777-
// module.evaluate(timeout, breakOnSigint)
778-
CHECK_EQ(args.Length(), 2);
788+
// module.evaluate(timeout, breakOnSigint, phase)
789+
CHECK_EQ(args.Length(), 3);
779790

780791
CHECK(args[0]->IsNumber());
781792
int64_t timeout;
@@ -786,6 +797,9 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
786797
CHECK(args[1]->IsBoolean());
787798
bool break_on_sigint = args[1]->IsTrue();
788799

800+
CHECK(args[2]->IsNumber());
801+
ModulePhase phase = static_cast<ModulePhase>(args[2].As<Int32>()->Value());
802+
789803
ShouldNotAbortOnUncaughtScope no_abort_scope(realm->env());
790804
TryCatchScope try_catch(realm->env());
791805

@@ -800,7 +814,13 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
800814
isolate, &received_signal)
801815
: std::nullopt;
802816

803-
result = module->Evaluate(context);
817+
CHECK_NE(phase, kSourcePhase);
818+
if (phase == kDeferPhase) {
819+
result = module->EvaluateForImportDefer(context);
820+
} else {
821+
CHECK_EQ(phase, kEvaluationPhase);
822+
result = module->Evaluate(context);
823+
}
804824

805825
Local<Value> res;
806826
if (result.ToLocal(&res) && microtask_queue) {
@@ -954,12 +974,19 @@ void ModuleWrap::GetNamespace(const FunctionCallbackInfo<Value>& args) {
954974
ModuleWrap* obj;
955975
ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
956976

977+
ModulePhase phase = kEvaluationPhase;
978+
if (args.Length() > 0) {
979+
CHECK(args[0]->IsInt32());
980+
phase = static_cast<ModulePhase>(args[0].As<Int32>()->Value());
981+
}
982+
957983
Local<Module> module = obj->module_.Get(isolate);
958984
if (module->GetStatus() < Module::kInstantiated) {
959985
return THROW_ERR_MODULE_NOT_INSTANTIATED(realm->env());
960986
}
961987

962-
Local<Value> result = module->GetModuleNamespace();
988+
Local<Value> result =
989+
module->GetModuleNamespace(to_module_import_phase(phase));
963990
args.GetReturnValue().Set(result);
964991
}
965992

‎test/cctest/test_environment.cc‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ TEST_F(EnvironmentTest, EnvironmentWithESMLoader) {
149149
node::Stop(*env);
150150
});
151151

152+
// Arguments to ModuleWrap::Evaluate:
153+
// -1 - timeout, false - breakOnSigint, 3 - ModulePhase::kEvaluationPhase
152154
node::LoadEnvironment(
153155
*env,
154156
"const { SourceTextModule } = require('vm');"
@@ -163,7 +165,7 @@ TEST_F(EnvironmentTest, EnvironmentWithESMLoader) {
163165
"}),"
164166
"});"
165167
"await m.link(() => 0);"
166-
"await m.evaluate();"
168+
"await m.evaluate(-1, false, 3);"
167169
"delete globalThis.importResult;"
168170
"process.exit(0);"
169171
"})()");
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Flags: --js-defer-import-eval
2+
3+
// Tests that defer import of a dynamically imported module
4+
// defers the execution of the module until any of its properties
5+
// is accessed for the first time.
6+
7+
import * as common from '../common/index.mjs';
8+
import * as assert from 'assert';
9+
10+
globalThis.eval_list ||= [];
11+
12+
console.log('main module loading...');
13+
14+
await import.defer('../fixtures/es-modules/module-deferred-eval.mjs').then((module) => {
15+
// Check that the module has been evaluated exactly once.
16+
assert.strictEqual(globalThis.eval_list.length, 1);
17+
assert.strictEqual(module.foo, 42);
18+
assert.partialDeepStrictEqual(['defer-1'], globalThis.eval_list);
19+
20+
// Clean-up
21+
delete globalThis.eval_list;
22+
}).then(common.mustCall());

‎test/parallel/test-internal-module-wrap.js‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ const common = require('../common');
44
const assert = require('assert');
55

66
const { internalBinding } = require('internal/test/binding');
7-
const { ModuleWrap } = internalBinding('module_wrap');
7+
const {
8+
ModuleWrap,
9+
kEvaluationPhase,
10+
} = internalBinding('module_wrap');
811

912
async function testModuleWrap() {
1013
const unlinked = new ModuleWrap('unlinked', undefined, 'export * from "bar";', 0, 0);
@@ -43,7 +46,7 @@ async function testModuleWrap() {
4346
assert.strictEqual(bar.hasAsyncGraph, false);
4447
assert.strictEqual(foo.hasAsyncGraph, false);
4548

46-
assert.strictEqual(await foo.evaluate(-1, false), undefined);
49+
assert.strictEqual(await foo.evaluate(-1, false, kEvaluationPhase), undefined);
4750
assert.strictEqual(foo.getNamespace().five, 5);
4851

4952
// Check that the module requests are the same after linking, instantiate, and evaluation.
@@ -68,7 +71,7 @@ async function testAsyncGraph() {
6871
assert.strictEqual(bar.hasAsyncGraph, true);
6972
assert.strictEqual(foo.hasAsyncGraph, true);
7073

71-
const evalPromise = foo.evaluate(-1, false);
74+
const evalPromise = foo.evaluate(-1, false, kEvaluationPhase);
7275
assert.throws(() => foo.getNamespace().five, ReferenceError);
7376
assert.strictEqual(await evalPromise, undefined);
7477
assert.strictEqual(foo.getNamespace().five, 5);

0 commit comments

Comments
 (0)