Skip to content

Commit 134d1e9

Browse files
devsnekBethGriggs
authored andcommitted
vm: add dynamic import support
Backport-PR-URL: #25421 PR-URL: #22381 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Tiancheng "Timothy" Gu <[email protected]>
1 parent c9a3e40 commit 134d1e9

17 files changed

+671
-432
lines changed

doc/api/errors.md

+5
Original file line numberDiff line numberDiff line change
@@ -1795,6 +1795,11 @@ The V8 `BreakIterator` API was used but the full ICU data set is not installed.
17951795
While using the Performance Timing API (`perf_hooks`), no valid performance
17961796
entry types were found.
17971797

1798+
<a id="ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING"></a>
1799+
### ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
1800+
1801+
A dynamic import callback was not specified.
1802+
17981803
<a id="ERR_VM_MODULE_ALREADY_LINKED"></a>
17991804
### ERR_VM_MODULE_ALREADY_LINKED
18001805

doc/api/vm.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,19 @@ const contextifiedSandbox = vm.createContext({ secret: 42 });
167167
in stack traces produced by this `Module`.
168168
* `columnOffset` {integer} Specifies the column number offset that is
169169
displayed in stack traces produced by this `Module`.
170-
* `initalizeImportMeta` {Function} Called during evaluation of this `Module`
170+
* `initializeImportMeta` {Function} Called during evaluation of this `Module`
171171
to initialize the `import.meta`. This function has the signature `(meta,
172172
module)`, where `meta` is the `import.meta` object in the `Module`, and
173173
`module` is this `vm.SourceTextModule` object.
174+
* `importModuleDynamically` {Function} Called during evaluation of this
175+
module when `import()` is called. This function has the signature
176+
`(specifier, module)` where `specifier` is the specifier passed to
177+
`import()` and `module` is this `vm.SourceTextModule`. If this option is
178+
not specified, calls to `import()` will reject with
179+
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. This method can return a
180+
[Module Namespace Object][], but returning a `vm.SourceTextModule` is
181+
recommended in order to take advantage of error tracking, and to avoid
182+
issues with namespaces that contain `then` function exports.
174183

175184
Creates a new ES `Module` object.
176185

@@ -436,6 +445,15 @@ changes:
436445
The `cachedDataProduced` value will be set to either `true` or `false`
437446
depending on whether code cache data is produced successfully.
438447
This option is deprecated in favor of `script.createCachedData()`.
448+
* `importModuleDynamically` {Function} Called during evaluation of this
449+
module when `import()` is called. This function has the signature
450+
`(specifier, module)` where `specifier` is the specifier passed to
451+
`import()` and `module` is this `vm.SourceTextModule`. If this option is
452+
not specified, calls to `import()` will reject with
453+
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. This method can return a
454+
[Module Namespace Object][], but returning a `vm.SourceTextModule` is
455+
recommended in order to take advantage of error tracking, and to avoid
456+
issues with namespaces that contain `then` function exports.
439457

440458
Creating a new `vm.Script` object compiles `code` but does not run it. The
441459
compiled `vm.Script` can be run later multiple times. The `code` is not bound to
@@ -977,6 +995,7 @@ This issue occurs because all contexts share the same microtask and nextTick
977995
queues.
978996

979997
[`Error`]: errors.html#errors_class_error
998+
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.html#ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
980999
[`URL`]: url.html#url_class_url
9811000
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
9821001
[`script.runInContext()`]: #vm_script_runincontext_contextifiedsandbox_options
@@ -985,6 +1004,7 @@ queues.
9851004
[`vm.createContext()`]: #vm_vm_createcontext_sandbox_options
9861005
[`vm.runInContext()`]: #vm_vm_runincontext_code_contextifiedsandbox_options
9871006
[`vm.runInThisContext()`]: #vm_vm_runinthiscontext_code_options
1007+
[Module Namespace Object]: https://tc39.github.io/ecma262/#sec-module-namespace-exotic-objects
9881008
[ECMAScript Module Loader]: esm.html#esm_ecmascript_modules
9891009
[Evaluate() concrete method]: https://tc39.github.io/ecma262/#sec-moduleevaluation
9901010
[GetModuleNamespace]: https://tc39.github.io/ecma262/#sec-getmodulenamespace

lib/internal/bootstrap/loaders.js

+2
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@
115115
};
116116
}
117117

118+
// Create this WeakMap in js-land because V8 has no C++ API for WeakMap
119+
internalBinding('module_wrap').callbackMap = new WeakMap();
118120
const { ContextifyScript } = process.binding('contextify');
119121

120122
// Set up NativeModule

lib/internal/errors.js

+2
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,8 @@ E('ERR_V8BREAKITERATOR',
920920
// This should probably be a `TypeError`.
921921
E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
922922
'At least one valid performance entry type is required', Error);
923+
E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
924+
'A dynamic import callback was not specified.', TypeError);
923925
E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked', Error);
924926
E('ERR_VM_MODULE_DIFFERENT_CONTEXT',
925927
'Linked modules must use the same context', Error);

lib/internal/modules/cjs/loader.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const assert = require('assert').ok;
2929
const fs = require('fs');
3030
const internalFS = require('internal/fs/utils');
3131
const path = require('path');
32+
const { URL } = require('url');
3233
const {
3334
internalModuleReadJSON,
3435
internalModuleStat
@@ -642,6 +643,13 @@ Module.prototype.require = function(id) {
642643
// (needed for setting breakpoint when called with --inspect-brk)
643644
var resolvedArgv;
644645

646+
function normalizeReferrerURL(referrer) {
647+
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
648+
return pathToFileURL(referrer).href;
649+
}
650+
return new URL(referrer).href;
651+
}
652+
645653

646654
// Run the file contents in the correct scope or sandbox. Expose
647655
// the correct helper variables (require, module, exports) to
@@ -657,7 +665,12 @@ Module.prototype._compile = function(content, filename) {
657665
var compiledWrapper = vm.runInThisContext(wrapper, {
658666
filename: filename,
659667
lineOffset: 0,
660-
displayErrors: true
668+
displayErrors: true,
669+
importModuleDynamically: experimentalModules ? async (specifier) => {
670+
if (asyncESM === undefined) lazyLoadESM();
671+
const loader = await asyncESM.loaderPromise;
672+
return loader.import(specifier, normalizeReferrerURL(filename));
673+
} : undefined,
661674
});
662675

663676
var inspectorWrapper = null;

lib/internal/modules/esm/translators.js

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
const { NativeModule } = require('internal/bootstrap/loaders');
4-
const { ModuleWrap } = internalBinding('module_wrap');
4+
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
55
const {
66
stripShebang,
77
stripBOM
@@ -15,6 +15,8 @@ const { _makeLong } = require('path');
1515
const { SafeMap } = require('internal/safe_globals');
1616
const { URL } = require('url');
1717
const { debuglog, promisify } = require('util');
18+
const esmLoader = require('internal/process/esm_loader');
19+
1820
const readFileAsync = promisify(fs.readFile);
1921
const readFileSync = fs.readFileSync;
2022
const StringReplace = Function.call.bind(String.prototype.replace);
@@ -25,13 +27,27 @@ const debug = debuglog('esm');
2527
const translators = new SafeMap();
2628
module.exports = translators;
2729

30+
function initializeImportMeta(meta, { url }) {
31+
meta.url = url;
32+
}
33+
34+
async function importModuleDynamically(specifier, { url }) {
35+
const loader = await esmLoader.loaderPromise;
36+
return loader.import(specifier, url);
37+
}
38+
2839
// Strategy for loading a standard JavaScript module
2940
translators.set('esm', async (url) => {
3041
const source = `${await readFileAsync(new URL(url))}`;
3142
debug(`Translating StandardModule ${url}`);
43+
const module = new ModuleWrap(stripShebang(source), url);
44+
callbackMap.set(module, {
45+
initializeImportMeta,
46+
importModuleDynamically,
47+
});
3248
return {
33-
module: new ModuleWrap(stripShebang(source), url),
34-
reflect: undefined
49+
module,
50+
reflect: undefined,
3551
};
3652
});
3753

lib/internal/process/esm_loader.js

+22-27
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,42 @@
22

33
const {
44
setImportModuleDynamicallyCallback,
5-
setInitializeImportMetaObjectCallback
5+
setInitializeImportMetaObjectCallback,
6+
callbackMap,
67
} = internalBinding('module_wrap');
78

89
const { pathToFileURL } = require('internal/url');
910
const Loader = require('internal/modules/esm/loader');
10-
const path = require('path');
11-
const { URL } = require('url');
1211
const {
13-
initImportMetaMap,
14-
wrapToModuleMap
12+
wrapToModuleMap,
1513
} = require('internal/vm/source_text_module');
14+
const {
15+
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
16+
} = require('internal/errors').codes;
1617

17-
function normalizeReferrerURL(referrer) {
18-
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
19-
return pathToFileURL(referrer).href;
18+
function initializeImportMetaObject(wrap, meta) {
19+
if (callbackMap.has(wrap)) {
20+
const { initializeImportMeta } = callbackMap.get(wrap);
21+
if (initializeImportMeta !== undefined) {
22+
initializeImportMeta(meta, wrapToModuleMap.get(wrap) || wrap);
23+
}
2024
}
21-
return new URL(referrer).href;
2225
}
2326

24-
function initializeImportMetaObject(wrap, meta) {
25-
const vmModule = wrapToModuleMap.get(wrap);
26-
if (vmModule === undefined) {
27-
// This ModuleWrap belongs to the Loader.
28-
meta.url = wrap.url;
29-
} else {
30-
const initializeImportMeta = initImportMetaMap.get(vmModule);
31-
if (initializeImportMeta !== undefined) {
32-
// This ModuleWrap belongs to vm.SourceTextModule,
33-
// initializer callback was provided.
34-
initializeImportMeta(meta, vmModule);
27+
async function importModuleDynamicallyCallback(wrap, specifier) {
28+
if (callbackMap.has(wrap)) {
29+
const { importModuleDynamically } = callbackMap.get(wrap);
30+
if (importModuleDynamically !== undefined) {
31+
return importModuleDynamically(
32+
specifier, wrapToModuleMap.get(wrap) || wrap);
3533
}
3634
}
35+
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
3736
}
3837

38+
setInitializeImportMetaObjectCallback(initializeImportMetaObject);
39+
setImportModuleDynamicallyCallback(importModuleDynamicallyCallback);
40+
3941
let loaderResolve;
4042
exports.loaderPromise = new Promise((resolve, reject) => {
4143
loaderResolve = resolve;
@@ -44,8 +46,6 @@ exports.loaderPromise = new Promise((resolve, reject) => {
4446
exports.ESMLoader = undefined;
4547

4648
exports.setup = function() {
47-
setInitializeImportMetaObjectCallback(initializeImportMetaObject);
48-
4949
let ESMLoader = new Loader();
5050
const loaderPromise = (async () => {
5151
const userLoader = require('internal/options').getOptionValue('--loader');
@@ -60,10 +60,5 @@ exports.setup = function() {
6060
})();
6161
loaderResolve(loaderPromise);
6262

63-
setImportModuleDynamicallyCallback(async (referrer, specifier) => {
64-
const loader = await loaderPromise;
65-
return loader.import(specifier, normalizeReferrerURL(referrer));
66-
});
67-
6863
exports.ESMLoader = ESMLoader;
6964
};

lib/internal/vm/source_text_module.js

+34-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
const { isModuleNamespaceObject } = require('util').types;
34
const { URL } = require('internal/url');
45
const { isContext } = process.binding('contextify');
56
const {
@@ -10,7 +11,7 @@ const {
1011
ERR_VM_MODULE_LINKING_ERRORED,
1112
ERR_VM_MODULE_NOT_LINKED,
1213
ERR_VM_MODULE_NOT_MODULE,
13-
ERR_VM_MODULE_STATUS
14+
ERR_VM_MODULE_STATUS,
1415
} = require('internal/errors').codes;
1516
const {
1617
getConstructorOf,
@@ -21,6 +22,7 @@ const { SafePromise } = require('internal/safe_globals');
2122

2223
const {
2324
ModuleWrap,
25+
callbackMap,
2426
kUninstantiated,
2527
kInstantiating,
2628
kInstantiated,
@@ -43,8 +45,6 @@ const perContextModuleId = new WeakMap();
4345
const wrapMap = new WeakMap();
4446
const dependencyCacheMap = new WeakMap();
4547
const linkingStatusMap = new WeakMap();
46-
// vm.SourceTextModule -> function
47-
const initImportMetaMap = new WeakMap();
4848
// ModuleWrap -> vm.SourceTextModule
4949
const wrapToModuleMap = new WeakMap();
5050
const defaultModuleName = 'vm:module';
@@ -63,7 +63,8 @@ class SourceTextModule {
6363
context,
6464
lineOffset = 0,
6565
columnOffset = 0,
66-
initializeImportMeta
66+
initializeImportMeta,
67+
importModuleDynamically,
6768
} = options;
6869

6970
if (context !== undefined) {
@@ -96,20 +97,39 @@ class SourceTextModule {
9697
validateInteger(lineOffset, 'options.lineOffset');
9798
validateInteger(columnOffset, 'options.columnOffset');
9899

99-
if (initializeImportMeta !== undefined) {
100-
if (typeof initializeImportMeta === 'function') {
101-
initImportMetaMap.set(this, initializeImportMeta);
102-
} else {
103-
throw new ERR_INVALID_ARG_TYPE(
104-
'options.initializeImportMeta', 'function', initializeImportMeta);
105-
}
100+
if (initializeImportMeta !== undefined &&
101+
typeof initializeImportMeta !== 'function') {
102+
throw new ERR_INVALID_ARG_TYPE(
103+
'options.initializeImportMeta', 'function', initializeImportMeta);
104+
}
105+
106+
if (importModuleDynamically !== undefined &&
107+
typeof importModuleDynamically !== 'function') {
108+
throw new ERR_INVALID_ARG_TYPE(
109+
'options.importModuleDynamically', 'function', importModuleDynamically);
106110
}
107111

108112
const wrap = new ModuleWrap(src, url, context, lineOffset, columnOffset);
109113
wrapMap.set(this, wrap);
110114
linkingStatusMap.set(this, 'unlinked');
111115
wrapToModuleMap.set(wrap, this);
112116

117+
callbackMap.set(wrap, {
118+
initializeImportMeta,
119+
importModuleDynamically: importModuleDynamically ? async (...args) => {
120+
const m = await importModuleDynamically(...args);
121+
if (isModuleNamespaceObject(m)) {
122+
return m;
123+
}
124+
if (!m || !wrapMap.has(m))
125+
throw new ERR_VM_MODULE_NOT_MODULE();
126+
const childLinkingStatus = linkingStatusMap.get(m);
127+
if (childLinkingStatus === 'errored')
128+
throw m.error;
129+
return m.namespace;
130+
} : undefined,
131+
});
132+
113133
Object.defineProperties(this, {
114134
url: { value: url, enumerable: true },
115135
context: { value: context, enumerable: true },
@@ -255,6 +275,7 @@ function validateInteger(prop, propName) {
255275

256276
module.exports = {
257277
SourceTextModule,
258-
initImportMetaMap,
259-
wrapToModuleMap
278+
wrapToModuleMap,
279+
wrapMap,
280+
linkingStatusMap,
260281
};

0 commit comments

Comments
 (0)