Skip to content

Commit 5e065ef

Browse files
committed
module: run require.resolve through module.registerHooks()
Previously, require.resolve() called Module._resolveFilename() directly, bypassing any resolve hooks registered via module.registerHooks(). This patch fixes that.
1 parent 787aeff commit 5e065ef

14 files changed

+267
-9
lines changed

lib/internal/modules/cjs/loader.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,9 +1041,11 @@ function getExportsForCircularRequire(module) {
10411041
* @param {Module|undefined} parent
10421042
* @param {boolean} isMain
10431043
* @param {boolean} shouldSkipModuleHooks
1044+
* @param {object} [resolveOptions] Options from require.resolve().
1045+
* @param {string[]} [resolveOptions.paths] Paths to search for modules in.
10441046
* @returns {{url?: string, format?: string, parentURL?: string, filename: string}}
10451047
*/
1046-
function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks) {
1048+
function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks, resolveOptions) {
10471049
let defaultResolvedURL;
10481050
let defaultResolvedFilename;
10491051
let format;
@@ -1067,7 +1069,7 @@ function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks
10671069

10681070
// Fast path: no hooks, just return simple results.
10691071
if (!resolveHooks.length || shouldSkipModuleHooks) {
1070-
const filename = defaultResolveImpl(specifier, parent, isMain);
1072+
const filename = defaultResolveImpl(specifier, parent, isMain, resolveOptions);
10711073
return { __proto__: null, url: defaultResolvedURL, filename, format };
10721074
}
10731075

@@ -1099,6 +1101,7 @@ function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks
10991101
}
11001102
defaultResolvedFilename = defaultResolveImpl(specifier, parent, isMain, {
11011103
__proto__: null,
1104+
paths: resolveOptions?.paths,
11021105
conditions: conditionSet,
11031106
});
11041107

lib/internal/modules/esm/loader.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -722,13 +722,16 @@ class ModuleLoader {
722722
* `module.registerHooks()` hooks.
723723
* @param {string} [parentURL] See {@link resolve}.
724724
* @param {ModuleRequest} request See {@link resolve}.
725+
* @param {boolean} [shouldSkipSyncHooks] Whether to skip the synchronous hooks registered by module.registerHooks().
726+
* This is used to maintain compatibility for the re-invented require.resolve (in imported CJS customized
727+
* by module.register()`) which invokes the CJS resolution separately from the hook chain.
725728
* @returns {{ format: string, url: string }}
726729
*/
727-
resolveSync(parentURL, request) {
730+
resolveSync(parentURL, request, shouldSkipSyncHooks = false) {
728731
const specifier = `${request.specifier}`;
729732
const importAttributes = request.attributes ?? kEmptyObject;
730733

731-
if (syncResolveHooks.length) {
734+
if (!shouldSkipSyncHooks && syncResolveHooks.length) {
732735
// Has module.registerHooks() hooks, chain the asynchronous hooks in the default step.
733736
return resolveWithSyncHooks(specifier, parentURL, importAttributes, this.#defaultConditions,
734737
this.#resolveAndMaybeBlockOnLoaderThread.bind(this));

lib/internal/modules/esm/translators.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,15 @@ function loadCJSModule(module, source, url, filename, isMain) {
164164
};
165165
setOwnProperty(requireFn, 'resolve', function resolve(specifier) {
166166
if (!StringPrototypeStartsWith(specifier, 'node:')) {
167-
const path = CJSModule._resolveFilename(specifier, module);
168-
if (specifier !== path) {
169-
specifier = `${pathToFileURL(path)}`;
167+
const { filename } = resolveForCJSWithHooks(specifier, module, false, false);
168+
if (specifier !== filename) {
169+
specifier = `${pathToFileURL(filename)}`;
170170
}
171171
}
172172

173173
const request = { specifier, __proto__: null, attributes: kEmptyObject };
174-
const { url: resolvedURL } = cascadedLoader.resolveSync(url, request);
174+
// Skip sync hooks in resolveSync since resolveForCJSWithHooks already ran them above.
175+
const { url: resolvedURL } = cascadedLoader.resolveSync(url, request, /* shouldSkipSyncHooks */ true);
175176
return urlToFilename(resolvedURL);
176177
});
177178
setOwnProperty(requireFn, 'main', process.mainModule);

lib/internal/modules/helpers.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const {
4242
flushCompileCache,
4343
} = internalBinding('modules');
4444

45+
const lazyCJSLoader = getLazy(() => require('internal/modules/cjs/loader'));
4546
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
4647
debug = fn;
4748
});
@@ -198,7 +199,10 @@ function makeRequireFunction(mod) {
198199
*/
199200
function resolve(request, options) {
200201
validateString(request, 'request');
201-
return Module._resolveFilename(request, mod, false, options);
202+
const { resolveForCJSWithHooks } = lazyCJSLoader();
203+
const { filename } = resolveForCJSWithHooks(
204+
request, mod, /* isMain */ false, /* shouldSkipModuleHooks */ false, options);
205+
return filename;
202206
}
203207

204208
require.resolve = resolve;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Fixture CJS file that calls require.resolve and exports the result.
2+
'use strict';
3+
const resolved = require.resolve('test-require-resolve-hook-target');
4+
module.exports = { resolved };
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Fixture CJS file that calls require.resolve with the paths option.
2+
'use strict';
3+
const path = require('path');
4+
const fixturesDir = path.resolve(__dirname, '..', '..');
5+
const nodeModules = path.join(fixturesDir, 'node_modules');
6+
7+
// Use the paths option to resolve 'bar' from the fixtures node_modules.
8+
const resolved = require.resolve('bar', { paths: [fixturesDir] });
9+
const expected = path.join(nodeModules, 'bar.js');
10+
if (resolved !== expected) {
11+
throw new Error(`Expected ${expected}, got ${resolved}`);
12+
}
13+
module.exports = { resolved };
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict';
2+
3+
// This tests that require.resolve() works with builtin redirection
4+
// via resolve hooks registered with module.registerHooks().
5+
6+
require('../common');
7+
const assert = require('assert');
8+
const { registerHooks } = require('module');
9+
10+
const hook = registerHooks({
11+
resolve(specifier, context, nextResolve) {
12+
if (specifier === 'assert') {
13+
return {
14+
url: 'node:zlib',
15+
shortCircuit: true,
16+
};
17+
}
18+
return nextResolve(specifier, context);
19+
},
20+
});
21+
22+
const resolved = require.resolve('assert');
23+
assert.strictEqual(resolved, 'zlib');
24+
25+
hook.deregister();
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
3+
// This tests that require.resolve() and require() both go through the same
4+
// resolve hooks registered via module.registerHooks().
5+
6+
require('../common');
7+
const assert = require('assert');
8+
const { registerHooks } = require('module');
9+
const fixtures = require('../common/fixtures');
10+
11+
const redirectedPath = fixtures.path('module-hooks', 'redirected-assert.js');
12+
13+
const resolvedSpecifiers = [];
14+
const hook = registerHooks({
15+
resolve(specifier, context, nextResolve) {
16+
if (specifier === 'test-consistency-target') {
17+
resolvedSpecifiers.push(specifier);
18+
return {
19+
url: `file://${redirectedPath}`,
20+
shortCircuit: true,
21+
};
22+
}
23+
return nextResolve(specifier, context);
24+
},
25+
});
26+
27+
const resolveResult = require.resolve('test-consistency-target');
28+
const requireResult = require('test-consistency-target');
29+
30+
assert.strictEqual(resolveResult, redirectedPath);
31+
assert.strictEqual(requireResult.exports_for_test, 'redirected assert');
32+
assert.deepStrictEqual(resolvedSpecifiers, [
33+
'test-consistency-target',
34+
'test-consistency-target',
35+
]);
36+
37+
hook.deregister();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
3+
// This tests that require.resolve() from a require function returned by
4+
// module.createRequire() goes through resolve hooks registered via
5+
// module.registerHooks().
6+
7+
require('../common');
8+
const assert = require('assert');
9+
const { registerHooks, createRequire } = require('module');
10+
const fixtures = require('../common/fixtures');
11+
12+
const redirectedPath = fixtures.path('module-hooks', 'redirected-assert.js');
13+
14+
const hook = registerHooks({
15+
resolve(specifier, context, nextResolve) {
16+
if (specifier === 'test-create-require-resolve-target') {
17+
return {
18+
url: `file://${redirectedPath}`,
19+
shortCircuit: true,
20+
};
21+
}
22+
return nextResolve(specifier, context);
23+
},
24+
});
25+
26+
const customRequire = createRequire(__filename);
27+
const resolved = customRequire.resolve('test-create-require-resolve-target');
28+
assert.strictEqual(resolved, redirectedPath);
29+
30+
hook.deregister();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
3+
// This tests that require.resolve() falls through to default resolution
4+
// when resolve hooks registered via module.registerHooks() don't override.
5+
6+
require('../common');
7+
const assert = require('assert');
8+
const { registerHooks } = require('module');
9+
10+
const hook = registerHooks({
11+
resolve(specifier, context, nextResolve) {
12+
return nextResolve(specifier, context);
13+
},
14+
});
15+
16+
const resolved = require.resolve('assert');
17+
assert.strictEqual(resolved, 'assert');
18+
19+
hook.deregister();

0 commit comments

Comments
 (0)