Skip to content

Commit fc373a6

Browse files
committed
module: fix sync hook short-circuit in require() in imported CJS
- For imported CJS, if it's not customized by asynchronous hooks, make sure it won't use the quirky re-invented require in all cases. - When the imported CJS module is customized by synchronous hooks, in the synthetic module evalutation step, avoid calling the respective default step again. - Make the branching of loadCJSModuleWithModuleLoad() and loadCJSModuleWithSpecialRequire() more explicit, and fold the tentative fs read in the 'commonjs' translator into the share createCJSModuleWrap() helper instead of checking it twice in the same path.
1 parent acb1bd7 commit fc373a6

6 files changed

Lines changed: 261 additions & 86 deletions

File tree

lib/internal/modules/cjs/loader.js

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,20 +1072,26 @@ function defaultResolveImplForCJSLoading(specifier, parent, isMain, options) {
10721072
return wrapResolveFilename(specifier, parent, isMain, options);
10731073
}
10741074

1075+
/**
1076+
* @typedef {{
1077+
* resolved?: {url?: string, format?: string, filename: string},
1078+
* shouldSkipModuleHooks?: boolean,
1079+
* source?: string|ArrayBufferView|ArrayBuffer,
1080+
* requireResolveOptions?: ResolveFilenameOptions,
1081+
* }} CJSModuleLoadInternalOptions
1082+
*/
1083+
10751084
/**
10761085
* Resolve a module request for CommonJS, invoking hooks from module.registerHooks()
10771086
* if necessary.
10781087
* @param {string} specifier
10791088
* @param {Module|undefined} parent
10801089
* @param {boolean} isMain
1081-
* @param {object} internalResolveOptions
1082-
* @param {boolean} internalResolveOptions.shouldSkipModuleHooks Whether to skip module hooks.
1083-
* @param {ResolveFilenameOptions} internalResolveOptions.requireResolveOptions Options from require.resolve().
1084-
* Only used when it comes from require.resolve().
1090+
* @param {CJSModuleLoadInternalOptions} internalOptions
10851091
* @returns {{url?: string, format?: string, parentURL?: string, filename: string}}
10861092
*/
1087-
function resolveForCJSWithHooks(specifier, parent, isMain, internalResolveOptions) {
1088-
const { requireResolveOptions, shouldSkipModuleHooks } = internalResolveOptions;
1093+
function resolveForCJSWithHooks(specifier, parent, isMain, internalOptions) {
1094+
const { requireResolveOptions, shouldSkipModuleHooks } = internalOptions;
10891095
const defaultResolveImpl = requireResolveOptions ?
10901096
wrapResolveFilename : defaultResolveImplForCJSLoading;
10911097
// Fast path: no hooks, just return simple results.
@@ -1232,10 +1238,10 @@ function loadBuiltinWithHooks(id, url, format) {
12321238
* @param {string} request Specifier of module to load via `require`
12331239
* @param {Module} parent Absolute path of the module importing the child
12341240
* @param {boolean} isMain Whether the module is the main entry point
1235-
* @param {object|undefined} internalResolveOptions Additional options for loading the module
1241+
* @param {CJSModuleLoadInternalOptions|undefined} internalOptions Additional options for loading the module
12361242
* @returns {object}
12371243
*/
1238-
Module._load = function(request, parent, isMain, internalResolveOptions = kEmptyObject) {
1244+
Module._load = function(request, parent, isMain, internalOptions = kEmptyObject) {
12391245
let relResolveCacheIdentifier;
12401246
if (parent) {
12411247
debug('Module._load REQUEST %s parent: %s', request, parent.id);
@@ -1258,7 +1264,10 @@ Module._load = function(request, parent, isMain, internalResolveOptions = kEmpty
12581264
}
12591265
}
12601266

1261-
const resolveResult = resolveForCJSWithHooks(request, parent, isMain, internalResolveOptions);
1267+
// If the module has been resolved by a short-circuiting synchronous resolve hook,
1268+
// avoid running the default resolution from disk again.
1269+
const resolveResult = internalOptions.resolved ??
1270+
resolveForCJSWithHooks(request, parent, isMain, internalOptions);
12621271
let { format } = resolveResult;
12631272
const { url, filename } = resolveResult;
12641273

@@ -1345,6 +1354,14 @@ Module._load = function(request, parent, isMain, internalResolveOptions = kEmpty
13451354
module[kLastModuleParent] = parent;
13461355
}
13471356

1357+
// The module source was provided by a short-circuiting synchronous hook,
1358+
// assign them into the module to avoid triggering the default load step again.
1359+
if (internalOptions.source !== undefined) {
1360+
module[kModuleSource] ??= internalOptions.source;
1361+
module[kURL] ??= url;
1362+
module[kFormat] ??= format;
1363+
}
1364+
13481365
if (parent !== undefined) {
13491366
relativeResolveCache[relResolveCacheIdentifier] = filename;
13501367
}

lib/internal/modules/esm/load.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ function defaultLoadSync(url, context = kEmptyObject) {
145145

146146
throwIfUnsupportedURLScheme(urlInstance, false);
147147

148-
let shouldBeReloadedByCJSLoader = false;
149148
if (urlInstance.protocol === 'node:') {
150149
source = null;
151150
format ??= 'builtin';
@@ -160,10 +159,6 @@ function defaultLoadSync(url, context = kEmptyObject) {
160159

161160
// Now that we have the source for the module, run `defaultGetFormat` to detect its format.
162161
format ??= defaultGetFormat(urlInstance, context);
163-
164-
// For backward compatibility reasons, we need to let go through Module._load
165-
// again.
166-
shouldBeReloadedByCJSLoader = (format === 'commonjs');
167162
}
168163
validateAttributes(url, format, importAttributes);
169164

@@ -172,7 +167,6 @@ function defaultLoadSync(url, context = kEmptyObject) {
172167
format,
173168
responseURL,
174169
source,
175-
shouldBeReloadedByCJSLoader,
176170
};
177171
}
178172

lib/internal/modules/esm/loader.js

Lines changed: 100 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,19 @@ const { defaultLoadSync, throwUnknownModuleFormat } = require('internal/modules/
124124
*/
125125

126126
/**
127-
* @typedef {{ format: ModuleFormat, source: ModuleSource, translatorKey: string }} TranslateContext
127+
* @typedef {{format: string, url: string, isResolvedBySyncHooks: boolean}} ResolveResult
128+
*/
129+
130+
/**
131+
* @typedef {{
132+
* url: string,
133+
* format: ModuleFormat,
134+
* source: ModuleSource,
135+
* responseURL?: string,
136+
* translatorKey: string,
137+
* isResolvedBySyncHooks: boolean,
138+
* isSourceLoadedSynchronously: boolean,
139+
* }} TranslateContext
128140
*/
129141

130142
/**
@@ -385,19 +397,25 @@ class ModuleLoader {
385397
/**
386398
* Load a module and translate it into a ModuleWrap for require(esm).
387399
* This is run synchronously, and the translator always return a ModuleWrap synchronously.
388-
* @param {string} url URL of the module to be translated.
400+
* @param {ResolveResult} resolveResult Result from the resolve step.
389401
* @param {object} loadContext See {@link load}
390402
* @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point.
391403
* @param {ModuleRequest} request Module request.
392404
* @returns {ModuleWrap}
393405
*/
394-
loadAndTranslateForImportInRequiredESM(url, loadContext, parentURL, request) {
406+
loadAndTranslateForImportInRequiredESM(resolveResult, loadContext, parentURL, request) {
407+
const { url } = resolveResult;
395408
const loadResult = this.#loadSync(url, loadContext);
396409
// Use the synchronous commonjs translator which can deal with cycles.
397410
const formatFromLoad = loadResult.format;
398411
const translatorKey = (formatFromLoad === 'commonjs' || formatFromLoad === 'commonjs-typescript') ?
399412
'commonjs-sync' : formatFromLoad;
400-
const translateContext = { ...loadResult, translatorKey, __proto__: null };
413+
const translateContext = {
414+
...resolveResult,
415+
...loadResult,
416+
translatorKey,
417+
__proto__: null,
418+
};
401419
const wrap = this.#translate(url, translateContext, parentURL);
402420
assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`);
403421

@@ -446,12 +464,13 @@ class ModuleLoader {
446464
/**
447465
* Load a module and translate it into a ModuleWrap for require() in imported CJS.
448466
* This is run synchronously, and the translator always return a ModuleWrap synchronously.
449-
* @param {string} url URL of the module to be translated.
467+
* @param {ResolveResult} resolveResult Result from the resolve step.
450468
* @param {object} loadContext See {@link load}
451469
* @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point.
452470
* @returns {ModuleWrap}
453471
*/
454-
loadAndTranslateForRequireInImportedCJS(url, loadContext, parentURL) {
472+
loadAndTranslateForRequireInImportedCJS(resolveResult, loadContext, parentURL) {
473+
const { url } = resolveResult;
455474
const loadResult = this.#loadSync(url, loadContext);
456475
const formatFromLoad = loadResult.format;
457476

@@ -473,7 +492,12 @@ class ModuleLoader {
473492
translatorKey = 'require-commonjs-typescript';
474493
}
475494

476-
const translateContext = { ...loadResult, translatorKey, __proto__: null };
495+
const translateContext = {
496+
...resolveResult,
497+
...loadResult,
498+
translatorKey,
499+
__proto__: null,
500+
};
477501
const wrap = this.#translate(url, translateContext, parentURL);
478502
assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`);
479503
return wrap;
@@ -482,15 +506,21 @@ class ModuleLoader {
482506
/**
483507
* Load a module and translate it into a ModuleWrap for ordinary imported ESM.
484508
* This may be run asynchronously if there are asynchronous module loader hooks registered.
485-
* @param {string} url URL of the module to be translated.
509+
* @param {ResolveResult} resolveResult Result from the resolve step.
486510
* @param {object} loadContext See {@link load}
487511
* @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point.
488512
* @returns {Promise<ModuleWrap>|ModuleWrap}
489513
*/
490-
loadAndTranslate(url, loadContext, parentURL) {
514+
loadAndTranslate(resolveResult, loadContext, parentURL) {
515+
const { url } = resolveResult;
491516
const maybePromise = this.load(url, loadContext);
492517
const afterLoad = (loadResult) => {
493-
const translateContext = { ...loadResult, translatorKey: loadResult.format, __proto__: null };
518+
const translateContext = {
519+
...resolveResult,
520+
...loadResult,
521+
translatorKey: loadResult.format,
522+
__proto__: null,
523+
};
494524
return this.#translate(url, translateContext, parentURL);
495525
};
496526
if (isPromise(maybePromise)) {
@@ -506,7 +536,7 @@ class ModuleLoader {
506536
* the module should be linked by the time this returns. Otherwise it may still have
507537
* pending module requests.
508538
* @param {string} parentURL See {@link getOrCreateModuleJob}
509-
* @param {{format: string, url: string}} resolveResult
539+
* @param {ResolveResult} resolveResult
510540
* @param {ModuleRequest} request Module request.
511541
* @param {ModuleRequestType} requestType Type of the module request.
512542
* @returns {ModuleJobBase} The (possibly pending) module job
@@ -535,11 +565,11 @@ class ModuleLoader {
535565

536566
let moduleOrModulePromise;
537567
if (requestType === kRequireInImportedCJS) {
538-
moduleOrModulePromise = this.loadAndTranslateForRequireInImportedCJS(url, context, parentURL);
568+
moduleOrModulePromise = this.loadAndTranslateForRequireInImportedCJS(resolveResult, context, parentURL);
539569
} else if (requestType === kImportInRequiredESM) {
540-
moduleOrModulePromise = this.loadAndTranslateForImportInRequiredESM(url, context, parentURL, request);
570+
moduleOrModulePromise = this.loadAndTranslateForImportInRequiredESM(resolveResult, context, parentURL, request);
541571
} else {
542-
moduleOrModulePromise = this.loadAndTranslate(url, context, parentURL);
572+
moduleOrModulePromise = this.loadAndTranslate(resolveResult, context, parentURL);
543573
}
544574

545575
if (requestType === kImportInRequiredESM || requestType === kRequireInImportedCJS ||
@@ -653,7 +683,7 @@ class ModuleLoader {
653683
* @param {string} [parentURL] The URL of the module where the module request is initiated.
654684
* It's undefined if it's from the root module.
655685
* @param {ModuleRequest} request Module request.
656-
* @returns {Promise<{format: string, url: string}>|{format: string, url: string}}
686+
* @returns {Promise<ResolveResult>|ResolveResult}
657687
*/
658688
#resolve(parentURL, request) {
659689
if (this.isForAsyncLoaderHookWorker) {
@@ -689,15 +719,18 @@ class ModuleLoader {
689719
/**
690720
* This is the default resolve step for module.registerHooks(), which incorporates asynchronous hooks
691721
* from module.register() which are run in a blocking fashion for it to be synchronous.
722+
* @param {{isResolvedByDefaultResolve: boolean}} out Output object to track whether the default resolve was used
723+
* without polluting the user-visible resolve result.
692724
* @param {string|URL} specifier See {@link resolveSync}.
693725
* @param {{ parentURL?: string, importAttributes: ImportAttributes, conditions?: string[]}} context
694726
* See {@link resolveSync}.
695727
* @returns {{ format: string, url: string }}
696728
*/
697-
#resolveAndMaybeBlockOnLoaderThread(specifier, context) {
729+
#resolveAndMaybeBlockOnLoaderThread(out, specifier, context) {
698730
if (this.#asyncLoaderHooks?.resolveSync) {
699731
return this.#asyncLoaderHooks.resolveSync(specifier, context.parentURL, context.importAttributes);
700732
}
733+
out.isResolvedByDefaultResolve = true;
701734
return this.#cachedDefaultResolve(specifier, context);
702735
}
703736

@@ -712,31 +745,45 @@ class ModuleLoader {
712745
* @param {boolean} [shouldSkipSyncHooks] Whether to skip the synchronous hooks registered by module.registerHooks().
713746
* This is used to maintain compatibility for the re-invented require.resolve (in imported CJS customized
714747
* by module.register()`) which invokes the CJS resolution separately from the hook chain.
715-
* @returns {{ format: string, url: string }}
748+
* @returns {ResolveResult}
716749
*/
717750
resolveSync(parentURL, request, shouldSkipSyncHooks = false) {
718751
const specifier = `${request.specifier}`;
719752
const importAttributes = request.attributes ?? kEmptyObject;
753+
// Use an output parameter to track the state and avoid polluting the user-visible resolve results.
754+
const out = { isResolvedByDefaultResolve: false, __proto__: null };
720755

756+
let result;
757+
let isResolvedBySyncHooks = false;
721758
if (!shouldSkipSyncHooks && syncResolveHooks.length) {
722759
// Has module.registerHooks() hooks, chain the asynchronous hooks in the default step.
723-
return resolveWithSyncHooks(specifier, parentURL, importAttributes, this.#defaultConditions,
724-
this.#resolveAndMaybeBlockOnLoaderThread.bind(this));
760+
result = resolveWithSyncHooks(specifier, parentURL, importAttributes, this.#defaultConditions,
761+
this.#resolveAndMaybeBlockOnLoaderThread.bind(this, out));
762+
// If the default step ran, sync hooks did not short-circuit the resolution.
763+
isResolvedBySyncHooks = !out.isResolvedByDefaultResolve;
764+
} else {
765+
const context = {
766+
...request,
767+
conditions: this.#defaultConditions,
768+
parentURL,
769+
importAttributes,
770+
__proto__: null,
771+
};
772+
result = this.#resolveAndMaybeBlockOnLoaderThread(out, specifier, context);
725773
}
726-
const context = {
727-
...request,
728-
conditions: this.#defaultConditions,
729-
parentURL,
730-
importAttributes,
731-
__proto__: null,
732-
};
733-
return this.#resolveAndMaybeBlockOnLoaderThread(specifier, context);
774+
result.isResolvedBySyncHooks = isResolvedBySyncHooks;
775+
return result;
734776
}
735777

736778
/**
737779
* Provide source that is understood by one of Node's translators. Handles customization hooks,
738780
* if any.
739-
* @typedef { {format: ModuleFormat, source: ModuleSource }} LoadResult
781+
* @typedef {{
782+
* format: ModuleFormat,
783+
* source: ModuleSource,
784+
* responseURL?: string,
785+
* isSourceLoadedSynchronously: boolean,
786+
* }} LoadResult
740787
* @param {string} url The URL of the module to be loaded.
741788
* @param {object} context Metadata about the module
742789
* @returns {Promise<LoadResult> | LoadResult}}
@@ -752,14 +799,19 @@ class ModuleLoader {
752799
/**
753800
* This is the default load step for module.registerHooks(), which incorporates asynchronous hooks
754801
* from module.register() which are run in a blocking fashion for it to be synchronous.
802+
* @param {{isSourceLoadedSynchronously: boolean}} out
803+
* Output object to track whether the source was loaded synchronously without polluting
804+
* the user-visible load result.
755805
* @param {string} url See {@link load}
756806
* @param {object} context See {@link load}
757807
* @returns {{ format: ModuleFormat, source: ModuleSource }}
758808
*/
759-
#loadAndMaybeBlockOnLoaderThread(url, context) {
809+
#loadAndMaybeBlockOnLoaderThread(out, url, context) {
760810
if (this.#asyncLoaderHooks?.loadSync) {
811+
out.isSourceLoadedSynchronously = false;
761812
return this.#asyncLoaderHooks.loadSync(url, context);
762813
}
814+
out.isSourceLoadedSynchronously = true;
763815
return defaultLoadSync(url, context);
764816
}
765817

@@ -770,17 +822,32 @@ class ModuleLoader {
770822
* This is here to support `require()` in imported CJS and `module.registerHooks()` hooks.
771823
* @param {string} url See {@link load}
772824
* @param {object} [context] See {@link load}
773-
* @returns {{ format: ModuleFormat, source: ModuleSource }}
825+
* @returns {LoadResult}
774826
*/
775827
#loadSync(url, context) {
828+
// Use an output parameter to track the state and avoid polluting the user-visible resolve results.
829+
const out = {
830+
isSourceLoadedSynchronously: true,
831+
__proto__: null,
832+
};
833+
let result;
776834
if (syncLoadHooks.length) {
777835
// Has module.registerHooks() hooks, chain the asynchronous hooks in the default step.
778836
// TODO(joyeecheung): construct the ModuleLoadContext in the loaders directly instead
779837
// of converting them from plain objects in the hooks.
780-
return loadWithSyncHooks(url, context.format, context.importAttributes, this.#defaultConditions,
781-
this.#loadAndMaybeBlockOnLoaderThread.bind(this), validateLoadSloppy);
838+
result = loadWithSyncHooks(
839+
url,
840+
context.format,
841+
context.importAttributes,
842+
this.#defaultConditions,
843+
this.#loadAndMaybeBlockOnLoaderThread.bind(this, out),
844+
validateLoadSloppy,
845+
);
846+
} else {
847+
result = this.#loadAndMaybeBlockOnLoaderThread(out, url, context);
782848
}
783-
return this.#loadAndMaybeBlockOnLoaderThread(url, context);
849+
result.isSourceLoadedSynchronously = out.isSourceLoadedSynchronously;
850+
return result;
784851
}
785852

786853
validateLoadResult(url, format) {

0 commit comments

Comments
 (0)