Skip to content

Commit 0211a3d

Browse files
joyeecheungrichardlau
authored andcommitted
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. PR-URL: #51244 Fixes: #51154 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]>
1 parent 9a68b47 commit 0211a3d

File tree

15 files changed

+595
-193
lines changed

15 files changed

+595
-193
lines changed

doc/api/vm.md

+309-106
Large diffs are not rendered by default.

lib/internal/bootstrap/switches/is_main_thread.js

-1
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,6 @@ require('url'); // eslint-disable-line no-restricted-modules
294294
internalBinding('module_wrap');
295295
require('internal/modules/cjs/loader');
296296
require('internal/modules/esm/utils');
297-
require('internal/vm/module');
298297

299298
// Needed to refresh the time origin.
300299
require('internal/perf/utils');

lib/internal/modules/cjs/loader.js

+5-13
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,
@@ -114,7 +113,6 @@ const {
114113
initializeCjsConditions,
115114
loadBuiltinModule,
116115
makeRequireFunction,
117-
normalizeReferrerURL,
118116
stripBOM,
119117
toRealPath,
120118
} = require('internal/modules/helpers');
@@ -125,12 +123,10 @@ const policy = getLazy(
125123
);
126124
const shouldReportRequiredModules = getLazy(() => process.env.WATCH_REPORT_DEPENDENCIES);
127125

128-
const getCascadedLoader = getLazy(
129-
() => require('internal/process/esm_loader').esmLoader,
130-
);
131-
132126
const permission = require('internal/process/permission');
133-
127+
const {
128+
vm_dynamic_import_default_internal,
129+
} = internalBinding('symbols');
134130
// Whether any user-provided CJS modules had been loaded (executed).
135131
// Used for internal assertions.
136132
let hasLoadedAnyUserCJSModule = false;
@@ -1258,12 +1254,8 @@ let hasPausedEntry = false;
12581254
* @param {object} codeCache The SEA code cache
12591255
*/
12601256
function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
1261-
const hostDefinedOptionId = Symbol(`cjs:${filename}`);
1262-
async function importModuleDynamically(specifier, _, importAttributes) {
1263-
const cascadedLoader = getCascadedLoader();
1264-
return cascadedLoader.import(specifier, normalizeReferrerURL(filename),
1265-
importAttributes);
1266-
}
1257+
const hostDefinedOptionId = vm_dynamic_import_default_internal;
1258+
const importModuleDynamically = vm_dynamic_import_default_internal;
12671259
if (patched) {
12681260
const wrapped = Module.wrap(content);
12691261
const script = makeContextifyScript(

lib/internal/modules/esm/translators.js

+7-7
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;
@@ -59,7 +58,9 @@ const { ModuleWrap } = moduleWrap;
5958
const asyncESM = require('internal/process/esm_loader');
6059
const { emitWarningSync } = require('internal/process/warning');
6160
const { internalCompileFunction } = require('internal/vm');
62-
61+
const {
62+
vm_dynamic_import_default_internal,
63+
} = internalBinding('symbols');
6364
// Lazy-loading to avoid circular dependencies.
6465
let getSourceSync;
6566
/**
@@ -206,9 +207,8 @@ function enrichCJSError(err, content, filename) {
206207
*/
207208
function loadCJSModule(module, source, url, filename) {
208209
let compiledWrapper;
209-
async function importModuleDynamically(specifier, _, importAttributes) {
210-
return asyncESM.esmLoader.import(specifier, url, importAttributes);
211-
}
210+
const hostDefinedOptionId = vm_dynamic_import_default_internal;
211+
const importModuleDynamically = vm_dynamic_import_default_internal;
212212
try {
213213
compiledWrapper = internalCompileFunction(
214214
source, // code,
@@ -226,8 +226,8 @@ function loadCJSModule(module, source, url, filename) {
226226
'__filename',
227227
'__dirname',
228228
],
229-
Symbol(`cjs:${filename}`), // hostDefinedOptionsId
230-
importModuleDynamically, // importModuleDynamically
229+
hostDefinedOptionId, // hostDefinedOptionsId
230+
importModuleDynamically, // importModuleDynamically
231231
).function;
232232
} catch (err) {
233233
enrichCJSError(err, source, filename);

lib/internal/modules/esm/utils.js

+44-27
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const {
44
ArrayIsArray,
55
SafeSet,
66
SafeWeakMap,
7-
Symbol,
87
ObjectFreeze,
98
} = primordials;
109

@@ -14,8 +13,10 @@ const {
1413
},
1514
} = internalBinding('util');
1615
const {
17-
default_host_defined_options,
16+
vm_dynamic_import_default_internal,
17+
vm_dynamic_import_main_context_default,
1818
vm_dynamic_import_missing_flag,
19+
vm_dynamic_import_no_callback,
1920
} = internalBinding('symbols');
2021

2122
const {
@@ -28,12 +29,19 @@ const {
2829
loadPreloadModules,
2930
initializeFrozenIntrinsics,
3031
} = require('internal/process/pre_execution');
31-
const { getCWDURL } = require('internal/util');
32+
const {
33+
emitExperimentalWarning,
34+
getCWDURL,
35+
getLazy,
36+
} = require('internal/util');
3237
const {
3338
setImportModuleDynamicallyCallback,
3439
setInitializeImportMetaObjectCallback,
3540
} = internalBinding('module_wrap');
3641
const assert = require('internal/assert');
42+
const {
43+
normalizeReferrerURL,
44+
} = require('internal/modules/helpers');
3745

3846
let defaultConditions;
3947
/**
@@ -145,8 +153,10 @@ const moduleRegistries = new SafeWeakMap();
145153
*/
146154
function registerModule(referrer, registry) {
147155
const idSymbol = referrer[host_defined_option_symbol];
148-
if (idSymbol === default_host_defined_options ||
149-
idSymbol === vm_dynamic_import_missing_flag) {
156+
if (idSymbol === vm_dynamic_import_no_callback ||
157+
idSymbol === vm_dynamic_import_missing_flag ||
158+
idSymbol === vm_dynamic_import_main_context_default ||
159+
idSymbol === vm_dynamic_import_default_internal) {
150160
// The referrer is compiled without custom callbacks, so there is
151161
// no registry to hold on to. We'll throw
152162
// ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING when a callback is
@@ -158,26 +168,6 @@ function registerModule(referrer, registry) {
158168
moduleRegistries.set(idSymbol, registry);
159169
}
160170

161-
/**
162-
* Registers the ModuleRegistry for dynamic import() calls with a realm
163-
* as the referrer. Similar to {@link registerModule}, but this function
164-
* generates a new id symbol instead of using the one from the referrer
165-
* object.
166-
* @param {globalThis} globalThis The globalThis object of the realm.
167-
* @param {ModuleRegistry} registry
168-
*/
169-
function registerRealm(globalThis, registry) {
170-
let idSymbol = globalThis[host_defined_option_symbol];
171-
// If the per-realm host-defined options is already registered, do nothing.
172-
if (idSymbol) {
173-
return;
174-
}
175-
// Otherwise, register the per-realm host-defined options.
176-
idSymbol = Symbol('Realm globalThis');
177-
globalThis[host_defined_option_symbol] = idSymbol;
178-
moduleRegistries.set(idSymbol, registry);
179-
}
180-
181171
/**
182172
* Defines the `import.meta` object for a given module.
183173
* @param {symbol} symbol - Reference to the module.
@@ -191,16 +181,44 @@ function initializeImportMetaObject(symbol, meta) {
191181
}
192182
}
193183
}
184+
const getCascadedLoader = getLazy(
185+
() => require('internal/process/esm_loader').esmLoader,
186+
);
187+
188+
/**
189+
* Proxy the dynamic import to the default loader.
190+
* @param {string} specifier - The module specifier string.
191+
* @param {Record<string, string>} attributes - The import attributes object.
192+
* @param {string|null|undefined} referrerName - name of the referrer.
193+
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
194+
*/
195+
function defaultImportModuleDynamically(specifier, attributes, referrerName) {
196+
const parentURL = normalizeReferrerURL(referrerName);
197+
return getCascadedLoader().import(specifier, parentURL, attributes);
198+
}
194199

195200
/**
196201
* Asynchronously imports a module dynamically using a callback function. The native callback.
197202
* @param {symbol} referrerSymbol - Referrer symbol of the registered script, function, module, or contextified object.
198203
* @param {string} specifier - The module specifier string.
199204
* @param {Record<string, string>} attributes - The import attributes object.
205+
* @param {string|null|undefined} referrerName - name of the referrer.
200206
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
201207
* @throws {ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING} - If the callback function is missing.
202208
*/
203-
async function importModuleDynamicallyCallback(referrerSymbol, specifier, attributes) {
209+
async function importModuleDynamicallyCallback(referrerSymbol, specifier, attributes, referrerName) {
210+
// For user-provided vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, emit the warning
211+
// and fall back to the default loader.
212+
if (referrerSymbol === vm_dynamic_import_main_context_default) {
213+
emitExperimentalWarning('vm.USE_MAIN_CONTEXT_DEFAULT_LOADER');
214+
return defaultImportModuleDynamically(specifier, attributes, referrerName);
215+
}
216+
// For script compiled internally that should use the default loader to handle dynamic
217+
// import, proxy the request to the default loader without the warning.
218+
if (referrerSymbol === vm_dynamic_import_default_internal) {
219+
return defaultImportModuleDynamically(specifier, attributes, referrerName);
220+
}
221+
204222
if (moduleRegistries.has(referrerSymbol)) {
205223
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(referrerSymbol);
206224
if (importModuleDynamically !== undefined) {
@@ -275,7 +293,6 @@ async function initializeHooks() {
275293

276294
module.exports = {
277295
registerModule,
278-
registerRealm,
279296
initializeESM,
280297
initializeHooks,
281298
getDefaultConditions,

lib/internal/modules/helpers.js

+28-7
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,19 @@ const { validateString } = require('internal/validators');
2323
const fs = require('fs'); // Import all of `fs` so that it can be monkey-patched.
2424
const internalFS = require('internal/fs/utils');
2525
const path = require('path');
26-
const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
26+
const { pathToFileURL, fileURLToPath } = require('internal/url');
27+
const assert = require('internal/assert');
2728

2829
const { getOptionValue } = require('internal/options');
2930
const { setOwnProperty } = require('internal/util');
31+
const { inspect } = require('internal/util/inspect');
3032

3133
const {
3234
privateSymbols: {
3335
require_private_symbol,
3436
},
3537
} = internalBinding('util');
38+
const { canParse: URLCanParse } = internalBinding('url');
3639

3740
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
3841
debug = fn;
@@ -288,14 +291,32 @@ function addBuiltinLibsToObject(object, dummyModuleName) {
288291
}
289292

290293
/**
291-
* If a referrer is an URL instance or absolute path, convert it into an URL string.
292-
* @param {string | URL} referrer
294+
* Normalize the referrer name as a URL.
295+
* If it's a string containing an absolute path or a URL it's normalized as
296+
* a URL string.
297+
* Otherwise it's returned as undefined.
298+
* @param {string | null | undefined} referrerName
299+
* @returns {string | undefined}
293300
*/
294-
function normalizeReferrerURL(referrer) {
295-
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
296-
return pathToFileURL(referrer).href;
301+
function normalizeReferrerURL(referrerName) {
302+
if (referrerName === null || referrerName === undefined) {
303+
return undefined;
297304
}
298-
return new URL(referrer).href;
305+
306+
if (typeof referrerName === 'string') {
307+
if (path.isAbsolute(referrerName)) {
308+
return pathToFileURL(referrerName).href;
309+
}
310+
311+
if (StringPrototypeStartsWith(referrerName, 'file://') ||
312+
URLCanParse(referrerName)) {
313+
return referrerName;
314+
}
315+
316+
return undefined;
317+
}
318+
319+
assert.fail('Unreachable code reached by ' + inspect(referrerName));
299320
}
300321

301322
module.exports = {

lib/internal/process/pre_execution.js

+17-9
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,31 @@ function prepareWorkerThreadExecution() {
6767
}
6868

6969
function prepareShadowRealmExecution() {
70-
const { registerRealm } = require('internal/modules/esm/utils');
7170
// Patch the process object with legacy properties and normalizations.
7271
// Do not expand argv1 as it is not available in ShadowRealm.
7372
patchProcessObject(false);
7473
setupDebugEnv();
7574

7675
// Disable custom loaders in ShadowRealm.
7776
setupUserModules(true);
78-
registerRealm(globalThis, {
79-
__proto__: null,
80-
importModuleDynamically: (specifier, _referrer, attributes) => {
81-
// The handler for `ShadowRealm.prototype.importValue`.
82-
const { esmLoader } = require('internal/process/esm_loader');
83-
// `parentURL` is not set in the case of a ShadowRealm top-level import.
84-
return esmLoader.import(specifier, undefined, attributes);
77+
const {
78+
privateSymbols: {
79+
host_defined_option_symbol,
8580
},
86-
});
81+
} = internalBinding('util');
82+
const {
83+
vm_dynamic_import_default_internal,
84+
} = internalBinding('symbols');
85+
86+
// For ShadowRealm.prototype.importValue(), the referrer name is
87+
// always null, so the native ImportModuleDynamically() callback would
88+
// always fallback to look up the host-defined option from the
89+
// global object using host_defined_option_symbol. Using
90+
// vm_dynamic_import_default_internal as the host-defined option
91+
// instructs the JS-land importModuleDynamicallyCallback() to
92+
// proxy the request to defaultImportModuleDynamically().
93+
globalThis[host_defined_option_symbol] =
94+
vm_dynamic_import_default_internal;
8795
}
8896

8997
function prepareExecution(options) {

lib/internal/source_map/source_map_cache.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,10 @@ function extractSourceMapURLMagicComment(content) {
107107
function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource, sourceURL, sourceMapURL) {
108108
const sourceMapsEnabled = getSourceMapsEnabled();
109109
if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
110-
try {
111-
const { normalizeReferrerURL } = require('internal/modules/helpers');
112-
filename = normalizeReferrerURL(filename);
113-
} catch (err) {
110+
const { normalizeReferrerURL } = require('internal/modules/helpers');
111+
filename = normalizeReferrerURL(filename);
112+
if (filename === undefined) {
114113
// This is most likely an invalid filename in sourceURL of [eval]-wrapper.
115-
debug(err);
116114
return;
117115
}
118116

0 commit comments

Comments
 (0)