Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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