GeezerVison is showing github.com — esm: bypass CommonJS loader under --default-type · nodejs/node@4de838f
File treeExpand file treeCollapse file tree Open diff view settings fixtures/es-modules/package-without-type
Expand file treeCollapse file tree Open diff view settings Collapse file 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 number | Diff line number | Diff line change |
|---|
@@ -25,14 +25,16 @@ For more info about `node inspect`, see the [debugger][] documentation. | 25 | 25 |
| 26 | 26 | The program entry point is a specifier-like string. If the string is not an | 27 | 27 | 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. | 30 | 31 |
| 31 | 32 | If a file is found, its path will be passed to the | 32 | 33 | [ES module loader][Modules loaders] under any of the following conditions: | 33 | 34 |
| 34 | 35 | * 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`][]. | 36 | 38 | * The file has an `.mjs` extension. | 37 | 39 | * The file does not have a `.cjs` extension, and the nearest parent | 38 | 40 | `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 | 45 | 47 |
| 46 | 48 | When loading, the [ES module loader][Modules loaders] loads the program | 47 | 49 | 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. | 50 | 53 |
| 51 | 54 | ## Options | 52 | 55 |
| @@ -2713,6 +2716,7 @@ done | 2713 | 2716 | [`--allow-worker`]: #--allow-worker | 2714 | 2717 | [`--cpu-prof-dir`]: #--cpu-prof-dir | 2715 | 2718 | [`--diagnostic-dir`]: #--diagnostic-dirdirectory | | 2719 | +[`--experimental-default-type=module`]: #--experimental-default-typetype | 2716 | 2720 | [`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs | 2717 | 2721 | [`--experimental-wasm-modules`]: #--experimental-wasm-modules | 2718 | 2722 | [`--heap-prof-dir`]: #--heap-prof-dir | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/main/run_main_module.js +14-8Lines changed: 14 additions & 8 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -6,18 +6,24 @@ const { | 6 | 6 | prepareMainThreadExecution, | 7 | 7 | markBootstrapComplete, | 8 | 8 | } = require('internal/process/pre_execution'); | | 9 | +const { getOptionValue } = require('internal/options'); | 9 | 10 |
| 10 | | -prepareMainThreadExecution(true); | | 11 | +const mainEntry = prepareMainThreadExecution(true); | 11 | 12 |
| 12 | 13 | markBootstrapComplete(); | 13 | 14 |
| 14 | 15 | // Necessary to reset RegExp statics before user code runs. | 15 | 16 | RegExpPrototypeExec(/^/, ''); | 16 | 17 |
| 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 Copy file name to clipboardExpand all lines: lib/internal/modules/esm/resolve.js +22-11Lines changed: 22 additions & 11 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1132,17 +1132,7 @@ function defaultResolve(specifier, context = {}) { | 1132 | 1132 | if (StringPrototypeStartsWith(specifier, 'file://')) { | 1133 | 1133 | specifier = fileURLToPath(specifier); | 1134 | 1134 | } | 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); | 1146 | 1136 | } | 1147 | 1137 | throw error; | 1148 | 1138 | } | @@ -1156,7 +1146,28 @@ function defaultResolve(specifier, context = {}) { | 1156 | 1146 | }; | 1157 | 1147 | } | 1158 | 1148 |
| | 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 | + | 1159 | 1169 | module.exports = { | | 1170 | + decorateErrorWithCommonJSHints, | 1160 | 1171 | defaultResolve, | 1161 | 1172 | encodedSepRegEx, | 1162 | 1173 | getPackageScopeConfig, | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/run_main.js +27-9Lines changed: 27 additions & 9 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -12,17 +12,33 @@ const path = require('path'); | 12 | 12 | * @param {string} main - Entry point path | 13 | 13 | */ | 14 | 14 | 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 | + } | 20 | 27 | if (!mainPath) { return; } | 21 | 28 |
| 22 | 29 | const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); | 23 | 30 | if (!preserveSymlinksMain) { | 24 | 31 | 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 | + } | 26 | 42 | } | 27 | 43 |
| 28 | 44 | return mainPath; | @@ -33,6 +49,8 @@ function resolveMainPath(main) { | 33 | 49 | * @param {string} mainPath - Absolute path to the main entry point | 34 | 50 | */ | 35 | 51 | function shouldUseESMLoader(mainPath) { | | 52 | + if (getOptionValue('--experimental-default-type') === 'module') { return true; } | | 53 | + | 36 | 54 | /** | 37 | 55 | * @type {string[]} userLoaders A list of custom loaders registered by the user | 38 | 56 | * (or an empty list when none have been registered). | @@ -62,10 +80,9 @@ function shouldUseESMLoader(mainPath) { | 62 | 80 | function runMainESM(mainPath) { | 63 | 81 | const { loadESM } = require('internal/process/esm_loader'); | 64 | 82 | const { pathToFileURL } = require('internal/url'); | | 83 | + const main = pathToFileURL(mainPath).href; | 65 | 84 |
| 66 | 85 | handleMainPromise(loadESM((esmLoader) => { | 67 | | - const main = path.isAbsolute(mainPath) ? | 68 | | - pathToFileURL(mainPath).href : mainPath; | 69 | 86 | return esmLoader.import(main, undefined, { __proto__: null }); | 70 | 87 | })); | 71 | 88 | } | @@ -90,8 +107,9 @@ async function handleMainPromise(promise) { | 90 | 107 | * Parse the CLI main entry point string and run it. | 91 | 108 | * For backwards compatibility, we have to run a bunch of monkey-patchable code that belongs to the CJS loader (exposed | 92 | 109 | * by `require('module')`) even when the entry point is ESM. | | 110 | + * This monkey-patchable code is bypassed under `--experimental-default-type=module`. | 93 | 111 | * 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` | 95 | 113 | */ | 96 | 114 | function executeUserEntryPoint(main = process.argv[1]) { | 97 | 115 | const resolvedMain = resolveMainPath(main); | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/process/pre_execution.js +11-4Lines changed: 11 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -52,7 +52,7 @@ const { | 52 | 52 | } = require('internal/v8/startup_snapshot'); | 53 | 53 |
| 54 | 54 | function prepareMainThreadExecution(expandArgv1 = false, initializeModules = true) { | 55 | | - prepareExecution({ | | 55 | + return prepareExecution({ | 56 | 56 | expandArgv1, | 57 | 57 | initializeModules, | 58 | 58 | isMainThread: true, | @@ -73,8 +73,8 @@ function prepareExecution(options) { | 73 | 73 | refreshRuntimeOptions(); | 74 | 74 | reconnectZeroFillToggle(); | 75 | 75 |
| 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); | 78 | 78 | setupTraceCategoryState(); | 79 | 79 | setupInspectorHooks(); | 80 | 80 | setupWarningHandler(); | @@ -131,6 +131,8 @@ function prepareExecution(options) { | 131 | 131 | if (initializeModules) { | 132 | 132 | setupUserModules(); | 133 | 133 | } | | 134 | + | | 135 | + return mainEntry; | 134 | 136 | } | 135 | 137 |
| 136 | 138 | function setupSymbolDisposePolyfill() { | @@ -202,14 +204,17 @@ function patchProcessObject(expandArgv1) { | 202 | 204 | process._exiting = false; | 203 | 205 | process.argv[0] = process.execPath; | 204 | 206 |
| | 207 | + /** @type {string} */ | | 208 | + let mainEntry; | 205 | 209 | // If requested, update process.argv[1] to replace whatever the user provided with the resolved absolute file path of | 206 | 210 | // the entry point. | 207 | 211 | if (expandArgv1 && process.argv[1] && | 208 | 212 | !StringPrototypeStartsWith(process.argv[1], '-')) { | 209 | 213 | // Expand process.argv[1] into a full path. | 210 | 214 | const path = require('path'); | 211 | 215 | try { | 212 | | - process.argv[1] = path.resolve(process.argv[1]); | | 216 | + mainEntry = path.resolve(process.argv[1]); | | 217 | + process.argv[1] = mainEntry; | 213 | 218 | } catch { | 214 | 219 | // Continue regardless of error. | 215 | 220 | } | @@ -236,6 +241,8 @@ function patchProcessObject(expandArgv1) { | 236 | 241 | addReadOnlyProcessAlias('traceDeprecation', '--trace-deprecation'); | 237 | 242 | addReadOnlyProcessAlias('_breakFirstLine', '--inspect-brk', false); | 238 | 243 | addReadOnlyProcessAlias('_breakNodeFirstLine', '--inspect-brk-node', false); | | 244 | + | | 245 | + return mainEntry; | 239 | 246 | } | 240 | 247 |
| 241 | 248 | function addReadOnlyProcessAlias(name, option, enumerable = true) { | |
Collapse file Copy file name to clipboard +92Lines changed: 92 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 Copy file name to clipboard +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +console.log('file#1'); |
|