Skip to content

Commit fe0195f

Browse files
joyeecheungaduh95
authored andcommitted
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. PR-URL: #59011Fixes: #59003 Reviewed-By: Zeyu "Alex" Yang <[email protected]> Reviewed-By: Jacob Smith <[email protected]>
1 parent 55a90ee commit fe0195f

File tree

10 files changed

+269
-33
lines changed

10 files changed

+269
-33
lines changed

‎lib/internal/modules/cjs/loader.js‎

Lines changed: 22 additions & 6 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,
@@ -653,7 +655,7 @@ const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/;
653655
* Resolves the exports for a given module path and request.
654656
* @param{string} nmPath The path to the module.
655657
* @param{string} request The request for the module.
656-
* @param{unknown} conditions
658+
* @param{Set<string>} conditions The conditions to use for resolution.
657659
* @returns{undefined|string}
658660
*/
659661
functionresolveExports(nmPath,request,conditions){
@@ -1068,17 +1070,30 @@ function resolveForCJSWithHooks(specifier, parent, isMain){
10681070
functiondefaultResolve(specifier,context){
10691071
// TODO(joyeecheung): parent and isMain should be part of context, then we
10701072
// no longer need to use a different defaultResolve for every resolution.
1073+
// In the hooks, context.conditions is passed around as an array, but internally
1074+
// the resolution helpers expect a SafeSet. Do the conversion here.
1075+
letconditionSet;
1076+
constconditions=context.conditions;
1077+
if(conditions!==undefined&&conditions!==getCjsConditionsArray()){
1078+
if(!ArrayIsArray(conditions)){
1079+
thrownewERR_INVALID_ARG_VALUE('context.conditions',conditions,
1080+
'expected an array');
1081+
}
1082+
conditionSet=newSafeSet(conditions);
1083+
}else{
1084+
conditionSet=getCjsConditions();
1085+
}
10711086
defaultResolvedFilename=defaultResolveImpl(specifier,parent,isMain,{
10721087
__proto__: null,
1073-
conditions: context.conditions,
1088+
conditions: conditionSet,
10741089
});
10751090

10761091
defaultResolvedURL=convertCJSFilenameToURL(defaultResolvedFilename);
10771092
return{__proto__: null,url: defaultResolvedURL};
10781093
}
10791094

10801095
constresolveResult=resolveWithHooks(specifier,parentURL,/* importAttributes */undefined,
1081-
getCjsConditions(),defaultResolve);
1096+
getCjsConditionsArray(),defaultResolve);
10821097
const{ url }=resolveResult;
10831098
format=resolveResult.format;
10841099

@@ -1154,7 +1169,7 @@ function loadBuiltinWithHooks(id, url, format){
11541169
url??=`node:${id}`;
11551170
// TODO(joyeecheung): do we really want to invoke the load hook for the builtins?
11561171
constloadResult=loadWithHooks(url,format||'builtin',/* importAttributes */undefined,
1157-
getCjsConditions(),getDefaultLoad(url,id));
1172+
getCjsConditionsArray(),getDefaultLoad(url,id));
11581173
if(loadResult.format&&loadResult.format!=='builtin'){
11591174
returnundefined;// Format has been overridden, return undefined for the caller to continue loading.
11601175
}
@@ -1306,7 +1321,7 @@ Module._load = function(request, parent, isMain){
13061321
* @param{ResolveFilenameOptions} options Options object
13071322
* @typedef{object} ResolveFilenameOptions
13081323
* @property{string[]} paths Paths to search for modules in
1309-
* @property{string[]} conditions Conditions used for resolution.
1324+
* @property{Set<string>?} conditions The conditions to use for resolution.
13101325
* @returns{void|string}
13111326
*/
13121327
Module._resolveFilename=function(request,parent,isMain,options){
@@ -1755,7 +1770,8 @@ function loadSource(mod, filename, formatFromNode){
17551770
mod[kURL]=convertCJSFilenameToURL(filename);
17561771
}
17571772

1758-
constloadResult=loadWithHooks(mod[kURL],mod[kFormat],/* importAttributes */undefined,getCjsConditions(),
1773+
constloadResult=loadWithHooks(mod[kURL],mod[kFormat],/* importAttributes */undefined,
1774+
getCjsConditionsArray(),
17591775
getDefaultLoad(mod[kURL],filename));
17601776

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

‎lib/internal/modules/esm/loader.js‎

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -709,24 +709,30 @@ class ModuleLoader{
709709
if(this.#customizations){// Only has module.register hooks.
710710
returnthis.#customizations.resolve(specifier,parentURL,importAttributes);
711711
}
712-
returnthis.#cachedDefaultResolve(specifier,parentURL,importAttributes);
712+
returnthis.#cachedDefaultResolve(specifier,{
713+
__proto__: null,
714+
conditions: this.#defaultConditions,
715+
parentURL,
716+
importAttributes,
717+
});
713718
}
714719

715720
/**
716721
* Either return a cached resolution, or perform the default resolution which is synchronous, and
717722
* cache the result.
718723
* @param{string} specifier See{@link resolve}.
719-
* @param{string} [parentURL] See{@link resolve}.
720-
* @param{ImportAttributes} importAttributes See{@link resolve}.
724+
* @param{{parentURL?: string, importAttributes: ImportAttributes, conditions?: string[]}} context
721725
* @returns{{format: string, url: string }}
722726
*/
723-
#cachedDefaultResolve(specifier,parentURL,importAttributes){
727+
#cachedDefaultResolve(specifier,context){
728+
const{ parentURL, importAttributes }=context;
724729
constrequestKey=this.#resolveCache.serializeKey(specifier,importAttributes);
725730
constcachedResult=this.#resolveCache.get(requestKey,parentURL);
726731
if(cachedResult!=null){
727732
returncachedResult;
728733
}
729-
constresult=this.defaultResolve(specifier,parentURL,importAttributes);
734+
defaultResolve??=require('internal/modules/esm/resolve').defaultResolve;
735+
constresult=defaultResolve(specifier,context);
730736
this.#resolveCache.set(requestKey,parentURL,result);
731737
returnresult;
732738
}
@@ -754,14 +760,15 @@ class ModuleLoader{
754760
* This is the default resolve step for module.registerHooks(), which incorporates asynchronous hooks
755761
* from module.register() which are run in a blocking fashion for it to be synchronous.
756762
* @param{string|URL} specifier See{@link resolveSync}.
757-
* @param{{parentURL?: string, importAttributes: ImportAttributes}} context See{@link resolveSync}.
763+
* @param{{parentURL?: string, importAttributes: ImportAttributes, conditions?: string[]}} context
764+
* See{@link resolveSync}.
758765
* @returns{{format: string, url: string }}
759766
*/
760767
#resolveAndMaybeBlockOnLoaderThread(specifier,context){
761768
if(this.#customizations){
762769
returnthis.#customizations.resolveSync(specifier,context.parentURL,context.importAttributes);
763770
}
764-
returnthis.#cachedDefaultResolve(specifier,context.parentURL,context.importAttributes);
771+
returnthis.#cachedDefaultResolve(specifier,context);
765772
}
766773

767774
/**
@@ -784,26 +791,12 @@ class ModuleLoader{
784791
returnresolveWithHooks(specifier,parentURL,importAttributes,this.#defaultConditions,
785792
this.#resolveAndMaybeBlockOnLoaderThread.bind(this));
786793
}
787-
returnthis.#resolveAndMaybeBlockOnLoaderThread(specifier,{ parentURL, importAttributes });
788-
}
789-
790-
/**
791-
* Our `defaultResolve` is synchronous and can be used in both
792-
* `resolve` and `resolveSync`. This function is here just to avoid
793-
* repeating the same code block twice in those functions.
794-
* @returns{string}
795-
*/
796-
defaultResolve(originalSpecifier,parentURL,importAttributes){
797-
defaultResolve??=require('internal/modules/esm/resolve').defaultResolve;
798-
799-
constcontext={
794+
returnthis.#resolveAndMaybeBlockOnLoaderThread(specifier,{
800795
__proto__: null,
801796
conditions: this.#defaultConditions,
802-
importAttributes,
803797
parentURL,
804-
};
805-
806-
returndefaultResolve(originalSpecifier,context);
798+
importAttributes,
799+
});
807800
}
808801

809802
/**

‎lib/internal/modules/helpers.js‎

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

6767
/** @type{Set<string>} */
6868
letcjsConditions;
69+
/** @type{string[]} */
70+
letcjsConditionsArray;
71+
6972
/**
7073
* Define the conditions that apply to the CommonJS loader.
7174
* @returns{void}
@@ -75,15 +78,17 @@ function initializeCjsConditions(){
7578
constnoAddons=getOptionValue('--no-addons');
7679
constaddonConditions=noAddons ? [] : ['node-addons'];
7780
// TODO: Use this set when resolving pkg#exports conditions in loader.js.
78-
cjsConditions=newSafeSet([
81+
cjsConditionsArray=[
7982
'require',
8083
'node',
8184
...addonConditions,
8285
...userConditions,
83-
]);
86+
];
8487
if(getOptionValue('--experimental-require-module')){
85-
cjsConditions.add('module-sync');
88+
cjsConditionsArray.push('module-sync');
8689
}
90+
ObjectFreeze(cjsConditionsArray);
91+
cjsConditions=newSafeSet(cjsConditionsArray);
8792
}
8893

8994
/**
@@ -97,6 +102,13 @@ function getCjsConditions(){
97102
returncjsConditions;
98103
}
99104

105+
functiongetCjsConditionsArray(){
106+
if(cjsConditionsArray===undefined){
107+
initializeCjsConditions();
108+
}
109+
returncjsConditionsArray;
110+
}
111+
100112
/**
101113
* Provide one of Node.js' public modules to user code.
102114
* @param{string} id - The identifier/specifier of the builtin module to load
@@ -418,6 +430,7 @@ module.exports ={
418430
flushCompileCache,
419431
getBuiltinModule,
420432
getCjsConditions,
433+
getCjsConditionsArray,
421434
getCompileCacheDir,
422435
initializeCjsConditions,
423436
loadBuiltinModule,

‎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+
'use strict';
4+
constcommon=require('../common');
5+
const{ registerHooks }=require('node:module');
6+
constassert=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((awaitesm('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+
consthooks=registerHooks({
18+
resolve(specifier,context,nextResolve){
19+
assert(Array.isArray(context.conditions));
20+
context.conditions=['foo', ...context.conditions];
21+
returnnextResolve(specifier,context);
22+
},
23+
});
24+
assert.strictEqual(cjs('foo/second').result,'foo');
25+
assert.strictEqual((awaitesm('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+
consthooks=registerHooks({
33+
resolve(specifier,context,nextResolve){
34+
assert(Array.isArray(context.conditions));
35+
context.conditions=['foo-esm', ...context.conditions];
36+
returnnextResolve(specifier,context);
37+
},
38+
});
39+
assert.strictEqual(cjs('foo/third').result,'foo-esm');
40+
assert.strictEqual((awaitesm('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+
consthooks=registerHooks({
47+
resolve(specifier,context,nextResolve){
48+
assert(Array.isArray(context.conditions));
49+
context.conditions=['foo', ...context.conditions,'foo'];
50+
returnnextResolve(specifier,context);
51+
},
52+
});
53+
assert.strictEqual(cjs('foo/fourth').result,'foo');
54+
assert.strictEqual((awaitesm('foo/fourth')).result,'foo');
55+
hooks.deregister();
56+
}
57+
})().then(common.mustCall());
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Check various special values of `conditions` in the context object
2+
// when using synchronous module hooks to override the loaders in a
3+
// CJS module.
4+
'use strict';
5+
constcommon=require('../common');
6+
const{ registerHooks }=require('node:module');
7+
constassert=require('node:assert');
8+
const{ cjs, esm }=require('../fixtures/es-modules/custom-condition/load.cjs');
9+
10+
(async()=>{
11+
// Setting it to undefined would lead to the default conditions being used.
12+
{
13+
consthooks=registerHooks({
14+
resolve(specifier,context,nextResolve){
15+
context.conditions=undefined;
16+
returnnextResolve(specifier,context);
17+
},
18+
});
19+
assert.strictEqual(cjs('foo').result,'default');
20+
assert.strictEqual((awaitesm('foo')).result,'default');
21+
hooks.deregister();
22+
}
23+
24+
// Setting it to an empty array would lead to the default conditions being used.
25+
{
26+
consthooks=registerHooks({
27+
resolve(specifier,context,nextResolve){
28+
context.conditions=[];
29+
returnnextResolve(specifier,context);
30+
},
31+
});
32+
assert.strictEqual(cjs('foo/second').result,'default');
33+
assert.strictEqual((awaitesm('foo/second')).result,'default');
34+
hooks.deregister();
35+
}
36+
37+
// If the exports have no default export, it should error.
38+
{
39+
consthooks=registerHooks({
40+
resolve(specifier,context,nextResolve){
41+
context.conditions=[];
42+
returnnextResolve(specifier,context);
43+
},
44+
});
45+
assert.throws(()=>cjs('foo/no-default'),{
46+
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED',
47+
});
48+
awaitassert.rejects(esm('foo/no-default'),{
49+
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED',
50+
});
51+
hooks.deregister();
52+
}
53+
54+
// If the exports have no default export, it should error.
55+
{
56+
consthooks=registerHooks({
57+
resolve(specifier,context,nextResolve){
58+
context.conditions='invalid';
59+
returnnextResolve(specifier,context);
60+
},
61+
});
62+
assert.throws(()=>cjs('foo/third'),{
63+
code: 'ERR_INVALID_ARG_VALUE',
64+
});
65+
awaitassert.rejects(esm('foo/third'),{
66+
code: 'ERR_INVALID_ARG_VALUE',
67+
});
68+
hooks.deregister();
69+
}
70+
})().then(common.mustCall());

0 commit comments

Comments
(0)