Skip to content

Commit 19b470f

Browse files
esm: bypass CommonJS loader under --default-type
PR-URL: #49986 Reviewed-By: Jacob Smith <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent b55adfb commit 19b470f

File tree

7 files changed

+176
-37
lines changed

7 files changed

+176
-37
lines changed

‎doc/api/cli.md‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@ For more info about `node inspect`, see the [debugger][] documentation.
2525

2626
The program entry point is a specifier-like string. If the string is not an
2727
absolute path, it's resolved as a relative path from the current working
28-
directory. That path is then resolved by [CommonJS][] module loader. If no
29-
corresponding file is found, an error is thrown.
28+
directory. That path is then resolved by [CommonJS][] module loader, or by the
29+
[ES module loader][Modules loaders] if [`--experimental-default-type=module`][]
30+
is passed. If no corresponding file is found, an error is thrown.
3031

3132
If a file is found, its path will be passed to the
3233
[ES module loader][Modules loaders] under any of the following conditions:
3334

3435
* The program was started with a command-line flag that forces the entry
35-
point to be loaded with ECMAScript module loader.
36+
point to be loaded with ECMAScript module loader, such as `--import` or
37+
[`--experimental-default-type=module`][].
3638
* The file has an `.mjs` extension.
3739
* The file does not have a `.cjs` extension, and the nearest parent
3840
`package.json` file contains a top-level [`"type"`][] field with a value of
@@ -45,8 +47,9 @@ Otherwise, the file is loaded using the CommonJS module loader. See
4547

4648
When loading, the [ES module loader][Modules loaders] loads the program
4749
entry point, the `node` command will accept as input only files with `.js`,
48-
`.mjs`, or `.cjs` extensions; and with `.wasm` extensions when
49-
[`--experimental-wasm-modules`][] is enabled.
50+
`.mjs`, or `.cjs` extensions; with `.wasm` extensions when
51+
[`--experimental-wasm-modules`][] is enabled; and with no extension when
52+
[`--experimental-default-type=module`][] is passed.
5053

5154
## Options
5255

@@ -2741,6 +2744,7 @@ done
27412744
[`--allow-worker`]: #--allow-worker
27422745
[`--cpu-prof-dir`]: #--cpu-prof-dir
27432746
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
2747+
[`--experimental-default-type=module`]: #--experimental-default-typetype
27442748
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
27452749
[`--experimental-wasm-modules`]: #--experimental-wasm-modules
27462750
[`--heap-prof-dir`]: #--heap-prof-dir

‎lib/internal/main/run_main_module.js‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,24 @@ const{
66
prepareMainThreadExecution,
77
markBootstrapComplete,
88
}=require('internal/process/pre_execution');
9+
const{ getOptionValue }=require('internal/options');
910

10-
prepareMainThreadExecution(true);
11+
constmainEntry=prepareMainThreadExecution(true);
1112

1213
markBootstrapComplete();
1314

1415
// Necessary to reset RegExp statics before user code runs.
1516
RegExpPrototypeExec(/^/,'');
1617

17-
// Note: this loads the module through the ESM loader if the module is
18-
// determined to be an ES module. This hangs from the CJS module loader
19-
// because we currently allow monkey-patching of the module loaders
20-
// in the preloaded scripts through require('module').
21-
// runMain here might be monkey-patched by users in --require.
22-
// XXX: the monkey-patchability here should probably be deprecated.
23-
require('internal/modules/cjs/loader').Module.runMain(process.argv[1]);
18+
if(getOptionValue('--experimental-default-type')==='module'){
19+
require('internal/modules/run_main').executeUserEntryPoint(mainEntry);
20+
}else{
21+
/**
22+
* To support legacy monkey-patching of `Module.runMain`, we call `runMain` here to have the CommonJS loader begin
23+
* the execution of the main entry point, even if the ESM loader immediately takes over because the main entry is an
24+
* ES module or one of the other opt-in conditions (such as the use of `--import`) are met. Users can monkey-patch
25+
* before the main entry point is loaded by doing so via scripts loaded through `--require`. This monkey-patchability
26+
* is undesirable and is removed in `--experimental-default-type=module` mode.
27+
*/
28+
require('internal/modules/cjs/loader').Module.runMain(mainEntry);
29+
}

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,17 +1132,7 @@ function defaultResolve(specifier, context ={}){
11321132
if(StringPrototypeStartsWith(specifier,'file://')){
11331133
specifier=fileURLToPath(specifier);
11341134
}
1135-
constfound=resolveAsCommonJS(specifier,parentURL);
1136-
if(found){
1137-
// Modify the stack and message string to include the hint
1138-
constlines=StringPrototypeSplit(error.stack,'\n');
1139-
consthint=`Did you mean to import ${found}?`;
1140-
error.stack=
1141-
ArrayPrototypeShift(lines)+'\n'+
1142-
hint+'\n'+
1143-
ArrayPrototypeJoin(lines,'\n');
1144-
error.message+=`\n${hint}`;
1145-
}
1135+
decorateErrorWithCommonJSHints(error,specifier,parentURL);
11461136
}
11471137
throwerror;
11481138
}
@@ -1156,7 +1146,28 @@ function defaultResolve(specifier, context ={}){
11561146
};
11571147
}
11581148

1149+
/**
1150+
* Decorates the given error with a hint for CommonJS modules.
1151+
* @param{Error} error - The error to decorate.
1152+
* @param{string} specifier - The specifier that was attempted to be imported.
1153+
* @param{string} parentURL - The URL of the parent module.
1154+
*/
1155+
functiondecorateErrorWithCommonJSHints(error,specifier,parentURL){
1156+
constfound=resolveAsCommonJS(specifier,parentURL);
1157+
if(found){
1158+
// Modify the stack and message string to include the hint
1159+
constlines=StringPrototypeSplit(error.stack,'\n');
1160+
consthint=`Did you mean to import ${found}?`;
1161+
error.stack=
1162+
ArrayPrototypeShift(lines)+'\n'+
1163+
hint+'\n'+
1164+
ArrayPrototypeJoin(lines,'\n');
1165+
error.message+=`\n${hint}`;
1166+
}
1167+
}
1168+
11591169
module.exports={
1170+
decorateErrorWithCommonJSHints,
11601171
defaultResolve,
11611172
encodedSepRegEx,
11621173
getPackageScopeConfig,

‎lib/internal/modules/run_main.js‎

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,33 @@ const path = require('path');
1212
* @param{string} main - Entry point path
1313
*/
1414
functionresolveMainPath(main){
15-
// Note extension resolution for the main entry point can be deprecated in a
16-
// future major.
17-
// Module._findPath is monkey-patchable here.
18-
const{ Module }=require('internal/modules/cjs/loader');
19-
letmainPath=Module._findPath(path.resolve(main),null,true);
15+
constdefaultType=getOptionValue('--experimental-default-type');
16+
/** @type{string} */
17+
letmainPath;
18+
if(defaultType==='module'){
19+
if(getOptionValue('--preserve-symlinks-main')){return;}
20+
mainPath=path.resolve(main);
21+
}else{
22+
// Extension searching for the main entry point is supported only in legacy mode.
23+
// Module._findPath is monkey-patchable here.
24+
const{ Module }=require('internal/modules/cjs/loader');
25+
mainPath=Module._findPath(path.resolve(main),null,true);
26+
}
2027
if(!mainPath){return;}
2128

2229
constpreserveSymlinksMain=getOptionValue('--preserve-symlinks-main');
2330
if(!preserveSymlinksMain){
2431
const{ toRealPath }=require('internal/modules/helpers');
25-
mainPath=toRealPath(mainPath);
32+
try{
33+
mainPath=toRealPath(mainPath);
34+
}catch(err){
35+
if(defaultType==='module'&&err?.code==='ENOENT'){
36+
const{ decorateErrorWithCommonJSHints }=require('internal/modules/esm/resolve');
37+
const{ getCWDURL }=require('internal/util');
38+
decorateErrorWithCommonJSHints(err,mainPath,getCWDURL());
39+
}
40+
throwerr;
41+
}
2642
}
2743

2844
returnmainPath;
@@ -33,6 +49,8 @@ function resolveMainPath(main){
3349
* @param{string} mainPath - Absolute path to the main entry point
3450
*/
3551
functionshouldUseESMLoader(mainPath){
52+
if(getOptionValue('--experimental-default-type')==='module'){returntrue;}
53+
3654
/**
3755
* @type{string[]} userLoaders A list of custom loaders registered by the user
3856
* (or an empty list when none have been registered).
@@ -62,10 +80,9 @@ function shouldUseESMLoader(mainPath){
6280
functionrunMainESM(mainPath){
6381
const{ loadESM }=require('internal/process/esm_loader');
6482
const{ pathToFileURL }=require('internal/url');
83+
constmain=pathToFileURL(mainPath).href;
6584

6685
handleMainPromise(loadESM((esmLoader)=>{
67-
constmain=path.isAbsolute(mainPath) ?
68-
pathToFileURL(mainPath).href : mainPath;
6986
returnesmLoader.import(main,undefined,{__proto__: null});
7087
}));
7188
}
@@ -90,8 +107,9 @@ async function handleMainPromise(promise){
90107
* Parse the CLI main entry point string and run it.
91108
* For backwards compatibility, we have to run a bunch of monkey-patchable code that belongs to the CJS loader (exposed
92109
* by `require('module')`) even when the entry point is ESM.
110+
* This monkey-patchable code is bypassed under `--experimental-default-type=module`.
93111
* Because of backwards compatibility, this function is exposed publicly via `import{runMain } from 'node:module'`.
94-
* @param{string} main - Resolved absolute path for the main entry point, if found
112+
* @param{string} main - First positional CLI argument, such as `'entry.js'` from `node entry.js`
95113
*/
96114
functionexecuteUserEntryPoint(main=process.argv[1]){
97115
constresolvedMain=resolveMainPath(main);

‎lib/internal/process/pre_execution.js‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const{
5252
}=require('internal/v8/startup_snapshot');
5353

5454
functionprepareMainThreadExecution(expandArgv1=false,initializeModules=true){
55-
prepareExecution({
55+
returnprepareExecution({
5656
expandArgv1,
5757
initializeModules,
5858
isMainThread: true,
@@ -73,8 +73,8 @@ function prepareExecution(options){
7373
refreshRuntimeOptions();
7474
reconnectZeroFillToggle();
7575

76-
// Patch the process object with legacy properties and normalizations
77-
patchProcessObject(expandArgv1);
76+
// Patch the process object and get the resolved main entry point.
77+
constmainEntry=patchProcessObject(expandArgv1);
7878
setupTraceCategoryState();
7979
setupInspectorHooks();
8080
setupWarningHandler();
@@ -131,6 +131,8 @@ function prepareExecution(options){
131131
if(initializeModules){
132132
setupUserModules();
133133
}
134+
135+
returnmainEntry;
134136
}
135137

136138
functionsetupSymbolDisposePolyfill(){
@@ -202,14 +204,17 @@ function patchProcessObject(expandArgv1){
202204
process._exiting=false;
203205
process.argv[0]=process.execPath;
204206

207+
/** @type{string} */
208+
letmainEntry;
205209
// If requested, update process.argv[1] to replace whatever the user provided with the resolved absolute file path of
206210
// the entry point.
207211
if(expandArgv1&&process.argv[1]&&
208212
!StringPrototypeStartsWith(process.argv[1],'-')){
209213
// Expand process.argv[1] into a full path.
210214
constpath=require('path');
211215
try{
212-
process.argv[1]=path.resolve(process.argv[1]);
216+
mainEntry=path.resolve(process.argv[1]);
217+
process.argv[1]=mainEntry;
213218
}catch{
214219
// Continue regardless of error.
215220
}
@@ -236,6 +241,8 @@ function patchProcessObject(expandArgv1){
236241
addReadOnlyProcessAlias('traceDeprecation','--trace-deprecation');
237242
addReadOnlyProcessAlias('_breakFirstLine','--inspect-brk',false);
238243
addReadOnlyProcessAlias('_breakNodeFirstLine','--inspect-brk-node',false);
244+
245+
returnmainEntry;
239246
}
240247

241248
functionaddReadOnlyProcessAlias(name,option,enumerable=true){
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import{spawnPromisified}from'../common/index.mjs';
2+
import*asfixturesfrom'../common/fixtures.mjs';
3+
import{describe,it}from'node:test';
4+
import{match,strictEqual}from'node:assert';
5+
6+
describe('--experimental-default-type=module should not support extension searching',{concurrency: true},()=>{
7+
it('should support extension searching under --experimental-default-type=commonjs',async()=>{
8+
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
9+
'--experimental-default-type=commonjs',
10+
'index',
11+
],{
12+
cwd: fixtures.path('es-modules/package-without-type'),
13+
});
14+
15+
strictEqual(stdout,'package-without-type\n');
16+
strictEqual(stderr,'');
17+
strictEqual(code,0);
18+
strictEqual(signal,null);
19+
});
20+
21+
it('should error with implicit extension under --experimental-default-type=module',async()=>{
22+
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
23+
'--experimental-default-type=module',
24+
'index',
25+
],{
26+
cwd: fixtures.path('es-modules/package-without-type'),
27+
});
28+
29+
match(stderr,/ENOENT.*Didyoumeantoimport.*index\.js\?/s);
30+
strictEqual(stdout,'');
31+
strictEqual(code,1);
32+
strictEqual(signal,null);
33+
});
34+
});
35+
36+
describe('--experimental-default-type=module should not parse paths as URLs',{concurrency: true},()=>{
37+
it('should not parse a `?` in a filename as starting a query string',async()=>{
38+
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
39+
'--experimental-default-type=module',
40+
'file#1.js',
41+
],{
42+
cwd: fixtures.path('es-modules/package-without-type'),
43+
});
44+
45+
strictEqual(stderr,'');
46+
strictEqual(stdout,'file#1\n');
47+
strictEqual(code,0);
48+
strictEqual(signal,null);
49+
});
50+
51+
it('should resolve `..`',async()=>{
52+
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
53+
'--experimental-default-type=module',
54+
'../package-without-type/file#1.js',
55+
],{
56+
cwd: fixtures.path('es-modules/package-without-type'),
57+
});
58+
59+
strictEqual(stderr,'');
60+
strictEqual(stdout,'file#1\n');
61+
strictEqual(code,0);
62+
strictEqual(signal,null);
63+
});
64+
65+
it('should allow a leading `./`',async()=>{
66+
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
67+
'--experimental-default-type=module',
68+
'./file#1.js',
69+
],{
70+
cwd: fixtures.path('es-modules/package-without-type'),
71+
});
72+
73+
strictEqual(stderr,'');
74+
strictEqual(stdout,'file#1\n');
75+
strictEqual(code,0);
76+
strictEqual(signal,null);
77+
});
78+
79+
it('should not require a leading `./`',async()=>{
80+
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
81+
'--experimental-default-type=module',
82+
'file#1.js',
83+
],{
84+
cwd: fixtures.path('es-modules/package-without-type'),
85+
});
86+
87+
strictEqual(stderr,'');
88+
strictEqual(stdout,'file#1\n');
89+
strictEqual(code,0);
90+
strictEqual(signal,null);
91+
});
92+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log('file#1');

0 commit comments

Comments
(0)