Skip to content

Commit b9240bc

Browse files
joyeecheungmarco-ippolito
authored andcommitted
module: fix sync resolve hooks for require with node: prefixes
Previously, when require()-ing builtins with the node: prefix, the sync resolve hooks were not properly invoked, and load hooks could not override the builtin's format. This fixes the handling and enables redirecting prefixed built-ins to on-disk files and overriding them with other module types via hooks. PR-URL: #61088 Backport-PR-URL: #62029Fixes: #60005 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Fixes: #61801
1 parent 2e91b28 commit b9240bc

10 files changed

Lines changed: 360 additions & 39 deletions

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

Lines changed: 83 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,7 +1086,9 @@ function resolveForCJSWithHooks(specifier, parent, isMain) {
10861086
filename=convertURLToCJSFilename(url);
10871087
}
10881088

1089-
return{__proto__: null, url, format, filename, parentURL };
1089+
constresult={__proto__: null, url, format, filename, parentURL };
1090+
debug('resolveForCJSWithHooks',specifier,parent?.id,'->',result);
1091+
returnresult;
10901092
}
10911093

10921094
/**
@@ -1143,24 +1145,29 @@ function getDefaultLoad(url, filename) {
11431145
* @param {string} id The module ID (without the node: prefix)
11441146
* @param {string} url The module URL (with the node: prefix)
11451147
* @param {string} format Format from resolution.
1146-
* @returns {any} If there are no load hooks or the load hooks do not override the format of the
1147-
* builtin, load and return the exports of the builtin. Otherwise, return undefined.
1148+
* @returns {{builtinExports: any, resultFromHook: undefined|ModuleLoadResult}} If there are no load
1149+
* hooks or the load hooks do not override the format of the builtin, load and return the exports
1150+
* of the builtin module. Otherwise, return the loadResult for the caller to continue loading.
11481151
*/
11491152
functionloadBuiltinWithHooks(id,url,format){
1153+
letresultFromHook;
11501154
if(loadHooks.length){
11511155
url??=`node:${id}`;
1156+
debug('loadBuiltinWithHooks ',loadHooks.length,id,url,format);
11521157
// TODO(joyeecheung): do we really want to invoke the load hook for the builtins?
1153-
constloadResult=loadWithHooks(url,format||'builtin',/* importAttributes */undefined,
1154-
getCjsConditionsArray(),getDefaultLoad(url,id),validateLoadStrict);
1155-
if(loadResult.format&&loadResult.format!=='builtin'){
1156-
returnundefined;// Format has been overridden, return undefined for the caller to continue loading.
1158+
resultFromHook=loadWithHooks(url,format||'builtin',/* importAttributes */undefined,
1159+
getCjsConditionsArray(),getDefaultLoad(url,id),validateLoadStrict);
1160+
if(resultFromHook.format&&resultFromHook.format!=='builtin'){
1161+
debug('loadBuiltinWithHooks overriding module',id,url,resultFromHook);
1162+
// Format has been overridden, return result for the caller to continue loading.
1163+
return{builtinExports: undefined, resultFromHook };
11571164
}
11581165
}
11591166

11601167
// No hooks or the hooks have not overridden the format. Load it as a builtin module and return the
11611168
// exports.
11621169
constmod=loadBuiltinModule(id);
1163-
returnmod.exports;
1170+
return{builtinExports: mod.exports,resultFromHook: undefined};
11641171
}
11651172

11661173
/**
@@ -1197,47 +1204,64 @@ Module._load = function(request, parent, isMain) {
11971204
}
11981205
}
11991206

1200-
const{ url, format, filename }=resolveForCJSWithHooks(request,parent,isMain);
1207+
constresolveResult=resolveForCJSWithHooks(request,parent,isMain);
1208+
let{ format }=resolveResult;
1209+
const{ url, filename }=resolveResult;
12011210

1211+
letresultFromLoadHook;
12021212
// For backwards compatibility, if the request itself starts with node:, load it before checking
12031213
// Module._cache. Otherwise, load it after the check.
1204-
if(StringPrototypeStartsWith(request,'node:')){
1205-
constresult=loadBuiltinWithHooks(filename,url,format);
1206-
if(result){
1207-
returnresult;
1214+
// TODO(joyeecheung): a more sensible handling is probably, if there are hooks, always go through the hooks
1215+
// first before checking the cache. Otherwise, check the cache first, then proceed to default loading.
1216+
if(request===url&&StringPrototypeStartsWith(request,'node:')){
1217+
constnormalized=BuiltinModule.normalizeRequirableId(request);
1218+
if(normalized){// It's a builtin module.
1219+
const{ resultFromHook, builtinExports }=loadBuiltinWithHooks(normalized,url,format);
1220+
if(builtinExports){
1221+
returnbuiltinExports;
1222+
}
1223+
// The format of the builtin has been overridden by user hooks. Continue loading.
1224+
resultFromLoadHook=resultFromHook;
1225+
format=resultFromLoadHook.format;
12081226
}
1209-
// The format of the builtin has been overridden by user hooks. Continue loading.
12101227
}
12111228

1212-
constcachedModule=Module._cache[filename];
1213-
if(cachedModule!==undefined){
1214-
updateChildren(parent,cachedModule,true);
1215-
if(cachedModule.loaded){
1216-
returncachedModule.exports;
1217-
}
1218-
// If it's not cached by the ESM loader, the loading request
1219-
// comes from required CJS, and we can consider it a circular
1220-
// dependency when it's cached.
1221-
if(!cachedModule[kIsCachedByESMLoader]){
1222-
returngetExportsForCircularRequire(cachedModule);
1223-
}
1224-
// If it's cached by the ESM loader as a way to indirectly pass
1225-
// the module in to avoid creating it twice, the loading request
1226-
// came from imported CJS. In that case use the kModuleCircularVisited
1227-
// to determine if it's loading or not.
1228-
if(cachedModule[kModuleCircularVisited]){
1229-
returngetExportsForCircularRequire(cachedModule);
1229+
// If load hooks overrides the format for a built-in, bypass the cache.
1230+
letcachedModule;
1231+
if(resultFromLoadHook===undefined){
1232+
cachedModule=Module._cache[filename];
1233+
debug('Module._load checking cache for',filename,!!cachedModule);
1234+
if(cachedModule!==undefined){
1235+
updateChildren(parent,cachedModule,true);
1236+
if(cachedModule.loaded){
1237+
returncachedModule.exports;
1238+
}
1239+
// If it's not cached by the ESM loader, the loading request
1240+
// comes from required CJS, and we can consider it a circular
1241+
// dependency when it's cached.
1242+
if(!cachedModule[kIsCachedByESMLoader]){
1243+
returngetExportsForCircularRequire(cachedModule);
1244+
}
1245+
// If it's cached by the ESM loader as a way to indirectly pass
1246+
// the module in to avoid creating it twice, the loading request
1247+
// came from imported CJS. In that case use the kModuleCircularVisited
1248+
// to determine if it's loading or not.
1249+
if(cachedModule[kModuleCircularVisited]){
1250+
returngetExportsForCircularRequire(cachedModule);
1251+
}
1252+
// This is an ESM loader created cache entry, mark it as visited and fallthrough to loading the module.
1253+
cachedModule[kModuleCircularVisited]=true;
12301254
}
1231-
// This is an ESM loader created cache entry, mark it as visited and fallthrough to loading the module.
1232-
cachedModule[kModuleCircularVisited]=true;
12331255
}
12341256

1235-
if(BuiltinModule.canBeRequiredWithoutScheme(filename)){
1236-
constresult=loadBuiltinWithHooks(filename,url,format);
1237-
if(result){
1238-
returnresult;
1257+
if(resultFromLoadHook===undefined&&BuiltinModule.canBeRequiredWithoutScheme(filename)){
1258+
const{ resultFromHook, builtinExports }=loadBuiltinWithHooks(filename,url,format);
1259+
if(builtinExports){
1260+
returnbuiltinExports;
12391261
}
12401262
// The format of the builtin has been overridden by user hooks. Continue loading.
1263+
resultFromLoadHook=resultFromHook;
1264+
format=resultFromLoadHook.format;
12411265
}
12421266

12431267
// Don't call updateChildren(), Module constructor already does.
@@ -1252,6 +1276,9 @@ Module._load = function(request, parent, isMain) {
12521276
}else{
12531277
module[kIsMainSymbol]=false;
12541278
}
1279+
if(resultFromLoadHook!==undefined){
1280+
module[kModuleSource]=resultFromLoadHook.source;
1281+
}
12551282

12561283
reportModuleToWatchMode(filename);
12571284
Module._cache[filename]=module;
@@ -1436,6 +1463,17 @@ function createEsmNotFoundErr(request, path) {
14361463
returnerr;
14371464
}
14381465

1466+
functiongetExtensionForFormat(format){
1467+
switch(format){
1468+
case'addon':
1469+
return'.node';
1470+
case'json':
1471+
return'.json';
1472+
default:
1473+
return'.js';
1474+
}
1475+
}
1476+
14391477
/**
14401478
* Given a file name, pass it to the proper extension handler.
14411479
* @param {string} filename The `require` specifier
@@ -1447,7 +1485,13 @@ Module.prototype.load = function(filename) {
14471485
this.filename??=filename;
14481486
this.paths??=Module._nodeModulePaths(path.dirname(filename));
14491487

1450-
constextension=findLongestRegisteredExtension(filename);
1488+
// If the format is already overridden by hooks, convert that back to extension.
1489+
letextension;
1490+
if(this[kFormat]!==undefined){
1491+
extension=getExtensionForFormat(this[kFormat]);
1492+
}else{
1493+
extension=findLongestRegisteredExtension(filename);
1494+
}
14511495

14521496
Module._extensions[extension](this,filename);
14531497
this.loaded=true;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exportconsturl=import.meta.url;
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 load hooks can override the format of builtin modules
4+
// to 'commonjs' format.
5+
constcommon=require('../common');
6+
constassert=require('assert');
7+
const{ registerHooks }=require('module');
8+
9+
// Pick a builtin that's unlikely to be loaded already - like zlib.
10+
assert(!process.moduleLoadList.includes('NativeModule zlib'));
11+
12+
consthook=registerHooks({
13+
load: common.mustCall(functionload(url,context,nextLoad){
14+
// Only intercept zlib builtin
15+
if(url==='node:zlib'){
16+
// Return a different format to override the builtin
17+
return{
18+
source: 'exports.custom_zlib = "overridden by load hook";',
19+
format: 'commonjs',
20+
shortCircuit: true,
21+
};
22+
}
23+
returnnextLoad(url,context);
24+
},2),// Called twice: once for 'zlib', once for 'node:zlib'
25+
});
26+
27+
// Test: Load hook overrides builtin format to commonjs
28+
constzlib=require('zlib');
29+
assert.strictEqual(zlib.custom_zlib,'overridden by load hook');
30+
assert.strictEqual(typeofzlib.createGzip,'undefined');// Original zlib API should not be available
31+
32+
// Test with node: prefix
33+
constzlib2=require('node:zlib');
34+
assert.strictEqual(zlib2.custom_zlib,'overridden by load hook');
35+
assert.strictEqual(typeofzlib2.createGzip,'undefined');
36+
37+
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 load hooks can override the format of builtin modules
4+
// to 'json' format.
5+
constcommon=require('../common');
6+
constassert=require('assert');
7+
const{ registerHooks }=require('module');
8+
9+
// Pick a builtin that's unlikely to be loaded already - like zlib.
10+
assert(!process.moduleLoadList.includes('NativeModule zlib'));
11+
12+
consthook=registerHooks({
13+
load: common.mustCall(functionload(url,context,nextLoad){
14+
// Only intercept zlib builtin
15+
if(url==='node:zlib'){
16+
// Return JSON format to override the builtin
17+
return{
18+
source: JSON.stringify({custom_zlib: 'JSON overridden zlib'}),
19+
format: 'json',
20+
shortCircuit: true,
21+
};
22+
}
23+
returnnextLoad(url,context);
24+
},2),// Called twice: once for 'zlib', once for 'node:zlib'
25+
});
26+
27+
// Test: Load hook overrides builtin format to json
28+
constzlib=require('zlib');
29+
assert.strictEqual(zlib.custom_zlib,'JSON overridden zlib');
30+
assert.strictEqual(typeofzlib.createGzip,'undefined');// Original zlib API should not be available
31+
32+
// Test with node: prefix
33+
constzlib2=require('node:zlib');
34+
assert.strictEqual(zlib2.custom_zlib,'JSON overridden zlib');
35+
assert.strictEqual(typeofzlib2.createGzip,'undefined');
36+
37+
hook.deregister();
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
3+
// This tests that load hooks can override the format of builtin modules
4+
// to 'module', and require() can load them.
5+
constcommon=require('../common');
6+
constassert=require('assert');
7+
const{ registerHooks }=require('module');
8+
9+
// Pick a builtin that's unlikely to be loaded already - like zlib.
10+
assert(!process.moduleLoadList.includes('NativeModule zlib'));
11+
12+
consthook=registerHooks({
13+
load: common.mustCall(functionload(url,context,nextLoad){
14+
// Only intercept zlib builtin
15+
if(url==='node:zlib'){
16+
// Return ES module format to override the builtin
17+
// Note: For require() to work with ESM, we need to export 'module.exports'
18+
return{
19+
source: `const exports = { custom_zlib: "ESM overridden zlib" };
20+
export default exports;
21+
export { exports as 'module.exports' };`,
22+
format: 'module',
23+
shortCircuit: true,
24+
};
25+
}
26+
returnnextLoad(url,context);
27+
},2),// Called twice: once for 'zlib', once for 'node:zlib'
28+
});
29+
30+
// Test: Load hook overrides builtin format to module.
31+
// With the 'module.exports' export, require() should work
32+
constzlib=require('zlib');
33+
assert.strictEqual(zlib.custom_zlib,'ESM overridden zlib');
34+
assert.strictEqual(typeofzlib.createGzip,'undefined');// Original zlib API should not be available
35+
36+
// Test with node: prefix
37+
constzlib2=require('node:zlib');
38+
assert.strictEqual(zlib2.custom_zlib,'ESM overridden zlib');
39+
assert.strictEqual(typeofzlib2.createGzip,'undefined');
40+
41+
hook.deregister();
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
3+
// This tests that builtins can be redirected to a local file when they are prefixed
4+
// with `node:`.
5+
require('../common');
6+
7+
constassert=require('assert');
8+
const{ registerHooks }=require('module');
9+
constfixtures=require('../common/fixtures');
10+
11+
// This tests that builtins can be redirected to a local file.
12+
// Pick a builtin that's unlikely to be loaded already - like zlib.
13+
assert(!process.moduleLoadList.includes('NativeModule zlib'));
14+
15+
consthook=registerHooks({
16+
resolve(specifier,context,nextLoad){
17+
specifier=specifier.replaceAll('node:','');
18+
return{
19+
url: fixtures.fileURL('module-hooks',`redirected-${specifier}.js`).href,
20+
shortCircuit: true,
21+
};
22+
},
23+
});
24+
25+
// Check assert, which is already loaded.
26+
// eslint-disable-next-line node-core/must-call-assert
27+
assert.strictEqual(require('node:assert').exports_for_test,'redirected assert');
28+
// Check zlib, which is not yet loaded.
29+
assert.strictEqual(require('node:zlib').exports_for_test,'redirected zlib');
30+
// Check fs, which is redirected to an ESM
31+
assert.strictEqual(require('node:fs').exports_for_test,'redirected fs');
32+
33+
hook.deregister();
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
3+
// This tests the interaction between resolve and load hooks for builtins with the
4+
// `node:` prefix.
5+
constcommon=require('../common');
6+
constassert=require('assert');
7+
const{ registerHooks }=require('module');
8+
constfixtures=require('../common/fixtures');
9+
10+
// Pick a builtin that's unlikely to be loaded already - like zlib.
11+
assert(!process.moduleLoadList.includes('NativeModule zlib'));
12+
13+
constredirectedURL=fixtures.fileURL('module-hooks/redirected-zlib.js').href;
14+
15+
registerHooks({
16+
resolve: common.mustCall(functionresolve(specifier,context,nextResolve){
17+
assert.strictEqual(specifier,'node:zlib');
18+
return{
19+
url: redirectedURL,
20+
format: 'module',
21+
shortCircuit: true,
22+
};
23+
}),
24+
25+
load: common.mustCall(functionload(url,context,nextLoad){
26+
assert.strictEqual(url,redirectedURL);
27+
return{
28+
source: 'export const loadURL = import.meta.url;',
29+
format: 'module',
30+
shortCircuit: true,
31+
};
32+
}),
33+
});
34+
35+
constzlib=require('node:zlib');
36+
assert.strictEqual(zlib.loadURL,redirectedURL);

0 commit comments

Comments
 (0)