Skip to content

Commit 5b2655d

Browse files
committed
vm: support using the default loader to handle dynamic import()
This patch adds support for using `vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER` as `importModuleDynamically` in all APIs that take the option except `vm.SourceTextModule`. This allows users to have a shortcut to support dynamic import() in the compiled code without missing the compilation cache if they don't need customization of the loading process. We emit an experimental warning when the `import()` is actually handled by the default loader through this option instead of requiring `--experimental-vm-modules`. In addition this refactors the documentation for `importModuleDynamically` and adds a dedicated section for it with examples. `vm.SourceTextModule` is not supported in this patch because it needs additional refactoring to handle `initializeImportMeta`, which can be done in a follow-up.
1 parent 1674cea commit 5b2655d

File tree

13 files changed

+552
-163
lines changed

13 files changed

+552
-163
lines changed

doc/api/vm.md

Lines changed: 254 additions & 105 deletions
Large diffs are not rendered by default.

lib/internal/modules/cjs/loader.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ const {
5252
SafeMap,
5353
SafeWeakMap,
5454
String,
55-
Symbol,
5655
StringPrototypeCharAt,
5756
StringPrototypeCharCodeAt,
5857
StringPrototypeEndsWith,
@@ -107,7 +106,6 @@ const {
107106
initializeCjsConditions,
108107
loadBuiltinModule,
109108
makeRequireFunction,
110-
normalizeReferrerURL,
111109
stripBOM,
112110
toRealPath,
113111
} = require('internal/modules/helpers');
@@ -121,9 +119,10 @@ const shouldReportRequiredModules = getLazy(() => process.env.WATCH_REPORT_DEPEN
121119
const getCascadedLoader = getLazy(
122120
() => require('internal/process/esm_loader').esmLoader,
123121
);
124-
125122
const permission = require('internal/process/permission');
126-
123+
const {
124+
vm_dynamic_import_default_internal,
125+
} = internalBinding('symbols');
127126
// Whether any user-provided CJS modules had been loaded (executed).
128127
// Used for internal assertions.
129128
let hasLoadedAnyUserCJSModule = false;
@@ -1254,12 +1253,8 @@ let hasPausedEntry = false;
12541253
* @param {object} codeCache The SEA code cache
12551254
*/
12561255
function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
1257-
const hostDefinedOptionId = Symbol(`cjs:${filename}`);
1258-
async function importModuleDynamically(specifier, _, importAttributes) {
1259-
const cascadedLoader = getCascadedLoader();
1260-
return cascadedLoader.import(specifier, normalizeReferrerURL(filename),
1261-
importAttributes);
1262-
}
1256+
const hostDefinedOptionId = vm_dynamic_import_default_internal;
1257+
const importModuleDynamically = vm_dynamic_import_default_internal;
12631258
if (patched) {
12641259
const wrapped = Module.wrap(content);
12651260
const script = makeContextifyScript(

lib/internal/modules/esm/translators.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const {
1515
StringPrototypeReplaceAll,
1616
StringPrototypeSlice,
1717
StringPrototypeStartsWith,
18-
Symbol,
1918
SyntaxErrorPrototype,
2019
globalThis: { WebAssembly },
2120
} = primordials;
@@ -58,7 +57,9 @@ const { ModuleWrap } = moduleWrap;
5857
const asyncESM = require('internal/process/esm_loader');
5958
const { emitWarningSync } = require('internal/process/warning');
6059
const { internalCompileFunction } = require('internal/vm');
61-
60+
const {
61+
vm_dynamic_import_default_internal,
62+
} = internalBinding('symbols');
6263
// Lazy-loading to avoid circular dependencies.
6364
let getSourceSync;
6465
/**
@@ -205,9 +206,8 @@ function enrichCJSError(err, content, filename) {
205206
*/
206207
function loadCJSModule(module, source, url, filename) {
207208
let compiledWrapper;
208-
async function importModuleDynamically(specifier, _, importAttributes) {
209-
return asyncESM.esmLoader.import(specifier, url, importAttributes);
210-
}
209+
const hostDefinedOptionId = vm_dynamic_import_default_internal;
210+
const importModuleDynamically = vm_dynamic_import_default_internal;
211211
try {
212212
compiledWrapper = internalCompileFunction(
213213
source, // code,
@@ -225,8 +225,8 @@ function loadCJSModule(module, source, url, filename) {
225225
'__filename',
226226
'__dirname',
227227
],
228-
Symbol(`cjs:${filename}`), // hostDefinedOptionsId
229-
importModuleDynamically, // importModuleDynamically
228+
hostDefinedOptionId, // hostDefinedOptionsId
229+
importModuleDynamically, // importModuleDynamically
230230
).function;
231231
} catch (err) {
232232
enrichCJSError(err, source, filename);

lib/internal/modules/esm/utils.js

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ const {
1414
},
1515
} = internalBinding('util');
1616
const {
17-
default_host_defined_options,
17+
vm_dynamic_import_default_internal,
18+
vm_dynamic_import_main_context_default,
1819
vm_dynamic_import_missing_flag,
20+
vm_dynamic_import_no_callback,
1921
} = internalBinding('symbols');
2022

2123
const {
@@ -28,12 +30,19 @@ const {
2830
loadPreloadModules,
2931
initializeFrozenIntrinsics,
3032
} = require('internal/process/pre_execution');
31-
const { getCWDURL } = require('internal/util');
33+
const {
34+
emitExperimentalWarning,
35+
getCWDURL,
36+
getLazy,
37+
} = require('internal/util');
3238
const {
3339
setImportModuleDynamicallyCallback,
3440
setInitializeImportMetaObjectCallback,
3541
} = internalBinding('module_wrap');
3642
const assert = require('internal/assert');
43+
const {
44+
normalizeReferrerURL,
45+
} = require('internal/modules/helpers');
3746

3847
let defaultConditions;
3948
/**
@@ -145,8 +154,10 @@ const moduleRegistries = new SafeWeakMap();
145154
*/
146155
function registerModule(referrer, registry) {
147156
const idSymbol = referrer[host_defined_option_symbol];
148-
if (idSymbol === default_host_defined_options ||
149-
idSymbol === vm_dynamic_import_missing_flag) {
157+
if (idSymbol === vm_dynamic_import_no_callback ||
158+
idSymbol === vm_dynamic_import_missing_flag ||
159+
idSymbol === vm_dynamic_import_main_context_default ||
160+
idSymbol === vm_dynamic_import_default_internal) {
150161
// The referrer is compiled without custom callbacks, so there is
151162
// no registry to hold on to. We'll throw
152163
// ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING when a callback is
@@ -191,16 +202,36 @@ function initializeImportMetaObject(symbol, meta) {
191202
}
192203
}
193204
}
205+
const getCascadedLoader = getLazy(
206+
() => require('internal/process/esm_loader').esmLoader,
207+
);
208+
function defaultImportModuleDynamically(specifier, attributes, referrerName) {
209+
const parentURL = normalizeReferrerURL(referrerName);
210+
return getCascadedLoader().import(specifier, parentURL, attributes);
211+
}
194212

195213
/**
196214
* Asynchronously imports a module dynamically using a callback function. The native callback.
197215
* @param {symbol} referrerSymbol - Referrer symbol of the registered script, function, module, or contextified object.
198216
* @param {string} specifier - The module specifier string.
199217
* @param {Record<string, string>} attributes - The import attributes object.
218+
* @param {string|null} referrerName - name of the referrer.
200219
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
201220
* @throws {ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING} - If the callback function is missing.
202221
*/
203-
async function importModuleDynamicallyCallback(referrerSymbol, specifier, attributes) {
222+
async function importModuleDynamicallyCallback(referrerSymbol, specifier, attributes, referrerName) {
223+
// For user-provided vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, emit the warning
224+
// and fall back to the default loader.
225+
if (referrerSymbol === vm_dynamic_import_main_context_default) {
226+
emitExperimentalWarning('vm.USE_MAIN_CONTEXT_DEFAULT_LOADER');
227+
return defaultImportModuleDynamically(specifier, attributes, referrerName);
228+
}
229+
// For script compiled internally that should use the default loader to handle dynamic
230+
// import, proxy the request to the default loader without the warning.
231+
if (referrerSymbol === vm_dynamic_import_default_internal) {
232+
return defaultImportModuleDynamically(specifier, attributes, referrerName);
233+
}
234+
204235
if (moduleRegistries.has(referrerSymbol)) {
205236
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(referrerSymbol);
206237
if (importModuleDynamically !== undefined) {

lib/internal/modules/helpers.js

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
require_private_symbol,
3434
},
3535
} = internalBinding('util');
36+
const { canParse: URLCanParse } = internalBinding('url');
3637

3738
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
3839
debug = fn;
@@ -288,14 +289,30 @@ function addBuiltinLibsToObject(object, dummyModuleName) {
288289
}
289290

290291
/**
291-
* If a referrer is an URL instance or absolute path, convert it into an URL string.
292-
* @param {string | URL} referrer
292+
* Normalize the referrer name as a URL.
293+
* If it's an absolute path or a file:// it's normalized as a file:// URL.
294+
* Otherwise it's returned as undefined;
295+
* @param {string | null | undefined | false | any } referrerName
296+
* @returns {string | undefined}
293297
*/
294-
function normalizeReferrerURL(referrer) {
295-
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
296-
return pathToFileURL(referrer).href;
298+
function normalizeReferrerURL(referrerName) {
299+
if (typeof referrerName !== 'string') {
300+
return undefined;
297301
}
298-
return new URL(referrer).href;
302+
303+
if (StringPrototypeStartsWith(referrerName, 'file://')) {
304+
return referrerName;
305+
}
306+
307+
if (path.isAbsolute(referrerName)) {
308+
return pathToFileURL(referrerName).href;
309+
}
310+
311+
if (URLCanParse(referrerName)) {
312+
return new URL(referrerName).href;
313+
}
314+
315+
return undefined;
299316
}
300317

301318
module.exports = {

lib/internal/process/pre_execution.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,21 @@ function prepareShadowRealmExecution() {
7676

7777
// Disable custom loaders in ShadowRealm.
7878
setupUserModules(true);
79-
registerRealm(globalThis, {
80-
__proto__: null,
81-
importModuleDynamically: (specifier, _referrer, attributes) => {
82-
// The handler for `ShadowRealm.prototype.importValue`.
83-
const { esmLoader } = require('internal/process/esm_loader');
84-
// `parentURL` is not set in the case of a ShadowRealm top-level import.
85-
return esmLoader.import(specifier, undefined, attributes);
79+
const {
80+
privateSymbols: {
81+
host_defined_option_symbol,
8682
},
87-
});
83+
} = internalBinding('util');
84+
const {
85+
vm_dynamic_import_default_internal,
86+
} = internalBinding('symbols');
87+
88+
// For ShadowRealm.prototype.importValue(), the referrer name is
89+
// always null which would be coerced to an undefined parentURL.
90+
// when we use vm_dynamic_import_default_internal
91+
// to proxy the request to the default handler.
92+
globalThis[host_defined_option_symbol] =
93+
vm_dynamic_import_default_internal;
8894
}
8995

9096
function prepareExecution(options) {

lib/internal/source_map/source_map_cache.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,10 @@ function extractSourceMapURLMagicComment(content) {
105105
function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource, sourceURL, sourceMapURL) {
106106
const sourceMapsEnabled = getSourceMapsEnabled();
107107
if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
108-
try {
109-
const { normalizeReferrerURL } = require('internal/modules/helpers');
110-
filename = normalizeReferrerURL(filename);
111-
} catch (err) {
108+
const { normalizeReferrerURL } = require('internal/modules/helpers');
109+
filename = normalizeReferrerURL(filename);
110+
if (filename === undefined) {
112111
// This is most likely an invalid filename in sourceURL of [eval]-wrapper.
113-
debug(err);
114112
return;
115113
}
116114

lib/internal/vm.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ const {
1414
runInContext,
1515
} = ContextifyScript.prototype;
1616
const {
17-
default_host_defined_options,
17+
vm_dynamic_import_default_internal,
18+
vm_dynamic_import_main_context_default,
19+
vm_dynamic_import_no_callback,
1820
vm_dynamic_import_missing_flag,
1921
} = internalBinding('symbols');
2022
const {
@@ -27,14 +29,18 @@ const {
2729
getOptionValue,
2830
} = require('internal/options');
2931

30-
3132
function isContext(object) {
3233
validateObject(object, 'object', kValidateObjectAllowArray);
3334

3435
return _isContext(object);
3536
}
3637

3738
function getHostDefinedOptionId(importModuleDynamically, hint) {
39+
if (importModuleDynamically === vm_dynamic_import_main_context_default ||
40+
importModuleDynamically === vm_dynamic_import_default_internal) {
41+
return importModuleDynamically;
42+
}
43+
3844
if (importModuleDynamically !== undefined) {
3945
// Check that it's either undefined or a function before we pass
4046
// it into the native constructor.
@@ -45,7 +51,7 @@ function getHostDefinedOptionId(importModuleDynamically, hint) {
4551
// We need a default host defined options that are the same for all
4652
// scripts not needing custom module callbacks so that the isolate
4753
// compilation cache can be hit.
48-
return default_host_defined_options;
54+
return vm_dynamic_import_no_callback;
4955
}
5056
// We should've thrown here immediately when we introduced
5157
// --experimental-vm-modules and importModuleDynamically, but since
@@ -61,6 +67,13 @@ function getHostDefinedOptionId(importModuleDynamically, hint) {
6167
}
6268

6369
function registerImportModuleDynamically(referrer, importModuleDynamically) {
70+
// If it's undefined or certain known symbol, there's no customization so
71+
// no need to register anything.
72+
if (importModuleDynamically === undefined ||
73+
importModuleDynamically === vm_dynamic_import_main_context_default ||
74+
importModuleDynamically === vm_dynamic_import_default_internal) {
75+
return;
76+
}
6477
const { importModuleDynamicallyWrap } = require('internal/vm/module');
6578
const { registerModule } = require('internal/modules/esm/utils');
6679
registerModule(referrer, {
@@ -99,9 +112,7 @@ function internalCompileFunction(
99112
result.function.cachedDataRejected = result.cachedDataRejected;
100113
}
101114

102-
if (importModuleDynamically !== undefined) {
103-
registerImportModuleDynamically(result.function, importModuleDynamically);
104-
}
115+
registerImportModuleDynamically(result.function, importModuleDynamically);
105116

106117
return result;
107118
}
@@ -132,9 +143,7 @@ function makeContextifyScript(code,
132143
throw e; /* node-do-not-add-exception-line */
133144
}
134145

135-
if (importModuleDynamically !== undefined) {
136-
registerImportModuleDynamically(script, importModuleDynamically);
137-
}
146+
registerImportModuleDynamically(script, importModuleDynamically);
138147
return script;
139148
}
140149

lib/vm.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
const {
2525
ArrayPrototypeForEach,
26+
ObjectFreeze,
2627
Symbol,
2728
PromiseReject,
2829
ReflectApply,
@@ -61,6 +62,9 @@ const {
6162
isContext,
6263
registerImportModuleDynamically,
6364
} = require('internal/vm');
65+
const {
66+
vm_dynamic_import_main_context_default,
67+
} = internalBinding('symbols');
6468
const kParsingContext = Symbol('script parsing context');
6569

6670
class Script extends ContextifyScript {
@@ -108,9 +112,7 @@ class Script extends ContextifyScript {
108112
throw e; /* node-do-not-add-exception-line */
109113
}
110114

111-
if (importModuleDynamically !== undefined) {
112-
registerImportModuleDynamically(this, importModuleDynamically);
113-
}
115+
registerImportModuleDynamically(this, importModuleDynamically);
114116
}
115117

116118
runInThisContext(options) {
@@ -245,9 +247,7 @@ function createContext(contextObject = {}, options = kEmptyObject) {
245247

246248
makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
247249
// Register the context scope callback after the context was initialized.
248-
if (importModuleDynamically !== undefined) {
249-
registerImportModuleDynamically(contextObject, importModuleDynamically);
250-
}
250+
registerImportModuleDynamically(contextObject, importModuleDynamically);
251251
return contextObject;
252252
}
253253

@@ -378,6 +378,13 @@ function measureMemory(options = kEmptyObject) {
378378
return result;
379379
}
380380

381+
const vmConstants = {
382+
__proto__: null,
383+
USE_MAIN_CONTEXT_DEFAULT_LOADER: vm_dynamic_import_main_context_default,
384+
};
385+
386+
ObjectFreeze(vmConstants);
387+
381388
module.exports = {
382389
Script,
383390
createContext,
@@ -388,6 +395,7 @@ module.exports = {
388395
isContext,
389396
compileFunction,
390397
measureMemory,
398+
constants: vmConstants,
391399
};
392400

393401
// The vm module is patched to include vm.Module, vm.SourceTextModule

0 commit comments

Comments
 (0)