GeezerVison is showing github.com — esm: --experimental-default-type flag to flip module defaults · nodejs/node@dc80cce
File treeExpand file treeCollapse file tree Open diff view settings Some content is hiddenLarge Commits have some content hidden by default. Use the searchbox below for content that may be hidden. Dismiss bannerExpand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/cli.md +31Lines changed: 31 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -578,6 +578,36 @@ On Windows, using `cmd.exe` a single quote will not work correctly because it | 578 | 578 | only recognizes double `"` for quoting. In Powershell or Git bash, both `'` | 579 | 579 | and `"` are usable. | 580 | 580 |
| | 581 | +### `--experimental-default-type=type` | | 582 | + | | 583 | +<!-- YAML | | 584 | +added: | | 585 | + - REPLACEME | | 586 | +--> | | 587 | + | | 588 | +> Stability: 1.0 - Early development | | 589 | +
| | 590 | +Define which module system, `module` or `commonjs`, to use for the following: | | 591 | + | | 592 | +* String input provided via `--eval` or STDIN, if `--input-type` is unspecified. | | 593 | + | | 594 | +* Files ending in `.js` or with no extension, if there is no `package.json` file | | 595 | + present in the same folder or any parent folder. | | 596 | + | | 597 | +* Files ending in `.js` or with no extension, if the nearest parent | | 598 | + `package.json` field lacks a `"type"` field; unless the `package.json` folder | | 599 | + or any parent folder is inside a `node_modules` folder. | | 600 | + | | 601 | +In other words, `--experimental-default-type=module` flips all the places where | | 602 | +Node.js currently defaults to CommonJS to instead default to ECMAScript modules, | | 603 | +with the exception of folders and subfolders below `node_modules`, for backward | | 604 | +compatibility. | | 605 | + | | 606 | +Under `--experimental-default-type=module` and `--experimental-wasm-modules`, | | 607 | +files with no extension will be treated as WebAssembly if they begin with the | | 608 | +WebAssembly magic number (`\0asm`); otherwise they will be treated as ES module | | 609 | +JavaScript. | | 610 | + | 581 | 611 | ### `--experimental-import-meta-resolve` | 582 | 612 |
| 583 | 613 | <!-- YAML | @@ -2215,6 +2245,7 @@ Node.js options that are allowed are: | 2215 | 2245 | * `--enable-network-family-autoselection` | 2216 | 2246 | * `--enable-source-maps` | 2217 | 2247 | * `--experimental-abortcontroller` | | 2248 | +* `--experimental-default-type` | 2218 | 2249 | * `--experimental-import-meta-resolve` | 2219 | 2250 | * `--experimental-json-modules` | 2220 | 2251 | * `--experimental-loader` | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/esm.md +6-4Lines changed: 6 additions & 4 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -106,10 +106,11 @@ provides interoperability between them and its original module format, | 106 | 106 |
| 107 | 107 | Node.js has two module systems: [CommonJS][] modules and ECMAScript modules. | 108 | 108 |
| 109 | | -Authors can tell Node.js to use the ECMAScript modules loader | 110 | | -via the `.mjs` file extension, the `package.json` [`"type"`][] field, or the | 111 | | -[`--input-type`][] flag. Outside of those cases, Node.js will use the CommonJS | 112 | | -module loader. See [Determining module system][] for more details. | | 109 | +Authors can tell Node.js to use the ECMAScript modules loader via the `.mjs` | | 110 | +file extension, the `package.json` [`"type"`][] field, the [`--input-type`][] | | 111 | +flag, or the [`--experimental-default-type`][] flag. Outside of those cases, | | 112 | +Node.js will use the CommonJS module loader. See [Determining module system][] | | 113 | +for more details. | 113 | 114 |
| 114 | 115 | <!-- Anchors to make sure old links find a target --> | 115 | 116 |
| @@ -1059,6 +1060,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][]. | 1059 | 1060 | [URL]: https://url.spec.whatwg.org/ | 1060 | 1061 | [`"exports"`]: packages.md#exports | 1061 | 1062 | [`"type"`]: packages.md#type | | 1063 | +[`--experimental-default-type`]: cli.md#--experimental-default-typetype | 1062 | 1064 | [`--input-type`]: cli.md#--input-typetype | 1063 | 1065 | [`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs | 1064 | 1066 | [`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/packages.md +30-13Lines changed: 30 additions & 13 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -55,6 +55,8 @@ along with a reference for the [`package.json`][] fields defined by Node.js. | 55 | 55 |
| 56 | 56 | ## Determining module system | 57 | 57 |
| | 58 | +### Introduction | | 59 | + | 58 | 60 | Node.js will treat the following as [ES modules][] when passed to `node` as the | 59 | 61 | initial input, or when referenced by `import` statements or `import()` | 60 | 62 | expressions: | @@ -67,14 +69,9 @@ expressions: | 67 | 69 | * Strings passed in as an argument to `--eval`, or piped to `node` via `STDIN`, | 68 | 70 | with the flag `--input-type=module`. | 69 | 71 |
| 70 | | -Node.js will treat as [CommonJS][] all other forms of input, such as `.js` files | 71 | | -where the nearest parent `package.json` file contains no top-level `"type"` | 72 | | -field, or string input without the flag `--input-type`. This behavior is to | 73 | | -preserve backward compatibility. However, now that Node.js supports both | 74 | | -CommonJS and ES modules, it is best to be explicit whenever possible. Node.js | 75 | | -will treat the following as CommonJS when passed to `node` as the initial input, | 76 | | -or when referenced by `import` statements, `import()` expressions, or | 77 | | -`require()` expressions: | | 72 | +Node.js will treat the following as [CommonJS][] when passed to `node` as the | | 73 | +initial input, or when referenced by `import` statements or `import()` | | 74 | +expressions: | 78 | 75 |
| 79 | 76 | * Files with a `.cjs` extension. | 80 | 77 |
| @@ -84,11 +81,30 @@ or when referenced by `import` statements, `import()` expressions, or | 84 | 81 | * Strings passed in as an argument to `--eval` or `--print`, or piped to `node` | 85 | 82 | via `STDIN`, with the flag `--input-type=commonjs`. | 86 | 83 |
| 87 | | -Package authors should include the [`"type"`][] field, even in packages where | 88 | | -all sources are CommonJS. Being explicit about the `type` of the package will | 89 | | -future-proof the package in case the default type of Node.js ever changes, and | 90 | | -it will also make things easier for build tools and loaders to determine how the | 91 | | -files in the package should be interpreted. | | 84 | +Aside from these explicit cases, there are other cases where Node.js defaults to | | 85 | +one module system or the other based on the value of the | | 86 | +[`--experimental-default-type`][] flag: | | 87 | + | | 88 | +* Files ending in `.js` or with no extension, if there is no `package.json` file | | 89 | + present in the same folder or any parent folder. | | 90 | + | | 91 | +* Files ending in `.js` or with no extension, if the nearest parent | | 92 | + `package.json` field lacks a `"type"` field; unless the folder is inside a | | 93 | + `node_modules` folder. (Package scopes under `node_modules` are always treated | | 94 | + as CommonJS when the `package.json` file lacks a `"type"` field, regardless | | 95 | + of `--experimental-default-type`, for backward compatibility.) | | 96 | + | | 97 | +* Strings passed in as an argument to `--eval` or piped to `node` via `STDIN`, | | 98 | + when `--input-type` is unspecified. | | 99 | + | | 100 | +This flag currently defaults to `"commonjs"`, but it may change in the future to | | 101 | +default to `"module"`. For this reason it is best to be explicit wherever | | 102 | +possible; in particular, package authors should always include the [`"type"`][] | | 103 | +field in their `package.json` files, even in packages where all sources are | | 104 | +CommonJS. Being explicit about the `type` of the package will future-proof the | | 105 | +package in case the default type of Node.js ever changes, and it will also make | | 106 | +things easier for build tools and loaders to determine how the files in the | | 107 | +package should be interpreted. | 92 | 108 |
| 93 | 109 | ### Modules loaders | 94 | 110 |
| @@ -1337,6 +1353,7 @@ This field defines [subpath imports][] for the current package. | 1337 | 1353 | [`"packageManager"`]: #packagemanager | 1338 | 1354 | [`"type"`]: #type | 1339 | 1355 | [`--conditions` / `-C` flag]: #resolving-user-conditions | | 1356 | +[`--experimental-default-type`]: cli.md#--experimental-default-typetype | 1340 | 1357 | [`--no-addons` flag]: cli.md#--no-addons | 1341 | 1358 | [`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported | 1342 | 1359 | [`esm`]: https://github.com/standard-things/esm#readme | |
Collapse file Copy file name to clipboardExpand all lines: doc/node.1 +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -152,6 +152,11 @@ Requires Node.js to be built with | 152 | 152 | .It Fl -enable-source-maps | 153 | 153 | Enable Source Map V3 support for stack traces. | 154 | 154 | . | | 155 | +.It Fl -experimental-default-type Ns = Ns Ar type | | 156 | +Interpret as either ES modules or CommonJS modules input via --eval or STDIN, when --input-type is unspecified; | | 157 | +.js or extensionless files with no sibling or parent package.json; | | 158 | +.js or extensionless files whose nearest parent package.json lacks a "type" field, unless under node_modules. | | 159 | +. | 155 | 160 | .It Fl -experimental-global-webcrypto | 156 | 161 | Expose the Web Crypto API on the global scope. | 157 | 162 | . | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/main/check_syntax.js +2-1Lines changed: 2 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -60,7 +60,8 @@ function loadESMIfNeeded(cb) { | 60 | 60 | async function checkSyntax(source, filename) { | 61 | 61 | let isModule = true; | 62 | 62 | if (filename === '[stdin]' || filename === '[eval]') { | 63 | | - isModule = getOptionValue('--input-type') === 'module'; | | 63 | + isModule = getOptionValue('--input-type') === 'module' || | | 64 | + (getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs'); | 64 | 65 | } else { | 65 | 66 | const { defaultResolve } = require('internal/modules/esm/resolve'); | 66 | 67 | const { defaultGetFormat } = require('internal/modules/esm/get_format'); | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/main/eval_stdin.js +4-2Lines changed: 4 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -25,12 +25,14 @@ readStdin((code) => { | 25 | 25 |
| 26 | 26 | const print = getOptionValue('--print'); | 27 | 27 | const loadESM = getOptionValue('--import').length > 0; | 28 | | - if (getOptionValue('--input-type') === 'module') | | 28 | + if (getOptionValue('--input-type') === 'module' || | | 29 | + (getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) { | 29 | 30 | evalModule(code, print); | 30 | | - else | | 31 | + } else { | 31 | 32 | evalScript('[stdin]', | 32 | 33 | code, | 33 | 34 | getOptionValue('--inspect-brk'), | 34 | 35 | print, | 35 | 36 | loadESM); | | 37 | + } | 36 | 38 | }); |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/main/eval_string.js +3-2Lines changed: 3 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -25,9 +25,10 @@ markBootstrapComplete(); | 25 | 25 | const source = getOptionValue('--eval'); | 26 | 26 | const print = getOptionValue('--print'); | 27 | 27 | const loadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0; | 28 | | -if (getOptionValue('--input-type') === 'module') | | 28 | +if (getOptionValue('--input-type') === 'module' || | | 29 | + (getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) { | 29 | 30 | evalModule(source, print); | 30 | | -else { | | 31 | +} else { | 31 | 32 | // For backward compatibility, we want the identifier crypto to be the | 32 | 33 | // `node:crypto` module rather than WebCrypto. | 33 | 34 | const isUsingCryptoIdentifier = | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/formats.js +29Lines changed: 29 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 2 | 2 |
| 3 | 3 | const { | 4 | 4 | RegExpPrototypeExec, | | 5 | + Uint8Array, | 5 | 6 | } = primordials; | 6 | 7 | const { getOptionValue } = require('internal/options'); | 7 | 8 |
| | 9 | +const { closeSync, openSync, readSync } = require('fs'); | | 10 | + | 8 | 11 | const experimentalWasmModules = getOptionValue('--experimental-wasm-modules'); | 9 | 12 |
| 10 | 13 | const extensionFormatMap = { | @@ -35,7 +38,33 @@ function mimeToFormat(mime) { | 35 | 38 | return null; | 36 | 39 | } | 37 | 40 |
| | 41 | +/** | | 42 | + * For extensionless files in a `module` package scope, or a default `module` scope enabled by the | | 43 | + * `--experimental-default-type` flag, we check the file contents to disambiguate between ES module JavaScript and Wasm. | | 44 | + * We do this by taking advantage of the fact that all Wasm files start with the header `0x00 0x61 0x73 0x6d` (`_asm`). | | 45 | + * @param {URL} url | | 46 | + */ | | 47 | +function getFormatOfExtensionlessFile(url) { | | 48 | + if (!experimentalWasmModules) { return 'module'; } | | 49 | + | | 50 | + const magic = new Uint8Array(4); | | 51 | + let fd; | | 52 | + try { | | 53 | + // TODO(@anonrig): Optimize the following by having a single C++ call | | 54 | + fd = openSync(url); | | 55 | + readSync(fd, magic, 0, 4); // Only read the first four bytes | | 56 | + if (magic[0] === 0x00 && magic[1] === 0x61 && magic[2] === 0x73 && magic[3] === 0x6d) { | | 57 | + return 'wasm'; | | 58 | + } | | 59 | + } finally { | | 60 | + if (fd !== undefined) { closeSync(fd); } | | 61 | + } | | 62 | + | | 63 | + return 'module'; | | 64 | +} | | 65 | + | 38 | 66 | module.exports = { | 39 | 67 | extensionFormatMap, | | 68 | + getFormatOfExtensionlessFile, | 40 | 69 | mimeToFormat, | 41 | 70 | }; |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/get_format.js +52-7Lines changed: 52 additions & 7 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use strict'; | | 2 | + | 2 | 3 | const { | 3 | 4 | RegExpPrototypeExec, | 4 | 5 | ObjectPrototypeHasOwnProperty, | 5 | 6 | PromisePrototypeThen, | 6 | 7 | PromiseResolve, | | 8 | + StringPrototypeIncludes, | 7 | 9 | StringPrototypeCharCodeAt, | 8 | 10 | StringPrototypeSlice, | 9 | 11 | } = primordials; | 10 | 12 | const { basename, relative } = require('path'); | 11 | 13 | const { getOptionValue } = require('internal/options'); | 12 | 14 | const { | 13 | 15 | extensionFormatMap, | | 16 | + getFormatOfExtensionlessFile, | 14 | 17 | mimeToFormat, | 15 | 18 | } = require('internal/modules/esm/formats'); | 16 | 19 |
| 17 | 20 | const experimentalNetworkImports = | 18 | 21 | getOptionValue('--experimental-network-imports'); | | 22 | +const defaultTypeFlag = getOptionValue('--experimental-default-type'); | | 23 | +// The next line is where we flip the default to ES modules someday. | | 24 | +const defaultType = defaultTypeFlag === 'module' ? 'module' : 'commonjs'; | 19 | 25 | const { getPackageType, getPackageScopeConfig } = require('internal/modules/esm/resolve'); | 20 | 26 | const { fileURLToPath } = require('internal/url'); | 21 | 27 | const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes; | @@ -66,6 +72,18 @@ function extname(url) { | 66 | 72 | return ''; | 67 | 73 | } | 68 | 74 |
| | 75 | +/** | | 76 | + * Determine whether the given file URL is under a `node_modules` folder. | | 77 | + * This function assumes that the input has already been verified to be a `file:` URL, | | 78 | + * and is a file rather than a folder. | | 79 | + * @param {URL} url | | 80 | + */ | | 81 | +function underNodeModules(url) { | | 82 | + if (url.protocol !== 'file:') { return false; } // We determine module types for other protocols based on MIME header | | 83 | + | | 84 | + return StringPrototypeIncludes(url.pathname, '/node_modules/'); | | 85 | +} | | 86 | + | 69 | 87 | /** | 70 | 88 | * @param {URL} url | 71 | 89 | * @param {{parentURL: string}} context | @@ -74,8 +92,37 @@ function extname(url) { | 74 | 92 | */ | 75 | 93 | function getFileProtocolModuleFormat(url, context, ignoreErrors) { | 76 | 94 | const ext = extname(url); | | 95 | + | 77 | 96 | if (ext === '.js') { | 78 | | - return getPackageType(url) === 'module' ? 'module' : 'commonjs'; | | 97 | + const packageType = getPackageType(url); | | 98 | + if (packageType !== 'none') { | | 99 | + return packageType; | | 100 | + } | | 101 | + // The controlling `package.json` file has no `type` field. | | 102 | + if (defaultType === 'module') { | | 103 | + // An exception to the type flag making ESM the default everywhere is that package scopes under `node_modules` | | 104 | + // should retain the assumption that a lack of a `type` field means CommonJS. | | 105 | + return underNodeModules(url) ? 'commonjs' : 'module'; | | 106 | + } | | 107 | + return 'commonjs'; | | 108 | + } | | 109 | + | | 110 | + if (ext === '') { | | 111 | + const packageType = getPackageType(url); | | 112 | + if (defaultType === 'commonjs') { // Legacy behavior | | 113 | + if (packageType === 'none' || packageType === 'commonjs') { | | 114 | + return 'commonjs'; | | 115 | + } | | 116 | + // If package type is `module`, fall through to the error case below | | 117 | + } else { // Else defaultType === 'module' | | 118 | + if (underNodeModules(url)) { // Exception for package scopes under `node_modules` | | 119 | + return 'commonjs'; | | 120 | + } | | 121 | + if (packageType === 'none' || packageType === 'module') { | | 122 | + return getFormatOfExtensionlessFile(url); | | 123 | + } // Else packageType === 'commonjs' | | 124 | + return 'commonjs'; | | 125 | + } | 79 | 126 | } | 80 | 127 |
| 81 | 128 | const format = extensionFormatMap[ext]; | @@ -89,12 +136,10 @@ function getFileProtocolModuleFormat(url, context, ignoreErrors) { | 89 | 136 | const config = getPackageScopeConfig(url); | 90 | 137 | const fileBasename = basename(filepath); | 91 | 138 | const relativePath = StringPrototypeSlice(relative(config.pjsonPath, filepath), 1); | 92 | | - suggestion = 'Loading extensionless files is not supported inside of ' + | 93 | | - '"type":"module" package.json contexts. The package.json file ' + | 94 | | - `${config.pjsonPath} caused this "type":"module" context. Try ` + | 95 | | - `changing ${filepath} to have a file extension. Note the "bin" ` + | 96 | | - 'field of package.json can point to a file with an extension, for example ' + | 97 | | - `{"type":"module","bin":{"${fileBasename}":"${relativePath}.js"}}`; | | 139 | + suggestion = 'Loading extensionless files is not supported inside of "type":"module" package.json contexts ' + | | 140 | + `without --experimental-default-type=module. The package.json file ${config.pjsonPath} caused this "type":"module" ` + | | 141 | + `context. Try changing ${filepath} to have a file extension. Note the "bin" field of package.json can point ` + | | 142 | + `to a file with an extension, for example {"type":"module","bin":{"${fileBasename}":"${relativePath}.js"}}`; | 98 | 143 | } | 99 | 144 | throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath, suggestion); | 100 | 145 | } | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/resolve.js +2-2Lines changed: 2 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -35,7 +35,7 @@ const preserveSymlinks = getOptionValue('--preserve-symlinks'); | 35 | 35 | const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); | 36 | 36 | const experimentalNetworkImports = | 37 | 37 | getOptionValue('--experimental-network-imports'); | 38 | | -const typeFlag = getOptionValue('--input-type'); | | 38 | +const inputTypeFlag = getOptionValue('--input-type'); | 39 | 39 | const { URL, pathToFileURL, fileURLToPath, isURL } = require('internal/url'); | 40 | 40 | const { getCWDURL } = require('internal/util'); | 41 | 41 | const { canParse: URLCanParse } = internalBinding('url'); | @@ -1112,7 +1112,7 @@ function defaultResolve(specifier, context = {}) { | 1112 | 1112 | // input, to avoid user confusion over how expansive the effect of the | 1113 | 1113 | // flag should be (i.e. entry point only, package scope surrounding the | 1114 | 1114 | // entry point, etc.). | 1115 | | - if (typeFlag) { throw new ERR_INPUT_TYPE_NOT_ALLOWED(); } | | 1115 | + if (inputTypeFlag) { throw new ERR_INPUT_TYPE_NOT_ALLOWED(); } | 1116 | 1116 | } | 1117 | 1117 |
| 1118 | 1118 | conditions = getConditionsSet(conditions); | |
|