Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 19b470f

Browse filesBrowse files
esm: bypass CommonJS loader under --default-type
PR-URL: #49986 Reviewed-By: Jacob Smith <jacob@frende.me> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent b55adfb commit 19b470f
Copy full SHA for 19b470f

File tree

Expand file treeCollapse file tree

7 files changed

+176
-37
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

7 files changed

+176
-37
lines changed
Open diff view settings
Collapse file

‎doc/api/cli.md‎

Copy file name to clipboardExpand all lines: doc/api/cli.md
+9-5Lines changed: 9 additions & 5 deletions
  • Display the source diff
  • Display the rich diff
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
Collapse file

‎lib/internal/main/run_main_module.js‎

Copy file name to clipboardExpand all lines: lib/internal/main/run_main_module.js
+14-8Lines 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+
const mainEntry = 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+
}
Collapse file

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

Copy file name to clipboardExpand all lines: lib/internal/modules/esm/resolve.js
+22-11Lines 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-
const found = resolveAsCommonJS(specifier, parentURL);
1136-
if (found) {
1137-
// Modify the stack and message string to include the hint
1138-
const lines = StringPrototypeSplit(error.stack, '\n');
1139-
const hint = `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
throw error;
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+
function decorateErrorWithCommonJSHints(error, specifier, parentURL) {
1156+
const found = resolveAsCommonJS(specifier, parentURL);
1157+
if (found) {
1158+
// Modify the stack and message string to include the hint
1159+
const lines = StringPrototypeSplit(error.stack, '\n');
1160+
const hint = `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,
Collapse file

‎lib/internal/modules/run_main.js‎

Copy file name to clipboardExpand all lines: lib/internal/modules/run_main.js
+27-9Lines 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
function resolveMainPath(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-
let mainPath = Module._findPath(path.resolve(main), null, true);
15+
const defaultType = getOptionValue('--experimental-default-type');
16+
/** @type {string} */
17+
let mainPath;
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
const preserveSymlinksMain = 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+
throw err;
41+
}
2642
}
2743

2844
return mainPath;
@@ -33,6 +49,8 @@ function resolveMainPath(main) {
3349
* @param {string} mainPath - Absolute path to the main entry point
3450
*/
3551
function shouldUseESMLoader(mainPath) {
52+
if (getOptionValue('--experimental-default-type') === 'module') { return true; }
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
function runMainESM(mainPath) {
6381
const { loadESM } = require('internal/process/esm_loader');
6482
const { pathToFileURL } = require('internal/url');
83+
const main = pathToFileURL(mainPath).href;
6584

6685
handleMainPromise(loadESM((esmLoader) => {
67-
const main = path.isAbsolute(mainPath) ?
68-
pathToFileURL(mainPath).href : mainPath;
6986
return esmLoader.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
function executeUserEntryPoint(main = process.argv[1]) {
97115
const resolvedMain = resolveMainPath(main);
Collapse file

‎lib/internal/process/pre_execution.js‎

Copy file name to clipboardExpand all lines: lib/internal/process/pre_execution.js
+11-4Lines 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
function prepareMainThreadExecution(expandArgv1 = false, initializeModules = true) {
55-
prepareExecution({
55+
return prepareExecution({
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+
const mainEntry = patchProcessObject(expandArgv1);
7878
setupTraceCategoryState();
7979
setupInspectorHooks();
8080
setupWarningHandler();
@@ -131,6 +131,8 @@ function prepareExecution(options) {
131131
if (initializeModules) {
132132
setupUserModules();
133133
}
134+
135+
return mainEntry;
134136
}
135137

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

207+
/** @type {string} */
208+
let mainEntry;
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
const path = 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+
return mainEntry;
239246
}
240247

241248
function addReadOnlyProcessAlias(name, option, enumerable = true) {
Collapse file
+92Lines 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 * as fixtures from '../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 } = await spawnPromisified(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 } = await spawnPromisified(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.*Did you mean to import .*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 } = await spawnPromisified(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 } = await spawnPromisified(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 } = await spawnPromisified(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 } = await spawnPromisified(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+
});
Collapse file
+1Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.