Skip to content

Commit 59855d0

Browse files
legendecasaduh95
authored andcommitted
vm: sync-ify SourceTextModule linkage
Split `module.link(linker)` into two synchronous step `sourceTextModule.linkRequests()` and `sourceTextModule.instantiate()`. This allows creating vm modules and resolving the dependencies in a complete synchronous procedure. This also makes `syntheticModule.link()` redundant. The link step for a SyntheticModule is no-op and is already taken care in the constructor by initializing the binding slots with the given export names. PR-URL: #59000 Refs: #37648 Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
1 parent 9381d7c commit 59855d0

16 files changed

+560
-101
lines changed

doc/api/errors.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2301,6 +2301,13 @@ The V8 platform used by this instance of Node.js does not support creating
23012301
Workers. This is caused by lack of embedder support for Workers. In particular,
23022302
this error will not occur with standard builds of Node.js.
23032303

2304+
<a id="ERR_MODULE_LINK_MISMATCH"></a>
2305+
2306+
### `ERR_MODULE_LINK_MISMATCH`
2307+
2308+
A module can not be linked because the same module requests in it are not
2309+
resolved to the same module.
2310+
23042311
<a id="ERR_MODULE_NOT_FOUND"></a>
23052312

23062313
### `ERR_MODULE_NOT_FOUND`

doc/api/vm.md

Lines changed: 163 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -417,9 +417,7 @@ class that closely mirrors [Module Record][]s as defined in the ECMAScript
417417
specification.
418418

419419
Unlike `vm.Script` however, every `vm.Module` object is bound to a context from
420-
its creation. Operations on `vm.Module` objects are intrinsically asynchronous,
421-
in contrast with the synchronous nature of `vm.Script` objects. The use of
422-
'async' functions can help with manipulating `vm.Module` objects.
420+
its creation.
423421

424422
Using a `vm.Module` object requires three distinct steps: creation/parsing,
425423
linking, and evaluation. These three steps are illustrated in the following
@@ -447,7 +445,7 @@ const contextifiedObject = vm.createContext({
447445
// Here, we attempt to obtain the default export from the module "foo", and
448446
// put it into local binding "secret".
449447

450-
const bar = new vm.SourceTextModule(`
448+
const rootModule = new vm.SourceTextModule(`
451449
import s from 'foo';
452450
s;
453451
print(s);
@@ -457,47 +455,56 @@ const bar = new vm.SourceTextModule(`
457455
//
458456
// "Link" the imported dependencies of this Module to it.
459457
//
460-
// The provided linking callback (the "linker") accepts two arguments: the
461-
// parent module (`bar` in this case) and the string that is the specifier of
462-
// the imported module. The callback is expected to return a Module that
463-
// corresponds to the provided specifier, with certain requirements documented
464-
// in `module.link()`.
465-
//
466-
// If linking has not started for the returned Module, the same linker
467-
// callback will be called on the returned Module.
458+
// Obtain the requested dependencies of a SourceTextModule by
459+
// `sourceTextModule.moduleRequests` and resolve them.
468460
//
469461
// Even top-level Modules without dependencies must be explicitly linked. The
470-
// callback provided would never be called, however.
471-
//
472-
// The link() method returns a Promise that will be resolved when all the
473-
// Promises returned by the linker resolve.
462+
// array passed to `sourceTextModule.linkRequests(modules)` can be
463+
// empty, however.
474464
//
475-
// Note: This is a contrived example in that the linker function creates a new
476-
// "foo" module every time it is called. In a full-fledged module system, a
477-
// cache would probably be used to avoid duplicated modules.
478-
479-
async function linker(specifier, referencingModule) {
480-
if (specifier === 'foo') {
481-
return new vm.SourceTextModule(`
482-
// The "secret" variable refers to the global variable we added to
483-
// "contextifiedObject" when creating the context.
484-
export default secret;
485-
`, { context: referencingModule.context });
486-
487-
// Using `contextifiedObject` instead of `referencingModule.context`
488-
// here would work as well.
489-
}
490-
throw new Error(`Unable to resolve dependency: ${specifier}`);
465+
// Note: This is a contrived example in that the resolveAndLinkDependencies
466+
// creates a new "foo" module every time it is called. In a full-fledged
467+
// module system, a cache would probably be used to avoid duplicated modules.
468+
469+
const moduleMap = new Map([
470+
['root', rootModule],
471+
]);
472+
473+
function resolveAndLinkDependencies(module) {
474+
const requestedModules = module.moduleRequests.map((request) => {
475+
// In a full-fledged module system, the resolveAndLinkDependencies would
476+
// resolve the module with the module cache key `[specifier, attributes]`.
477+
// In this example, we just use the specifier as the key.
478+
const specifier = request.specifier;
479+
480+
let requestedModule = moduleMap.get(specifier);
481+
if (requestedModule === undefined) {
482+
requestedModule = new vm.SourceTextModule(`
483+
// The "secret" variable refers to the global variable we added to
484+
// "contextifiedObject" when creating the context.
485+
export default secret;
486+
`, { context: referencingModule.context });
487+
moduleMap.set(specifier, linkedModule);
488+
// Resolve the dependencies of the new module as well.
489+
resolveAndLinkDependencies(requestedModule);
490+
}
491+
492+
return requestedModule;
493+
});
494+
495+
module.linkRequests(requestedModules);
491496
}
492-
await bar.link(linker);
497+
498+
resolveAndLinkDependencies(rootModule);
499+
rootModule.instantiate();
493500

494501
// Step 3
495502
//
496503
// Evaluate the Module. The evaluate() method returns a promise which will
497504
// resolve after the module has finished evaluating.
498505

499506
// Prints 42.
500-
await bar.evaluate();
507+
await rootModule.evaluate();
501508
```
502509

503510
```cjs
@@ -519,7 +526,7 @@ const contextifiedObject = vm.createContext({
519526
// Here, we attempt to obtain the default export from the module "foo", and
520527
// put it into local binding "secret".
521528

522-
const bar = new vm.SourceTextModule(`
529+
const rootModule = new vm.SourceTextModule(`
523530
import s from 'foo';
524531
s;
525532
print(s);
@@ -529,47 +536,56 @@ const contextifiedObject = vm.createContext({
529536
//
530537
// "Link" the imported dependencies of this Module to it.
531538
//
532-
// The provided linking callback (the "linker") accepts two arguments: the
533-
// parent module (`bar` in this case) and the string that is the specifier of
534-
// the imported module. The callback is expected to return a Module that
535-
// corresponds to the provided specifier, with certain requirements documented
536-
// in `module.link()`.
537-
//
538-
// If linking has not started for the returned Module, the same linker
539-
// callback will be called on the returned Module.
539+
// Obtain the requested dependencies of a SourceTextModule by
540+
// `sourceTextModule.moduleRequests` and resolve them.
540541
//
541542
// Even top-level Modules without dependencies must be explicitly linked. The
542-
// callback provided would never be called, however.
543-
//
544-
// The link() method returns a Promise that will be resolved when all the
545-
// Promises returned by the linker resolve.
543+
// array passed to `sourceTextModule.linkRequests(modules)` can be
544+
// empty, however.
546545
//
547-
// Note: This is a contrived example in that the linker function creates a new
548-
// "foo" module every time it is called. In a full-fledged module system, a
549-
// cache would probably be used to avoid duplicated modules.
550-
551-
async function linker(specifier, referencingModule) {
552-
if (specifier === 'foo') {
553-
return new vm.SourceTextModule(`
554-
// The "secret" variable refers to the global variable we added to
555-
// "contextifiedObject" when creating the context.
556-
export default secret;
557-
`, { context: referencingModule.context });
546+
// Note: This is a contrived example in that the resolveAndLinkDependencies
547+
// creates a new "foo" module every time it is called. In a full-fledged
548+
// module system, a cache would probably be used to avoid duplicated modules.
549+
550+
const moduleMap = new Map([
551+
['root', rootModule],
552+
]);
553+
554+
function resolveAndLinkDependencies(module) {
555+
const requestedModules = module.moduleRequests.map((request) => {
556+
// In a full-fledged module system, the resolveAndLinkDependencies would
557+
// resolve the module with the module cache key `[specifier, attributes]`.
558+
// In this example, we just use the specifier as the key.
559+
const specifier = request.specifier;
560+
561+
let requestedModule = moduleMap.get(specifier);
562+
if (requestedModule === undefined) {
563+
requestedModule = new vm.SourceTextModule(`
564+
// The "secret" variable refers to the global variable we added to
565+
// "contextifiedObject" when creating the context.
566+
export default secret;
567+
`, { context: referencingModule.context });
568+
moduleMap.set(specifier, linkedModule);
569+
// Resolve the dependencies of the new module as well.
570+
resolveAndLinkDependencies(requestedModule);
571+
}
572+
573+
return requestedModule;
574+
});
558575

559-
// Using `contextifiedObject` instead of `referencingModule.context`
560-
// here would work as well.
561-
}
562-
throw new Error(`Unable to resolve dependency: ${specifier}`);
576+
module.linkRequests(requestedModules);
563577
}
564-
await bar.link(linker);
578+
579+
resolveAndLinkDependencies(rootModule);
580+
rootModule.instantiate();
565581

566582
// Step 3
567583
//
568584
// Evaluate the Module. The evaluate() method returns a promise which will
569585
// resolve after the module has finished evaluating.
570586

571587
// Prints 42.
572-
await bar.evaluate();
588+
await rootModule.evaluate();
573589
})();
574590
```
575591

@@ -658,6 +674,10 @@ changes:
658674
Link module dependencies. This method must be called before evaluation, and
659675
can only be called once per module.
660676

677+
Use [`sourceTextModule.linkRequests(modules)`][] and
678+
[`sourceTextModule.instantiate()`][] to link modules either synchronously or
679+
asynchronously.
680+
661681
The function is expected to return a `Module` object or a `Promise` that
662682
eventually resolves to a `Module` object. The returned `Module` must satisfy the
663683
following two invariants:
@@ -803,8 +823,9 @@ const module = new vm.SourceTextModule(
803823
meta.prop = {};
804824
},
805825
});
806-
// Since module has no dependencies, the linker function will never be called.
807-
await module.link(() => {});
826+
// The module has an empty `moduleRequests` array.
827+
module.linkRequests([]);
828+
module.instantiate();
808829
await module.evaluate();
809830
810831
// Now, Object.prototype.secret will be equal to 42.
@@ -830,8 +851,9 @@ const contextifiedObject = vm.createContext({ secret: 42 });
830851
meta.prop = {};
831852
},
832853
});
833-
// Since module has no dependencies, the linker function will never be called.
834-
await module.link(() => {});
854+
// The module has an empty `moduleRequests` array.
855+
module.linkRequests([]);
856+
module.instantiate();
835857
await module.evaluate();
836858
// Now, Object.prototype.secret will be equal to 42.
837859
//
@@ -896,6 +918,69 @@ to disallow any changes to it.
896918
Corresponds to the `[[RequestedModules]]` field of [Cyclic Module Record][]s in
897919
the ECMAScript specification.
898920
921+
### `sourceTextModule.instantiate()`
922+
923+
<!-- YAML
924+
added: REPLACEME
925+
-->
926+
927+
* Returns: {undefined}
928+
929+
Instantiate the module with the linked requested modules.
930+
931+
This resolves the imported bindings of the module, including re-exported
932+
binding names. When there are any bindings that cannot be resolved,
933+
an error would be thrown synchronously.
934+
935+
If the requested modules include cyclic dependencies, the
936+
[`sourceTextModule.linkRequests(modules)`][] method must be called on all
937+
modules in the cycle before calling this method.
938+
939+
### `sourceTextModule.linkRequests(modules)`
940+
941+
<!-- YAML
942+
added: REPLACEME
943+
-->
944+
945+
* `modules` {vm.Module\[]} Array of `vm.Module` objects that this module depends on.
946+
The order of the modules in the array is the order of
947+
[`sourceTextModule.moduleRequests`][].
948+
* Returns: {undefined}
949+
950+
Link module dependencies. This method must be called before evaluation, and
951+
can only be called once per module.
952+
953+
The order of the module instances in the `modules` array should correspond to the order of
954+
[`sourceTextModule.moduleRequests`][] being resolved. If two module requests have the same
955+
specifier and import attributes, they must be resolved with the same module instance or an
956+
`ERR_MODULE_LINK_MISMATCH` would be thrown. For example, when linking requests for this
957+
module:
958+
959+
<!-- eslint-disable no-duplicate-imports -->
960+
961+
```mjs
962+
import foo from 'foo';
963+
import source Foo from 'foo';
964+
```
965+
966+
<!-- eslint-enable no-duplicate-imports -->
967+
968+
The `modules` array must contain two references to the same instance, because the two
969+
module requests are identical but in two phases.
970+
971+
If the module has no dependencies, the `modules` array can be empty.
972+
973+
Users can use `sourceTextModule.moduleRequests` to implement the host-defined
974+
[HostLoadImportedModule][] abstract operation in the ECMAScript specification,
975+
and using `sourceTextModule.linkRequests()` to invoke specification defined
976+
[FinishLoadingImportedModule][], on the module with all dependencies in a batch.
977+
978+
It's up to the creator of the `SourceTextModule` to determine if the resolution
979+
of the dependencies is synchronous or asynchronous.
980+
981+
After each module in the `modules` array is linked, call
982+
[`sourceTextModule.instantiate()`][].
983+
899984
### `sourceTextModule.moduleRequests`
900985

901986
<!-- YAML
@@ -1005,14 +1090,17 @@ the module to access information outside the specified `context`. Use
10051090
added:
10061091
- v13.0.0
10071092
- v12.16.0
1093+
changes:
1094+
- version: REPLACEME
1095+
pr-url: https://github.com/nodejs/node/pull/59000
1096+
description: No longer need to call `syntheticModule.link()` before
1097+
calling this method.
10081098
-->
10091099

10101100
* `name` {string} Name of the export to set.
10111101
* `value` {any} The value to set the export to.
10121102

1013-
This method is used after the module is linked to set the values of exports. If
1014-
it is called before the module is linked, an [`ERR_VM_MODULE_STATUS`][] error
1015-
will be thrown.
1103+
This method sets the module export binding slots with the given value.
10161104

10171105
```mjs
10181106
import vm from 'node:vm';
@@ -1021,7 +1109,6 @@ const m = new vm.SyntheticModule(['x'], () => {
10211109
m.setExport('x', 1);
10221110
});
10231111
1024-
await m.link(() => {});
10251112
await m.evaluate();
10261113
10271114
assert.strictEqual(m.namespace.x, 1);
@@ -1033,7 +1120,6 @@ const vm = require('node:vm');
10331120
const m = new vm.SyntheticModule(['x'], () => {
10341121
m.setExport('x', 1);
10351122
});
1036-
await m.link(() => {});
10371123
await m.evaluate();
10381124
assert.strictEqual(m.namespace.x, 1);
10391125
})();
@@ -2083,7 +2169,9 @@ const { Script, SyntheticModule } = require('node:vm');
20832169
[Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records
20842170
[ECMAScript Module Loader]: esm.md#modules-ecmascript-modules
20852171
[Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation
2172+
[FinishLoadingImportedModule]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule
20862173
[GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace
2174+
[HostLoadImportedModule]: https://tc39.es/ecma262/#sec-HostLoadImportedModule
20872175
[HostResolveImportedModule]: https://tc39.es/ecma262/#sec-hostresolveimportedmodule
20882176
[ImportDeclaration]: https://tc39.es/ecma262/#prod-ImportDeclaration
20892177
[Link() concrete method]: https://tc39.es/ecma262/#sec-moduledeclarationlinking
@@ -2095,13 +2183,14 @@ const { Script, SyntheticModule } = require('node:vm');
20952183
[WithClause]: https://tc39.es/ecma262/#prod-WithClause
20962184
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`]: errors.md#err_vm_dynamic_import_callback_missing_flag
20972185
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.md#err_vm_dynamic_import_callback_missing
2098-
[`ERR_VM_MODULE_STATUS`]: errors.md#err_vm_module_status
20992186
[`Error`]: errors.md#class-error
21002187
[`URL`]: url.md#class-url
21012188
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
21022189
[`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call
21032190
[`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options
21042191
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
2192+
[`sourceTextModule.instantiate()`]: #sourcetextmoduleinstantiate
2193+
[`sourceTextModule.linkRequests(modules)`]: #sourcetextmodulelinkrequestsmodules
21052194
[`sourceTextModule.moduleRequests`]: #sourcetextmodulemodulerequests
21062195
[`url.origin`]: url.md#urlorigin
21072196
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options

lib/internal/bootstrap/realm.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ class BuiltinModule {
360360
this.setExport('default', builtin.exports);
361361
});
362362
// Ensure immediate sync execution to capture exports now
363+
this.module.link([]);
363364
this.module.instantiate();
364365
this.module.evaluate(-1, false);
365366
return this.module;

lib/internal/errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,6 +1592,7 @@ E('ERR_MISSING_ARGS',
15921592
return `${msg} must be specified`;
15931593
}, TypeError);
15941594
E('ERR_MISSING_OPTION', '%s is required', TypeError);
1595+
E('ERR_MODULE_LINK_MISMATCH', '%s', TypeError);
15951596
E('ERR_MODULE_NOT_FOUND', function(path, base, exactUrl) {
15961597
if (exactUrl) {
15971598
lazyInternalUtil().setOwnProperty(this, 'url', `${exactUrl}`);

0 commit comments

Comments
 (0)