Skip to content

Commit 6487f07

Browse files
devsnekjasnell
authored andcommitted
vm: add dynamic import support
PR-URL: #22381 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
1 parent 3c62d08 commit 6487f07

18 files changed

+674
-433
lines changed

doc/api/errors.md

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

1782+
<a id="ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING"></a>
1783+
### ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
1784+
1785+
A dynamic import callback was not specified.
1786+
17821787
<a id="ERR_VM_MODULE_ALREADY_LINKED"></a>
17831788
### ERR_VM_MODULE_ALREADY_LINKED
17841789

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
@@ -945,6 +963,7 @@ associating it with the `sandbox` object is what this document refers to as
945963
"contextifying" the `sandbox`.
946964

947965
[`Error`]: errors.html#errors_class_error
966+
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.html#ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
948967
[`URL`]: url.html#url_class_url
949968
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
950969
[`script.runInContext()`]: #vm_script_runincontext_contextifiedsandbox_options
@@ -954,6 +973,7 @@ associating it with the `sandbox` object is what this document refers to as
954973
[`vm.runInContext()`]: #vm_vm_runincontext_code_contextifiedsandbox_options
955974
[`vm.runInThisContext()`]: #vm_vm_runinthiscontext_code_options
956975
[GetModuleNamespace]: https://tc39.github.io/ecma262/#sec-getmodulenamespace
976+
[Module Namespace Object]: https://tc39.github.io/ecma262/#sec-module-namespace-exotic-objects
957977
[ECMAScript Module Loader]: esm.html#esm_ecmascript_modules
958978
[Evaluate() concrete method]: https://tc39.github.io/ecma262/#sec-moduleevaluation
959979
[HostResolveImportedModule]: https://tc39.github.io/ecma262/#sec-hostresolveimportedmodule

lib/internal/bootstrap/loaders.js

+2
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@
107107
};
108108
}
109109

110+
// Create this WeakMap in js-land because V8 has no C++ API for WeakMap
111+
internalBinding('module_wrap').callbackMap = new WeakMap();
110112
const { ContextifyScript } = internalBinding('contextify');
111113

112114
// Set up NativeModule

lib/internal/errors.js

+2
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,8 @@ E('ERR_V8BREAKITERATOR',
873873
// This should probably be a `TypeError`.
874874
E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
875875
'At least one valid performance entry type is required', Error);
876+
E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
877+
'A dynamic import callback was not specified.', TypeError);
876878
E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked', Error);
877879
E('ERR_VM_MODULE_DIFFERENT_CONTEXT',
878880
'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
@@ -656,6 +657,13 @@ Module.prototype.require = function(id) {
656657
// (needed for setting breakpoint when called with --inspect-brk)
657658
var resolvedArgv;
658659

660+
function normalizeReferrerURL(referrer) {
661+
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
662+
return pathToFileURL(referrer).href;
663+
}
664+
return new URL(referrer).href;
665+
}
666+
659667

660668
// Run the file contents in the correct scope or sandbox. Expose
661669
// the correct helper variables (require, module, exports) to
@@ -671,7 +679,12 @@ Module.prototype._compile = function(content, filename) {
671679
var compiledWrapper = vm.runInThisContext(wrapper, {
672680
filename: filename,
673681
lineOffset: 0,
674-
displayErrors: true
682+
displayErrors: true,
683+
importModuleDynamically: experimentalModules ? async (specifier) => {
684+
if (asyncESM === undefined) lazyLoadESM();
685+
const loader = await asyncESM.loaderPromise;
686+
return loader.import(specifier, normalizeReferrerURL(filename));
687+
} : undefined,
675688
});
676689

677690
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 = process.binding('config').userLoader;
@@ -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 } = internalBinding('contextify');
56
const {
@@ -9,7 +10,7 @@ const {
910
ERR_VM_MODULE_LINKING_ERRORED,
1011
ERR_VM_MODULE_NOT_LINKED,
1112
ERR_VM_MODULE_NOT_MODULE,
12-
ERR_VM_MODULE_STATUS
13+
ERR_VM_MODULE_STATUS,
1314
} = require('internal/errors').codes;
1415
const {
1516
getConstructorOf,
@@ -21,6 +22,7 @@ const { validateInt32, validateUint32 } = require('internal/validators');
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
validateInt32(lineOffset, 'options.lineOffset');
9798
validateInt32(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 },
@@ -245,6 +265,7 @@ class SourceTextModule {
245265

246266
module.exports = {
247267
SourceTextModule,
248-
initImportMetaMap,
249-
wrapToModuleMap
268+
wrapToModuleMap,
269+
wrapMap,
270+
linkingStatusMap,
250271
};

0 commit comments

Comments
 (0)