Skip to content

Commit 410d2b6

Browse files
committed
module: fix conditions override in synchronous resolve hooks
1. Make sure that the conditions are converted into arrays when being passed into user hooks. 2. Pass the conditions from user hooks into the ESM resolution so that it takes effect.
1 parent 1effb26 commit 410d2b6

File tree

12 files changed

+276
-31
lines changed

12 files changed

+276
-31
lines changed

lib/internal/modules/cjs/loader.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const {
5151
ReflectSet,
5252
RegExpPrototypeExec,
5353
SafeMap,
54+
SafeSet,
5455
String,
5556
StringPrototypeCharAt,
5657
StringPrototypeCharCodeAt,
@@ -154,6 +155,7 @@ const internalFsBinding = internalBinding('fs');
154155
const { safeGetenv } = internalBinding('credentials');
155156
const {
156157
getCjsConditions,
158+
getCjsConditionsArray,
157159
initializeCjsConditions,
158160
loadBuiltinModule,
159161
makeRequireFunction,
@@ -635,6 +637,7 @@ const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/;
635637
* Resolves the exports for a given module path and request.
636638
* @param {string} nmPath The path to the module.
637639
* @param {string} request The request for the module.
640+
* @param {Set<string>} conditions The conditions to use for resolution.
638641
*/
639642
function resolveExports(nmPath, request, conditions) {
640643
// The implementation's behavior is meant to mirror resolution in ESM.
@@ -1043,17 +1046,30 @@ function resolveForCJSWithHooks(specifier, parent, isMain) {
10431046
function defaultResolve(specifier, context) {
10441047
// TODO(joyeecheung): parent and isMain should be part of context, then we
10451048
// no longer need to use a different defaultResolve for every resolution.
1049+
// In the hooks, context.conditions is passed around as an array, but internally
1050+
// the resolution helpers expect a SafeSet. Do the conversion here.
1051+
let conditionSet;
1052+
const conditions = context.conditions;
1053+
if (conditions !== undefined && conditions !== getCjsConditionsArray()) {
1054+
if (!ArrayIsArray(conditions)) {
1055+
throw new ERR_INVALID_ARG_VALUE('context.conditions', conditions,
1056+
'expected an array');
1057+
}
1058+
conditionSet = new SafeSet(conditions);
1059+
} else {
1060+
conditionSet = getCjsConditions();
1061+
}
10461062
defaultResolvedFilename = defaultResolveImpl(specifier, parent, isMain, {
10471063
__proto__: null,
1048-
conditions: context.conditions,
1064+
conditions: conditionSet,
10491065
});
10501066

10511067
defaultResolvedURL = convertCJSFilenameToURL(defaultResolvedFilename);
10521068
return { __proto__: null, url: defaultResolvedURL };
10531069
}
10541070

10551071
const resolveResult = resolveWithHooks(specifier, parentURL, /* importAttributes */ undefined,
1056-
getCjsConditions(), defaultResolve);
1072+
getCjsConditionsArray(), defaultResolve);
10571073
const { url } = resolveResult;
10581074
format = resolveResult.format;
10591075

@@ -1129,7 +1145,7 @@ function loadBuiltinWithHooks(id, url, format) {
11291145
url ??= `node:${id}`;
11301146
// TODO(joyeecheung): do we really want to invoke the load hook for the builtins?
11311147
const loadResult = loadWithHooks(url, format || 'builtin', /* importAttributes */ undefined,
1132-
getCjsConditions(), getDefaultLoad(url, id));
1148+
getCjsConditionsArray(), getDefaultLoad(url, id));
11331149
if (loadResult.format && loadResult.format !== 'builtin') {
11341150
return undefined; // Format has been overridden, return undefined for the caller to continue loading.
11351151
}
@@ -1280,7 +1296,7 @@ Module._load = function(request, parent, isMain) {
12801296
* @param {ResolveFilenameOptions} options Options object
12811297
* @typedef {object} ResolveFilenameOptions
12821298
* @property {string[]} paths Paths to search for modules in
1283-
* @property {string[]} conditions Conditions used for resolution.
1299+
* @property {Set<string>} conditions Conditions used for resolution.
12841300
*/
12851301
Module._resolveFilename = function(request, parent, isMain, options) {
12861302
if (BuiltinModule.normalizeRequirableId(request)) {
@@ -1723,7 +1739,8 @@ function loadSource(mod, filename, formatFromNode) {
17231739
mod[kURL] = convertCJSFilenameToURL(filename);
17241740
}
17251741

1726-
const loadResult = loadWithHooks(mod[kURL], mod[kFormat], /* importAttributes */ undefined, getCjsConditions(),
1742+
const loadResult = loadWithHooks(mod[kURL], mod[kFormat], /* importAttributes */ undefined,
1743+
getCjsConditionsArray(),
17271744
getDefaultLoad(mod[kURL], filename));
17281745

17291746
// Reset the module properties with load hook results.

lib/internal/modules/customization_hooks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const {
4+
ArrayIsArray,
45
ArrayPrototypeFindIndex,
56
ArrayPrototypePush,
67
ArrayPrototypeSplice,

lib/internal/modules/esm/loader.js

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -705,24 +705,30 @@ class ModuleLoader {
705705
if (this.#customizations) { // Only has module.register hooks.
706706
return this.#customizations.resolve(specifier, parentURL, importAttributes);
707707
}
708-
return this.#cachedDefaultResolve(specifier, parentURL, importAttributes);
708+
return this.#cachedDefaultResolve(specifier, {
709+
__proto__: null,
710+
conditions: this.#defaultConditions,
711+
parentURL,
712+
importAttributes
713+
});
709714
}
710715

711716
/**
712717
* Either return a cached resolution, or perform the default resolution which is synchronous, and
713718
* cache the result.
714719
* @param {string} specifier See {@link resolve}.
715-
* @param {string} [parentURL] See {@link resolve}.
716-
* @param {ImportAttributes} importAttributes See {@link resolve}.
720+
* @param {{ parentURL?: string, importAttributes: ImportAttributes, conditions?: string[]}} context
717721
* @returns {{ format: string, url: string }}
718722
*/
719-
#cachedDefaultResolve(specifier, parentURL, importAttributes) {
723+
#cachedDefaultResolve(specifier, context) {
724+
const { parentURL, importAttributes, conditions } = context;
720725
const requestKey = this.#resolveCache.serializeKey(specifier, importAttributes);
721726
const cachedResult = this.#resolveCache.get(requestKey, parentURL);
722727
if (cachedResult != null) {
723728
return cachedResult;
724729
}
725-
const result = this.defaultResolve(specifier, parentURL, importAttributes);
730+
defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve;
731+
const result = defaultResolve(specifier, context);
726732
this.#resolveCache.set(requestKey, parentURL, result);
727733
return result;
728734
}
@@ -750,14 +756,14 @@ class ModuleLoader {
750756
* This is the default resolve step for module.registerHooks(), which incorporates asynchronous hooks
751757
* from module.register() which are run in a blocking fashion for it to be synchronous.
752758
* @param {string|URL} specifier See {@link resolveSync}.
753-
* @param {{ parentURL?: string, importAttributes: ImportAttributes}} context See {@link resolveSync}.
759+
* @param {{ parentURL?: string, importAttributes: ImportAttributes, conditions?: string[]}} context See {@link resolveSync}.
754760
* @returns {{ format: string, url: string }}
755761
*/
756762
#resolveAndMaybeBlockOnLoaderThread(specifier, context) {
757763
if (this.#customizations) {
758764
return this.#customizations.resolveSync(specifier, context.parentURL, context.importAttributes);
759765
}
760-
return this.#cachedDefaultResolve(specifier, context.parentURL, context.importAttributes);
766+
return this.#cachedDefaultResolve(specifier, context);
761767
}
762768

763769
/**
@@ -780,25 +786,12 @@ class ModuleLoader {
780786
return resolveWithHooks(specifier, parentURL, importAttributes, this.#defaultConditions,
781787
this.#resolveAndMaybeBlockOnLoaderThread.bind(this));
782788
}
783-
return this.#resolveAndMaybeBlockOnLoaderThread(specifier, { parentURL, importAttributes });
784-
}
785-
786-
/**
787-
* Our `defaultResolve` is synchronous and can be used in both
788-
* `resolve` and `resolveSync`. This function is here just to avoid
789-
* repeating the same code block twice in those functions.
790-
*/
791-
defaultResolve(originalSpecifier, parentURL, importAttributes) {
792-
defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve;
793-
794-
const context = {
789+
return this.#resolveAndMaybeBlockOnLoaderThread(specifier, {
795790
__proto__: null,
796791
conditions: this.#defaultConditions,
797-
importAttributes,
798792
parentURL,
799-
};
800-
801-
return defaultResolve(originalSpecifier, context);
793+
importAttributes
794+
});
802795
}
803796

804797
/**

lib/internal/modules/helpers.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ function toRealPath(requestPath) {
6565

6666
/** @type {Set<string>} */
6767
let cjsConditions;
68+
/** @type {string[]} */
69+
let cjsConditionsArray;
70+
6871
/**
6972
* Define the conditions that apply to the CommonJS loader.
7073
*/
@@ -73,15 +76,17 @@ function initializeCjsConditions() {
7376
const noAddons = getOptionValue('--no-addons');
7477
const addonConditions = noAddons ? [] : ['node-addons'];
7578
// TODO: Use this set when resolving pkg#exports conditions in loader.js.
76-
cjsConditions = new SafeSet([
79+
cjsConditionsArray = [
7780
'require',
7881
'node',
7982
...addonConditions,
8083
...userConditions,
81-
]);
84+
];
8285
if (getOptionValue('--experimental-require-module')) {
83-
cjsConditions.add('module-sync');
86+
cjsConditionsArray.push('module-sync');
8487
}
88+
ObjectFreeze(cjsConditionsArray);
89+
cjsConditions = new SafeSet(cjsConditionsArray);
8590
}
8691

8792
/**
@@ -94,6 +99,13 @@ function getCjsConditions() {
9499
return cjsConditions;
95100
}
96101

102+
function getCjsConditionsArray() {
103+
if (cjsConditionsArray === undefined) {
104+
initializeCjsConditions();
105+
}
106+
return cjsConditionsArray;
107+
}
108+
97109
/**
98110
* Provide one of Node.js' public modules to user code.
99111
* @param {string} id - The identifier/specifier of the builtin module to load
@@ -408,6 +420,7 @@ module.exports = {
408420
flushCompileCache,
409421
getBuiltinModule,
410422
getCjsConditions,
423+
getCjsConditionsArray,
411424
getCompileCacheDir,
412425
initializeCjsConditions,
413426
loadBuiltinModule,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
exports.cjs = function(key) {
2+
return require(key);
3+
};
4+
exports.esm = function(key) {
5+
return import(key);
6+
};
7+

test/fixtures/es-modules/custom-condition/node_modules/foo/default.cjs

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/custom-condition/node_modules/foo/foo-esm.mjs

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/custom-condition/node_modules/foo/foo.cjs

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/custom-condition/node_modules/foo/package.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Similar to test-module-hooks-custom-conditions.mjs, but checking the
2+
// real require() instead of the re-invented one for imported CJS.
3+
4+
const common = require('../common');
5+
const { registerHooks } = require('node:module');
6+
const assert = require('node:assert');
7+
const { cjs, esm } = require('../fixtures/es-modules/custom-condition/load.cjs');
8+
9+
(async () => {
10+
// Without hooks, the default condition is used.
11+
assert.strictEqual(cjs('foo').result, 'default');
12+
assert.strictEqual((await esm('foo')).result, 'default');
13+
14+
// Prepending 'foo' to the conditions array in the resolve hook should
15+
// allow a CJS to be resolved with that condition.
16+
{
17+
const hooks = registerHooks({
18+
resolve(specifier, context, nextResolve) {
19+
assert(Array.isArray(context.conditions));
20+
context.conditions = ['foo', ...context.conditions];
21+
return nextResolve(specifier, context);
22+
}
23+
});
24+
assert.strictEqual(cjs('foo/second').result, 'foo');
25+
assert.strictEqual((await esm('foo/second')).result, 'foo');
26+
hooks.deregister();
27+
}
28+
29+
// Prepending 'foo-esm' to the conditions array in the resolve hook should
30+
// allow a ESM to be resolved with that condition.
31+
{
32+
const hooks = registerHooks({
33+
resolve(specifier, context, nextResolve) {
34+
assert(Array.isArray(context.conditions));
35+
context.conditions = ['foo-esm', ...context.conditions];
36+
return nextResolve(specifier, context);
37+
}
38+
});
39+
assert.strictEqual(cjs('foo/third').result, 'foo-esm');
40+
assert.strictEqual((await esm('foo/third')).result, 'foo-esm');
41+
hooks.deregister();
42+
}
43+
44+
// Duplicating the 'foo' condition in the resolve hook should not change the result.
45+
{
46+
const hooks = registerHooks({
47+
resolve(specifier, context, nextResolve) {
48+
assert(Array.isArray(context.conditions));
49+
context.conditions = ['foo', ...context.conditions, 'foo'];
50+
return nextResolve(specifier, context);
51+
}
52+
});
53+
assert.strictEqual(cjs('foo/fourth').result, 'foo');
54+
assert.strictEqual((await esm('foo/fourth')).result, 'foo');
55+
hooks.deregister();
56+
}
57+
})().catch(common.mustNotCall());

0 commit comments

Comments
 (0)