Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1041,9 +1041,11 @@ function getExportsForCircularRequire(module) {
* @param {Module|undefined} parent
* @param {boolean} isMain
* @param {boolean} shouldSkipModuleHooks
* @param {object} [resolveOptions] Options from require.resolve().
* @param {string[]} [resolveOptions.paths] Paths to search for modules in.
* @returns {{url?: string, format?: string, parentURL?: string, filename: string}}
*/
function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks) {
function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks, resolveOptions) {
let defaultResolvedURL;
let defaultResolvedFilename;
let format;
Expand All @@ -1067,7 +1069,7 @@ function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks

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

Expand Down Expand Up @@ -1099,6 +1101,7 @@ function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks
}
defaultResolvedFilename = defaultResolveImpl(specifier, parent, isMain, {
__proto__: null,
paths: resolveOptions?.paths,
conditions: conditionSet,
});

Expand Down
7 changes: 5 additions & 2 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -722,13 +722,16 @@ class ModuleLoader {
* `module.registerHooks()` hooks.
* @param {string} [parentURL] See {@link resolve}.
* @param {ModuleRequest} request See {@link resolve}.
* @param {boolean} [shouldSkipSyncHooks] Whether to skip the synchronous hooks registered by module.registerHooks().
* This is used to maintain compatibility for the re-invented require.resolve (in imported CJS customized
* by module.register()`) which invokes the CJS resolution separately from the hook chain.
* @returns {{ format: string, url: string }}
*/
resolveSync(parentURL, request) {
resolveSync(parentURL, request, shouldSkipSyncHooks = false) {
const specifier = `${request.specifier}`;
const importAttributes = request.attributes ?? kEmptyObject;

if (syncResolveHooks.length) {
if (!shouldSkipSyncHooks && syncResolveHooks.length) {
// Has module.registerHooks() hooks, chain the asynchronous hooks in the default step.
return resolveWithSyncHooks(specifier, parentURL, importAttributes, this.#defaultConditions,
this.#resolveAndMaybeBlockOnLoaderThread.bind(this));
Expand Down
9 changes: 5 additions & 4 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,15 @@ function loadCJSModule(module, source, url, filename, isMain) {
};
setOwnProperty(requireFn, 'resolve', function resolve(specifier) {
if (!StringPrototypeStartsWith(specifier, 'node:')) {
const path = CJSModule._resolveFilename(specifier, module);
if (specifier !== path) {
specifier = `${pathToFileURL(path)}`;
const { filename } = resolveForCJSWithHooks(specifier, module, false, false);
if (specifier !== filename) {
specifier = `${pathToFileURL(filename)}`;
}
}

const request = { specifier, __proto__: null, attributes: kEmptyObject };
const { url: resolvedURL } = cascadedLoader.resolveSync(url, request);
// Skip sync hooks in resolveSync since resolveForCJSWithHooks already ran them above.
const { url: resolvedURL } = cascadedLoader.resolveSync(url, request, /* shouldSkipSyncHooks */ true);
return urlToFilename(resolvedURL);
});
setOwnProperty(requireFn, 'main', process.mainModule);
Expand Down
6 changes: 5 additions & 1 deletion lib/internal/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const {
flushCompileCache,
} = internalBinding('modules');

const lazyCJSLoader = getLazy(() => require('internal/modules/cjs/loader'));
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
debug = fn;
});
Expand Down Expand Up @@ -198,7 +199,10 @@ function makeRequireFunction(mod) {
*/
function resolve(request, options) {
validateString(request, 'request');
return Module._resolveFilename(request, mod, false, options);
const { resolveForCJSWithHooks } = lazyCJSLoader();
const { filename } = resolveForCJSWithHooks(
request, mod, /* isMain */ false, /* shouldSkipModuleHooks */ false, options);
return filename;
}

require.resolve = resolve;
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/module-hooks/require-resolve-caller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Fixture CJS file that calls require.resolve and exports the result.
'use strict';
const resolved = require.resolve('test-require-resolve-hook-target');
module.exports = { resolved };
13 changes: 13 additions & 0 deletions test/fixtures/module-hooks/require-resolve-paths-caller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Fixture CJS file that calls require.resolve with the paths option.
'use strict';
const path = require('path');
const fixturesDir = path.resolve(__dirname, '..', '..');
const nodeModules = path.join(fixturesDir, 'node_modules');

// Use the paths option to resolve 'bar' from the fixtures node_modules.
const resolved = require.resolve('bar', { paths: [fixturesDir] });
const expected = path.join(nodeModules, 'bar.js');
if (resolved !== expected) {
throw new Error(`Expected ${expected}, got ${resolved}`);
}
module.exports = { resolved };
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict';

// This tests that require.resolve() works with builtin redirection
// via resolve hooks registered with module.registerHooks().

require('../common');
const assert = require('assert');
const { registerHooks } = require('module');

const hook = registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === 'assert') {
return {
url: 'node:zlib',
shortCircuit: true,
};
}
return nextResolve(specifier, context);
},
});

const resolved = require.resolve('assert');
assert.strictEqual(resolved, 'zlib');

hook.deregister();
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

// This tests that require.resolve() and require() both go through the same
// resolve hooks registered via module.registerHooks().

require('../common');
const assert = require('assert');
const { registerHooks } = require('module');
const fixtures = require('../common/fixtures');

const redirectedPath = fixtures.path('module-hooks', 'redirected-assert.js');

const resolvedSpecifiers = [];
const hook = registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === 'test-consistency-target') {
resolvedSpecifiers.push(specifier);
return {
url: `file://${redirectedPath}`,
shortCircuit: true,
};
}
return nextResolve(specifier, context);
},
});

const resolveResult = require.resolve('test-consistency-target');
const requireResult = require('test-consistency-target');

assert.strictEqual(resolveResult, redirectedPath);
assert.strictEqual(requireResult.exports_for_test, 'redirected assert');
assert.deepStrictEqual(resolvedSpecifiers, [
'test-consistency-target',
'test-consistency-target',
]);

hook.deregister();
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

// This tests that require.resolve() from a require function returned by
// module.createRequire() goes through resolve hooks registered via
// module.registerHooks().

require('../common');
const assert = require('assert');
const { registerHooks, createRequire } = require('module');
const fixtures = require('../common/fixtures');

const redirectedPath = fixtures.path('module-hooks', 'redirected-assert.js');

const hook = registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === 'test-create-require-resolve-target') {
return {
url: `file://${redirectedPath}`,
shortCircuit: true,
};
}
return nextResolve(specifier, context);
},
});

const customRequire = createRequire(__filename);
const resolved = customRequire.resolve('test-create-require-resolve-target');
assert.strictEqual(resolved, redirectedPath);

hook.deregister();
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

// This tests that require.resolve() falls through to default resolution
// when resolve hooks registered via module.registerHooks() don't override.

require('../common');
const assert = require('assert');
const { registerHooks } = require('module');

const hook = registerHooks({
resolve(specifier, context, nextResolve) {
return nextResolve(specifier, context);
},
});

const resolved = require.resolve('assert');
assert.strictEqual(resolved, 'assert');

hook.deregister();
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

// This tests that require.resolve() in an imported CJS file goes through
// resolve hooks registered via module.registerHooks().

const common = require('../common');
const assert = require('assert');
const { registerHooks } = require('module');
const fixtures = require('../common/fixtures');

const redirectedPath = fixtures.path('module-hooks', 'redirected-assert.js');

const hook = registerHooks({
resolve: common.mustCall((specifier, context, nextResolve) => {
if (specifier === 'test-require-resolve-hook-target') {
return {
url: `file://${redirectedPath}`,
shortCircuit: true,
};
}
return nextResolve(specifier, context);
}, 2),
});

import('../fixtures/module-hooks/require-resolve-caller.js').then(common.mustCall((ns) => {
assert.strictEqual(ns.default.resolved, redirectedPath);
hook.deregister();
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

// This tests that require.resolve() in a CJS file loaded via the re-invented
// require (triggered when module.register() installs an async loader that
// provides source for a CJS file) still goes through resolve hooks registered
// via module.registerHooks().

const common = require('../common');
const assert = require('assert');
const { register, registerHooks } = require('module');
const fixtures = require('../common/fixtures');

const redirectedPath = fixtures.path('module-hooks', 'redirected-assert.js');

// Register an async loader that provides source for CJS files, which triggers
// the re-invented require path.
register(fixtures.fileURL('module-hooks', 'logger-async-hooks.mjs'));

const hook = registerHooks({
resolve: common.mustCall((specifier, context, nextResolve) => {
if (specifier === 'test-require-resolve-hook-target') {
return {
url: `file://${redirectedPath}`,
shortCircuit: true,
};
}
return nextResolve(specifier, context);
}, 2),
});

import('../fixtures/module-hooks/require-resolve-caller.js').then(common.mustCall((ns) => {
assert.strictEqual(ns.default.resolved, redirectedPath);
hook.deregister();
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

// This tests that require.resolve() with the paths option work transparently
// when resolve hooks are registered via module.registerHooks().

require('../common');
const assert = require('assert');
const path = require('path');
const { registerHooks } = require('module');
const fixtures = require('../common/fixtures');

const nodeModules = path.join(fixtures.path(), 'node_modules');
const resolveCallCount = [];

const hook = registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === 'bar') {
resolveCallCount.push(specifier);
}
return nextResolve(specifier, context);
},
});

// require.resolve with paths option should go through hooks and resolve correctly.
const resolved = require.resolve('bar', { paths: [fixtures.path()] });
assert.strictEqual(resolved, path.join(nodeModules, 'bar.js'));
assert.deepStrictEqual(resolveCallCount, ['bar']);

hook.deregister();
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

// This tests that require.resolve() invokes resolve hooks registered
// via module.registerHooks() and can redirect to a different file.

require('../common');
const assert = require('assert');
const { registerHooks } = require('module');
const fixtures = require('../common/fixtures');

const redirectedPath = fixtures.path('module-hooks', 'redirected-assert.js');

const hook = registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === 'test-resolve-target') {
return {
url: `file://${redirectedPath}`,
shortCircuit: true,
};
}
return nextResolve(specifier, context);
},
});

const resolved = require.resolve('test-resolve-target');
assert.strictEqual(resolved, redirectedPath);

hook.deregister();
Loading