Skip to content

Commit 75ec142

Browse files
committed
fixup! module: run require.resolve through module.registerHooks()
1 parent 5e065ef commit 75ec142

File tree

4 files changed

+85
-50
lines changed

4 files changed

+85
-50
lines changed

lib/internal/modules/cjs/loader.js

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,43 +1034,63 @@ function getExportsForCircularRequire(module) {
10341034
return module.exports;
10351035
}
10361036

1037+
10371038
/**
1038-
* Resolve a module request for CommonJS, invoking hooks from module.registerHooks()
1039-
* if necessary.
1039+
* Wraps result of Module._resolveFilename to include additional fields for hooks.
1040+
* See resolveForCJSWithHooks.
10401041
* @param {string} specifier
10411042
* @param {Module|undefined} parent
10421043
* @param {boolean} isMain
1043-
* @param {boolean} shouldSkipModuleHooks
1044-
* @param {object} [resolveOptions] Options from require.resolve().
1045-
* @param {string[]} [resolveOptions.paths] Paths to search for modules in.
1044+
* @param {ResolveFilenameOptions} options
10461045
* @returns {{url?: string, format?: string, parentURL?: string, filename: string}}
10471046
*/
1048-
function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks, resolveOptions) {
1049-
let defaultResolvedURL;
1050-
let defaultResolvedFilename;
1051-
let format;
1052-
1053-
function defaultResolveImpl(specifier, parent, isMain, options) {
1054-
// For backwards compatibility, when encountering requests starting with node:,
1055-
// throw ERR_UNKNOWN_BUILTIN_MODULE on failure or return the normalized ID on success
1056-
// without going into Module._resolveFilename.
1057-
let normalized;
1058-
if (StringPrototypeStartsWith(specifier, 'node:')) {
1059-
normalized = BuiltinModule.normalizeRequirableId(specifier);
1060-
if (!normalized) {
1061-
throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier);
1062-
}
1063-
defaultResolvedURL = specifier;
1064-
format = 'builtin';
1065-
return normalized;
1047+
function wrapResolveFilename(specifier, parent, isMain, options) {
1048+
const filename = Module._resolveFilename(specifier, parent, isMain, options).toString();
1049+
return { __proto__: null, url: undefined, format: undefined, filename };
1050+
}
1051+
1052+
/**
1053+
* See resolveForCJSWithHooks.
1054+
* @param {string} specifier
1055+
* @param {Module|undefined} parent
1056+
* @param {boolean} isMain
1057+
* @param {ResolveFilenameOptions} options
1058+
* @returns {{url?: string, format?: string, parentURL?: string, filename: string}}
1059+
*/
1060+
function defaultResolveImplForCJSLoading(specifier, parent, isMain, options) {
1061+
// For backwards compatibility, when encountering requests starting with node:,
1062+
// throw ERR_UNKNOWN_BUILTIN_MODULE on failure or return the normalized ID on success
1063+
// without going into Module._resolveFilename.
1064+
let normalized;
1065+
if (StringPrototypeStartsWith(specifier, 'node:')) {
1066+
normalized = BuiltinModule.normalizeRequirableId(specifier);
1067+
if (!normalized) {
1068+
throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier);
10661069
}
1067-
return Module._resolveFilename(specifier, parent, isMain, options).toString();
1070+
return { __proto__: null, url: specifier, format: 'builtin', filename: normalized };
10681071
}
1072+
return wrapResolveFilename(specifier, parent, isMain, options);
1073+
}
10691074

1075+
/**
1076+
* Resolve a module request for CommonJS, invoking hooks from module.registerHooks()
1077+
* if necessary.
1078+
* @param {string} specifier
1079+
* @param {Module|undefined} parent
1080+
* @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().
1085+
* @returns {{url?: string, format?: string, parentURL?: string, filename: string}}
1086+
*/
1087+
function resolveForCJSWithHooks(specifier, parent, isMain, internalResolveOptions) {
1088+
const { requireResolveOptions, shouldSkipModuleHooks } = internalResolveOptions;
1089+
const defaultResolveImpl = requireResolveOptions ?
1090+
wrapResolveFilename : defaultResolveImplForCJSLoading;
10701091
// Fast path: no hooks, just return simple results.
10711092
if (!resolveHooks.length || shouldSkipModuleHooks) {
1072-
const filename = defaultResolveImpl(specifier, parent, isMain, resolveOptions);
1073-
return { __proto__: null, url: defaultResolvedURL, filename, format };
1093+
return defaultResolveImpl(specifier, parent, isMain, requireResolveOptions);
10741094
}
10751095

10761096
// Slow path: has hooks, do the URL conversions and invoke hooks with contexts.
@@ -1099,28 +1119,25 @@ function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks
10991119
} else {
11001120
conditionSet = getCjsConditions();
11011121
}
1102-
defaultResolvedFilename = defaultResolveImpl(specifier, parent, isMain, {
1122+
1123+
const result = defaultResolveImpl(specifier, parent, isMain, {
11031124
__proto__: null,
1104-
paths: resolveOptions?.paths,
1125+
paths: requireResolveOptions?.paths,
11051126
conditions: conditionSet,
11061127
});
1128+
// If the default resolver does not return a URL, convert it for the public API.
1129+
result.url ??= convertCJSFilenameToURL(result.filename);
11071130

1108-
defaultResolvedURL = convertCJSFilenameToURL(defaultResolvedFilename);
1109-
return { __proto__: null, url: defaultResolvedURL };
1131+
// Remove filename because it's not part of the public API.
1132+
// TODO(joyeecheung): maybe expose it in the public API to avoid re-conversion for users too.
1133+
return { __proto__: null, url: result.url, format: result.format };
11101134
}
11111135

11121136
const resolveResult = resolveWithHooks(specifier, parentURL, /* importAttributes */ undefined,
11131137
getCjsConditionsArray(), defaultResolve);
1114-
const { url } = resolveResult;
1115-
format = resolveResult.format;
1116-
1117-
let filename;
1118-
if (url === defaultResolvedURL) { // Not overridden, skip the re-conversion.
1119-
filename = defaultResolvedFilename;
1120-
} else {
1121-
filename = convertURLToCJSFilename(url);
1122-
}
1123-
1138+
const { url, format } = resolveResult;
1139+
// Convert the URL from the hook chain back to a filename for internal use.
1140+
const filename = convertURLToCJSFilename(url);
11241141
const result = { __proto__: null, url, format, filename, parentURL };
11251142
debug('resolveForCJSWithHooks', specifier, parent?.id, isMain, shouldSkipModuleHooks, '->', result);
11261143
return result;
@@ -1215,10 +1232,10 @@ function loadBuiltinWithHooks(id, url, format) {
12151232
* @param {string} request Specifier of module to load via `require`
12161233
* @param {Module} parent Absolute path of the module importing the child
12171234
* @param {boolean} isMain Whether the module is the main entry point
1218-
* @param {object|undefined} options Additional options for loading the module
1235+
* @param {object|undefined} internalResolveOptions Additional options for loading the module
12191236
* @returns {object}
12201237
*/
1221-
Module._load = function(request, parent, isMain, options = kEmptyObject) {
1238+
Module._load = function(request, parent, isMain, internalResolveOptions = kEmptyObject) {
12221239
let relResolveCacheIdentifier;
12231240
if (parent) {
12241241
debug('Module._load REQUEST %s parent: %s', request, parent.id);
@@ -1241,7 +1258,7 @@ Module._load = function(request, parent, isMain, options = kEmptyObject) {
12411258
}
12421259
}
12431260

1244-
const resolveResult = resolveForCJSWithHooks(request, parent, isMain, options.shouldSkipModuleHooks);
1261+
const resolveResult = resolveForCJSWithHooks(request, parent, isMain, internalResolveOptions);
12451262
let { format } = resolveResult;
12461263
const { url, filename } = resolveResult;
12471264

lib/internal/modules/esm/translators.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ translators.set('module', function moduleStrategy(url, translateContext, parentU
9494

9595
const { requestTypes: { kRequireInImportedCJS } } = require('internal/modules/esm/utils');
9696
const kShouldSkipModuleHooks = { __proto__: null, shouldSkipModuleHooks: true };
97+
const kShouldNotSkipModuleHooks = { __proto__: null, shouldSkipModuleHooks: false };
98+
9799
/**
98100
* Loads a CommonJS module via the ESM Loader sync CommonJS translator.
99101
* This translator creates its own version of the `require` function passed into CommonJS modules.
@@ -164,9 +166,11 @@ function loadCJSModule(module, source, url, filename, isMain) {
164166
};
165167
setOwnProperty(requireFn, 'resolve', function resolve(specifier) {
166168
if (!StringPrototypeStartsWith(specifier, 'node:')) {
167-
const { filename } = resolveForCJSWithHooks(specifier, module, false, false);
169+
const {
170+
filename, url: resolvedURL,
171+
} = resolveForCJSWithHooks(specifier, module, false, kShouldNotSkipModuleHooks);
168172
if (specifier !== filename) {
169-
specifier = `${pathToFileURL(filename)}`;
173+
specifier = resolvedURL ?? `${pathToFileURL(filename)}`;
170174
}
171175
}
172176

@@ -402,7 +406,7 @@ function cjsPreparseModuleExports(filename, source, format) {
402406
let resolved;
403407
let format;
404408
try {
405-
({ format, filename: resolved } = resolveForCJSWithHooks(reexport, module, false));
409+
({ format, filename: resolved } = resolveForCJSWithHooks(reexport, module, false, kShouldNotSkipModuleHooks));
406410
} catch (e) {
407411
debug(`Failed to resolve '${reexport}', skipping`, e);
408412
continue;

lib/internal/modules/helpers.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,21 @@ function makeRequireFunction(mod) {
200200
function resolve(request, options) {
201201
validateString(request, 'request');
202202
const { resolveForCJSWithHooks } = lazyCJSLoader();
203-
const { filename } = resolveForCJSWithHooks(
204-
request, mod, /* isMain */ false, /* shouldSkipModuleHooks */ false, options);
203+
// require.resolve() has different behaviors from the internal resolution used by
204+
// Module._load:
205+
// 1. When the request resolves to a non-existent built-in, it throws MODULE_NOT_FOUND
206+
// instead of UNKNOWN_BUILTIN_MODULE. This is handled by resolveForCJSWithHooks.
207+
// 2. If the request is a prefixed built-in, the returned value is also prefixed. This
208+
// is handled below.
209+
const { filename, url } = resolveForCJSWithHooks(
210+
request, mod, /* isMain */ false, {
211+
__proto__: null,
212+
shouldSkipModuleHooks: false,
213+
requireResolveOptions: options ?? {},
214+
});
215+
if (url === request && StringPrototypeStartsWith(request, 'node:')) {
216+
return url;
217+
}
205218
return filename;
206219
}
207220

test/parallel/test-repl.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -608,17 +608,18 @@ const errorTests = [
608608
// access to internal modules without the --expose-internals flag.
609609
{
610610
// Shrink the stack trace to avoid having to update this test whenever the
611-
// implementation of require() changes. It's set to 4 because somehow setting it
611+
// implementation of require() changes. It's set to 5 because somehow setting it
612612
// to a lower value breaks the error formatting and the message becomes
613613
// "Uncaught [Error...", which is probably a bug(?).
614-
send: 'Error.stackTraceLimit = 4; require("internal/repl")',
614+
send: 'Error.stackTraceLimit = 5; require("internal/repl")',
615615
expect: [
616616
/^Uncaught Error: Cannot find module 'internal\/repl'/,
617617
/^Require stack:/,
618618
/^- <repl>/, // This just tests MODULE_NOT_FOUND so let's skip the stack trace
619619
/^ {4}at .*/, // Some stack frame that we have to capture otherwise error message is buggy.
620620
/^ {4}at .*/, // Some stack frame that we have to capture otherwise error message is buggy.
621621
/^ {4}at .*/, // Some stack frame that we have to capture otherwise error message is buggy.
622+
/^ {4}at .*/, // Some stack frame that we have to capture otherwise error message is buggy.
622623
" code: 'MODULE_NOT_FOUND',",
623624
" requireStack: [ '<repl>' ]",
624625
'}',

0 commit comments

Comments
 (0)