diff --git a/.eslintrc.js b/.eslintrc.js index 8279dfc9c4ab41..fe936370a37557 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,6 +44,7 @@ module.exports = { files: [ 'doc/api/esm.md', 'doc/api/modules.md', + 'doc/api/packages.md', 'test/es-module/test-esm-type-flag.js', 'test/es-module/test-esm-type-flag-alias.js', '*.mjs', diff --git a/LICENSE b/LICENSE index a42e1d52038bb0..298631ee09e2dd 100644 --- a/LICENSE +++ b/LICENSE @@ -137,6 +137,20 @@ The externally maintained libraries used by Node.js are: IN THE SOFTWARE. """ +- cjs-module-lexer, located at deps/cjs-module-lexer, is licensed as follows: + """ + MIT License + ----------- + + Copyright (C) 2018-2020 Guy Bedford + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + """ + - ICU, located at deps/icu-small, is licensed as follows: """ COPYRIGHT AND PERMISSION NOTICE (ICU 58 and later) diff --git a/deps/cjs-module-lexer/.gitignore b/deps/cjs-module-lexer/.gitignore new file mode 100755 index 00000000000000..55ee2f0d6cfb5d --- /dev/null +++ b/deps/cjs-module-lexer/.gitignore @@ -0,0 +1,11 @@ +node_modules +*.lock +test +.* +Makefile +bench +build.js +include-wasm +include +lib +src diff --git a/deps/cjs-module-lexer/LICENSE b/deps/cjs-module-lexer/LICENSE new file mode 100755 index 00000000000000..935b357962d08b --- /dev/null +++ b/deps/cjs-module-lexer/LICENSE @@ -0,0 +1,10 @@ +MIT License +----------- + +Copyright (C) 2018-2020 Guy Bedford + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/deps/cjs-module-lexer/README.md b/deps/cjs-module-lexer/README.md new file mode 100755 index 00000000000000..726dd407769398 --- /dev/null +++ b/deps/cjs-module-lexer/README.md @@ -0,0 +1,331 @@ +# CJS Module Lexer + +[![Build Status][travis-image]][travis-url] + +A [very fast](#benchmarks) JS CommonJS module syntax lexer used to detect the most likely list of named exports of a CommonJS module. + +Outputs the list of named exports (`exports.name = ...`) and possible module reexports (`module.exports = require('...')`), including the common transpiler variations of these cases. + +Forked from https://github.com/guybedford/es-module-lexer. + +_Comprehensively handles the JS language grammar while remaining small and fast. - ~90ms per MB of JS cold and ~15ms per MB of JS warm, [see benchmarks](#benchmarks) for more info._ + +### Usage + +``` +npm install cjs-module-lexer +``` + +For use in CommonJS: + +```js +const parse = require('cjs-module-lexer'); + +const { exports, reexports } = parse(` + // named exports detection + module.exports.a = 'a'; + (function () { + exports.b = 'b'; + })(); + Object.defineProperty(exports, 'c', { value: 'c' }); + /* exports.d = 'not detected'; */ + + // reexports detection + if (maybe) module.exports = require('./dep1.js'); + if (another) module.exports = require('./dep2.js'); + + // literal exports assignments + module.exports = { a, b: c, d, 'e': f } + + // __esModule detection + Object.defineProperty(module.exports, '__esModule', { value: true }) +`); + +// exports === ['a', 'b', 'c', '__esModule'] +// reexports === ['./dep1.js', './dep2.js'] +``` + +When using the ESM version, Wasm is supported instead: + +```js +import { parse, init } from 'cjs-module-lexer'; +// init needs to be called and waited upon +await init(); +const { exports, reexports } = parse(source); +``` + +The Wasm build is around 1.5x faster and without a cold start. + +### Grammar + +CommonJS exports matches are run against the source token stream. + +The token grammar is: + +``` +IDENTIFIER: As defined by ECMA-262, without support for identifier `\` escapes, filtered to remove strict reserved words: + "implements", "interface", "let", "package", "private", "protected", "public", "static", "yield", "enum" + +STRING_LITERAL: A `"` or `'` bounded ECMA-262 string literal. + +IDENTIFIER_STRING: ( `"` IDENTIFIER `"` | `'` IDENTIFIER `'` ) + +COMMENT_SPACE: Any ECMA-262 whitespace, ECMA-262 block comment or ECMA-262 line comment + +MODULE_EXPORTS: `module` COMMENT_SPACE `.` COMMENT_SPACE `exports` + +EXPORTS_IDENTIFIER: MODULE_EXPORTS_IDENTIFIER | `exports` + +EXPORTS_DOT_ASSIGN: EXPORTS_IDENTIFIER COMMENT_SPACE `.` COMMENT_SPACE IDENTIFIER COMMENT_SPACE `=` + +EXPORTS_LITERAL_COMPUTED_ASSIGN: EXPORTS_IDENTIFIER COMMENT_SPACE `[` COMMENT_SPACE IDENTIFIER_STRING COMMENT_SPACE `]` COMMENT_SPACE `=` + +EXPORTS_LITERAL_PROP: (IDENTIFIER (COMMENT_SPACE `:` COMMENT_SPACE IDENTIFIER)?) | (IDENTIFIER_STRING COMMENT_SPACE `:` COMMENT_SPACE IDENTIFIER) + +EXPORTS_MEMBER: EXPORTS_DOT_ASSIGN | EXPORTS_LITERAL_COMPUTED_ASSIGN + +EXPORTS_DEFINE: `Object` COMMENT_SPACE `.` COMMENT_SPACE `defineProperty COMMENT_SPACE `(` EXPORTS_IDENTIFIER COMMENT_SPACE `,` COMMENT_SPACE IDENTIFIER_STRING + +EXPORTS_LITERAL: MODULE_EXPORTS COMMENT_SPACE `=` COMMENT_SPACE `{` COMMENT_SPACE (EXPORTS_LITERAL_PROP COMMENT_SPACE `,` COMMENT_SPACE)+ `}` + +REQUIRE: `require` COMMENT_SPACE `(` COMMENT_SPACE STRING_LITERAL COMMENT_SPACE `)` + +EXPORTS_ASSIGN: (`var` | `const` | `let`) IDENTIFIER `=` REQUIRE + +MODULE_EXPORTS_ASSIGN: MODULE_EXPORTS COMMENT_SPACE `=` COMMENT_SPACE REQUIRE + +EXPORT_STAR: (`__export` | `__exportStar`) `(` REQUIRE + +EXPORT_STAR_LIB: `Object.keys(` IDENTIFIER$1 `).forEach(function (` IDENTIFIER$2 `) {` + ( + `if (` IDENTIFIER$2 `===` ( `'default'` | `"default"` ) `||` IDENTIFIER$2 `===` ( '__esModule' | `"__esModule"` ) `) return` `;`? | + `if (` IDENTIFIER$2 `!==` ( `'default'` | `"default"` ) `)` + ) + ( + EXPORTS_IDENTIFIER `[` IDENTIFIER$2 `] =` IDENTIFIER$1 `[` IDENTIFIER$2 `]` `;`? | + `Object.defineProperty(` EXPORTS_IDENTIFIER `, ` IDENTIFIER$2 `, { enumerable: true, get: function () { return ` IDENTIFIER$1 `[` IDENTIFIER$2 `]` `;`? } })` `;`? + ) + `})` +``` + +* The returned export names are the matched `IDENTIFIER` and `IDENTIFIER_STRING` slots for all `EXPORTS_MEMBER`, `EXPORTS_DEFINE` and `EXPORTS_LITERAL` matches. +* The reexport specifiers are taken to be the `STRING_LITERAL` slots of all `MODULE_EXPORTS_ASSIGN` as well as all _top-level_ `EXPORT_STAR` `REQUIRE` matches and `EXPORTS_ASSIGN` matches whose `IDENTIFIER` also matches the first `IDENTIFIER` in `EXPORT_STAR_LIB`. + +### Parsing Examples + +#### Named Exports Parsing + +The basic matching rules for named exports are `exports.name`, `exports['name']` or `Object.defineProperty(exports, 'name', ...)`. This matching is done without scope analysis and regardless of the expression position: + +```js +// DETECTS EXPORTS: a, b, c +(function (exports) { + exports.a = 'a'; + exports['b'] = 'b'; + Object.defineProperty(exports, 'c', { value: 'c' }); +})(exports); +``` + +Because there is no scope analysis, the above detection may overclassify: + +```js +// DETECTS EXPORTS: a, b, c +(function (exports, Object) { + exports.a = 'a'; + exports['b'] = 'b'; + if (false) + Object.defineProperty(exports, 'c', { value: 'c' }); +})(NOT_EXPORTS, NOT_OBJECT); +``` + +It will in turn underclassify in cases where the identifiers are renamed: + +```js +// DETECTS: NO EXPORTS +(function (e, defineProperty) { + e.a = 'a'; + e['b'] = 'b'; + defineProperty(e, 'c', { value: 'c' }); +})(exports, defineProperty); +``` + +#### Exports Object Assignment + +A best-effort is made to detect `module.exports` object assignments, but because this is not a full parser, arbitrary expressions are not handled in the +object parsing process. + +Simple object definitions are supported: + +```js +// DETECTS EXPORTS: a, b, c +module.exports = { + a, + b: 'c', + c: c +}; +``` + +Object properties that are not identifiers or string expressions will bail out of the object detection: + +```js +// DETECTS EXPORTS: a, b +module.exports = { + a, + b: require('c'), + c: "not detected since require('c') above bails the object detection" +} +``` + +`Object.defineProperties` is not currently supported either. + +#### module.exports reexport assignment + +Any `module.exports = require('mod')` assignment is detected as a reexport: + +```js +// DETECTS REEXPORTS: a, b, c +module.exports = require('a'); +(module => module.exports = require('b'))(NOT_MODULE); +if (false) module.exports = require('c'); +``` + +As a result, the total list of exports would be inferred as the union of all of these reexported modules, which can lead to possible over-classification. + +#### Transpiler Re-exports + +For named exports, transpiler output works well with the rules described above. + +But for star re-exports, special care is taken to support common patterns of transpiler outputs from Babel and TypeScript as well as bundlers like RollupJS. +These reexport and star reexport patterns are restricted to only be detected at the top-level as provided by the direct output of these tools. + +For example, `export * from 'external'` is output by Babel as: + +```js +"use strict"; + +exports.__esModule = true; + +var _external = require("external"); + +Object.keys(_external).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + exports[key] = _external[key]; +}); +``` + +Where the `var _external = require("external")` is specifically detected as well as the `Object.keys(_external)` statement, down to the exact +for of that entire expression including minor variations of the output. The `_external` and `key` identifiers are carefully matched in this +detection. + +Similarly for TypeScript, `export * from 'external'` is output as: + +```js +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +Object.defineProperty(exports, "__esModule", { value: true }); +__export(require("external")); +``` + +Where the `__export(require("external"))` statement is explicitly detected as a reexport, including variations `tslib.__export` and `__exportStar`. + +### Environment Support + +Node.js 10+, and [all browsers with Web Assembly support](https://caniuse.com/#feat=wasm). + +### JS Grammar Support + +* Token state parses all line comments, block comments, strings, template strings, blocks, parens and punctuators. +* Division operator / regex token ambiguity is handled via backtracking checks against punctuator prefixes, including closing brace or paren backtracking. +* Always correctly parses valid JS source, but may parse invalid JS source without errors. + +### Benchmarks + +Benchmarks can be run with `npm run bench`. + +Current results: + +JS Build: + +``` +Module load time +> 2ms +Cold Run, All Samples +test/samples/*.js (3635 KiB) +> 333ms + +Warm Runs (average of 25 runs) +test/samples/angular.js (1410 KiB) +> 16.48ms +test/samples/angular.min.js (303 KiB) +> 5.36ms +test/samples/d3.js (553 KiB) +> 8.32ms +test/samples/d3.min.js (250 KiB) +> 4.28ms +test/samples/magic-string.js (34 KiB) +> 1ms +test/samples/magic-string.min.js (20 KiB) +> 0.36ms +test/samples/rollup.js (698 KiB) +> 10.48ms +test/samples/rollup.min.js (367 KiB) +> 6.64ms + +Warm Runs, All Samples (average of 25 runs) +test/samples/*.js (3635 KiB) +> 49.28ms +``` + +Wasm Build: +``` +Module load time +> 11ms +Cold Run, All Samples +test/samples/*.js (3635 KiB) +> 48ms + +Warm Runs (average of 25 runs) +test/samples/angular.js (1410 KiB) +> 12.32ms +test/samples/angular.min.js (303 KiB) +> 3.76ms +test/samples/d3.js (553 KiB) +> 6.08ms +test/samples/d3.min.js (250 KiB) +> 3ms +test/samples/magic-string.js (34 KiB) +> 0.24ms +test/samples/magic-string.min.js (20 KiB) +> 0ms +test/samples/rollup.js (698 KiB) +> 7.2ms +test/samples/rollup.min.js (367 KiB) +> 4.2ms + +Warm Runs, All Samples (average of 25 runs) +test/samples/*.js (3635 KiB) +> 33.6ms +``` + +### Wasm Build Steps + +To build download the WASI SDK from https://github.com/CraneStation/wasi-sdk/releases. + +The Makefile assumes the existence of "wasi-sdk-10.0", "binaryen" and "wabt" (both optional) as sibling folders to this project. + +The build through the Makefile is then run via `make lib/lexer.wasm`, which can also be triggered via `npm run build-wasm` to create `dist/lexer.js`. + +On Windows it may be preferable to use the Linux subsystem. + +After the Web Assembly build, the CJS build can be triggered via `npm run build`. + +Optimization passes are run with [Binaryen](https://github.com/WebAssembly/binaryen) prior to publish to reduce the Web Assembly footprint. + +### License + +MIT + +[travis-url]: https://travis-ci.org/guybedford/es-module-lexer +[travis-image]: https://travis-ci.org/guybedford/es-module-lexer.svg?branch=master diff --git a/deps/cjs-module-lexer/lexer.js b/deps/cjs-module-lexer/lexer.js new file mode 100755 index 00000000000000..7f7c971fcc15bd --- /dev/null +++ b/deps/cjs-module-lexer/lexer.js @@ -0,0 +1,1169 @@ +let source, pos, end; +let openTokenDepth, + templateDepth, + lastTokenPos, + lastSlashWasDivision, + templateStack, + templateStackDepth, + openTokenPosStack, + openClassPosStack, + nextBraceIsClass, + starExportMap, + lastStarExportSpecifier, + lastExportsAssignSpecifier, + _exports, + reexports; + +function resetState () { + openTokenDepth = 0; + templateDepth = -1; + lastTokenPos = -1; + lastSlashWasDivision = false; + templateStack = new Array(1024); + templateStackDepth = 0; + openTokenPosStack = new Array(1024); + openClassPosStack = new Array(1024); + nextBraceIsClass = false; + starExportMap = Object.create(null); + lastStarExportSpecifier = null; + lastExportsAssignSpecifier = null; + + _exports = new Set(); + reexports = new Set(); +} + +// RequireType +const Import = 0; +const ExportAssign = 1; +const ExportStar = 2; + +const strictReserved = new Set(['implements', 'interface', 'let', 'package', 'private', 'protected', 'public', 'static', 'yield', 'enum']); + +module.exports = function parseCJS (source, name = '@') { + resetState(); + try { + parseSource(source); + } + catch (e) { + e.message += `\n at ${name}:${source.slice(0, pos).split('\n').length}:${pos - source.lastIndexOf('\n', pos - 1)}`; + e.loc = pos; + throw e; + } + if (lastExportsAssignSpecifier) + reexports.add(lastExportsAssignSpecifier); + const result = { exports: [..._exports], reexports: [...reexports] }; + resetState(); + return result; +} + +function addExport (name) { + if (!strictReserved.has(name)) + _exports.add(name); +} + +function parseSource (cjsSource) { + source = cjsSource; + pos = -1; + end = source.length - 1; + let ch = 0; + + // Handle #! + if (source.charCodeAt(0) === 35/*#*/ && source.charCodeAt(1) === 33/*!*/) { + if (source.length === 2) + return true; + pos += 2; + while (pos++ < end) { + ch = source.charCodeAt(pos); + if (ch === 10/*\n*/ || ch === 13/*\r*/) + break; + } + } + + while (pos++ < end) { + ch = source.charCodeAt(pos); + + if (ch === 32 || ch < 14 && ch > 8) + continue; + + if (openTokenDepth === 0) { + switch (ch) { + case 105/*i*/: + if (source.startsWith('mport', pos + 1) && keywordStart(pos)) + throwIfImportStatement(); + lastTokenPos = pos; + continue; + case 114/*r*/: + const startPos = pos; + if (tryParseRequire(Import) && keywordStart(startPos)) + tryBacktrackAddStarExportBinding(startPos - 1); + lastTokenPos = pos; + continue; + case 95/*_*/: + if (source.startsWith('_export', pos + 1) && (keywordStart(pos) || source.charCodeAt(pos - 1) === 46/*.*/)) { + pos += 8; + if (source.startsWith('Star', pos)) + pos += 4; + if (source.charCodeAt(pos) === 40/*(*/) { + openTokenPosStack[openTokenDepth++] = lastTokenPos; + if (source.charCodeAt(++pos) === 114/*r*/) + tryParseRequire(ExportStar); + } + } + lastTokenPos = pos; + continue; + } + } + + switch (ch) { + case 101/*e*/: + if (source.startsWith('xport', pos + 1) && keywordStart(pos)) { + if (source.charCodeAt(pos + 6) === 115/*s*/) + tryParseExportsDotAssign(false); + else if (openTokenDepth === 0) + throwIfExportStatement(); + } + break; + case 99/*c*/: + if (keywordStart(pos) && source.startsWith('lass', pos + 1) && isBrOrWs(source.charCodeAt(pos + 5))) + nextBraceIsClass = true; + break; + case 109/*m*/: + if (source.startsWith('odule', pos + 1) && keywordStart(pos)) + tryParseModuleExportsDotAssign(); + break; + case 79/*O*/: + if (source.startsWith('bject', pos + 1) && keywordStart(pos)) + tryParseObjectDefineOrKeys(openTokenDepth === 0); + break; + case 40/*(*/: + openTokenPosStack[openTokenDepth++] = lastTokenPos; + break; + case 41/*)*/: + if (openTokenDepth === 0) + throw new Error('Unexpected closing bracket.'); + openTokenDepth--; + break; + case 123/*{*/: + openClassPosStack[openTokenDepth] = nextBraceIsClass; + nextBraceIsClass = false; + openTokenPosStack[openTokenDepth++] = lastTokenPos; + break; + case 125/*}*/: + if (openTokenDepth === 0) + throw new Error('Unexpected closing brace.'); + if (openTokenDepth-- === templateDepth) { + templateDepth = templateStack[--templateStackDepth]; + templateString(); + } + else { + if (templateDepth !== -1 && openTokenDepth < templateDepth) + throw new Error('Unexpected closing brace.'); + } + break; + case 60/*>*/: + // TODO: ```js @@ -2529,7 +2529,8 @@ closed. [crypto digest algorithm]: crypto.html#crypto_crypto_gethashes [domains]: domain.html [event emitter-based]: events.html#events_class_eventemitter -[exports]: esm.html#esm_package_entry_points +[`package.json`]: packages.html#packages_node_js_package_json_field_definitions +[`"exports"`]: packages.html#packages_exports [file descriptors]: https://en.wikipedia.org/wiki/File_descriptor [policy]: policy.html [stream-based]: stream.html @@ -2537,6 +2538,6 @@ closed. [Subresource Integrity specification]: https://www.w3.org/TR/SRI/#the-integrity-attribute [try-catch]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch [vm]: vm.html -[self-reference a package using its name]: esm.html#esm_self_referencing_a_package_using_its_name -[define a custom subpath]: esm.html#esm_subpath_exports -["imports" field]: esm.html#esm_internal_package_imports +[self-reference a package using its name]: packages.html#packages_self_referencing_a_package_using_its_name +[define a custom subpath]: packages.html#packages_subpath_exports +[`"imports"`]: packages.html#packages_imports diff --git a/doc/api/esm.md b/doc/api/esm.md index 341bc4c3cd64f4..d9f4007c494825 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -1,7 +1,27 @@ -# ECMAScript modules +# Modules: ECMAScript modules + > Stability: 1 - Experimental @@ -44,822 +64,41 @@ Node.js contains support for ES Modules based upon the Expect major changes in the implementation including interoperability support, specifier resolution, and default behavior. + + + + + ## Enabling -Experimental support for ECMAScript modules is enabled by default. -Node.js will treat the following as ES modules when passed to `node` as the -initial input, or when referenced by `import` statements within ES module code: - -* Files ending in `.mjs`. - -* Files ending in `.js` when the nearest parent `package.json` file contains a - top-level field `"type"` with a value of `"module"`. - -* Strings passed in as an argument to `--eval` or `--print`, or piped to - `node` via `STDIN`, with the flag `--input-type=module`. - -Node.js will treat as CommonJS all other forms of input, such as `.js` files -where the nearest parent `package.json` file contains no top-level `"type"` -field, or string input without the flag `--input-type`. This behavior is to -preserve backward compatibility. However, now that Node.js supports both -CommonJS and ES modules, it is best to be explicit whenever possible. Node.js -will treat the following as CommonJS when passed to `node` as the initial input, -or when referenced by `import` statements within ES module code: - -* Files ending in `.cjs`. - -* Files ending in `.js` when the nearest parent `package.json` file contains a - top-level field `"type"` with a value of `"commonjs"`. - -* Strings passed in as an argument to `--eval` or `--print`, or piped to - `node` via `STDIN`, with the flag `--input-type=commonjs`. - -### `package.json` `"type"` field - -Files ending with `.js` will be loaded as ES modules when the nearest parent -`package.json` file contains a top-level field `"type"` with a value of -`"module"`. - -The nearest parent `package.json` is defined as the first `package.json` found -when searching in the current folder, that folder’s parent, and so on up -until the root of the volume is reached. - - -```js -// package.json -{ - "type": "module" -} -``` - -```bash -# In same folder as preceding package.json -node my-app.js # Runs as ES module -``` - -If the nearest parent `package.json` lacks a `"type"` field, or contains -`"type": "commonjs"`, `.js` files are treated as CommonJS. If the volume root is -reached and no `package.json` is found, Node.js defers to the default, a -`package.json` with no `"type"` field. - -`import` statements of `.js` files are treated as ES modules if the nearest -parent `package.json` contains `"type": "module"`. - -```js -// my-app.js, part of the same example as above -import './startup.js'; // Loaded as ES module because of package.json -``` - -Package authors should include the `"type"` field, even in packages where all -sources are CommonJS. Being explicit about the `type` of the package will -future-proof the package in case the default type of Node.js ever changes, and -it will also make things easier for build tools and loaders to determine how the -files in the package should be interpreted. - -Regardless of the value of the `"type"` field, `.mjs` files are always treated -as ES modules and `.cjs` files are always treated as CommonJS. - -### Package scope and file extensions - -A folder containing a `package.json` file, and all subfolders below that folder -until the next folder containing another `package.json`, are a -_package scope_. The `"type"` field defines how to treat `.js` files -within the package scope. Every package in a -project’s `node_modules` folder contains its own `package.json` file, so each -project’s dependencies have their own package scopes. If a `package.json` file -does not have a `"type"` field, the default `"type"` is `"commonjs"`. - -The package scope applies not only to initial entry points (`node my-app.js`) -but also to files referenced by `import` statements and `import()` expressions. - -```js -// my-app.js, in an ES module package scope because there is a package.json -// file in the same folder with "type": "module". - -import './startup/init.js'; -// Loaded as ES module since ./startup contains no package.json file, -// and therefore inherits the ES module package scope from one level up. - -import 'commonjs-package'; -// Loaded as CommonJS since ./node_modules/commonjs-package/package.json -// lacks a "type" field or contains "type": "commonjs". - -import './node_modules/commonjs-package/index.js'; -// Loaded as CommonJS since ./node_modules/commonjs-package/package.json -// lacks a "type" field or contains "type": "commonjs". -``` - -Files ending with `.mjs` are always loaded as ES modules regardless of package -scope. - -Files ending with `.cjs` are always loaded as CommonJS regardless of package -scope. - -```js -import './legacy-file.cjs'; -// Loaded as CommonJS since .cjs is always loaded as CommonJS. - -import 'commonjs-package/src/index.mjs'; -// Loaded as ES module since .mjs is always loaded as ES module. -``` - -The `.mjs` and `.cjs` extensions may be used to mix types within the same -package scope: - -* Within a `"type": "module"` package scope, Node.js can be instructed to - interpret a particular file as CommonJS by naming it with a `.cjs` extension - (since both `.js` and `.mjs` files are treated as ES modules within a - `"module"` package scope). - -* Within a `"type": "commonjs"` package scope, Node.js can be instructed to - interpret a particular file as an ES module by naming it with an `.mjs` - extension (since both `.js` and `.cjs` files are treated as CommonJS within a - `"commonjs"` package scope). - -### `--input-type` flag - -Strings passed in as an argument to `--eval` or `--print` (or `-e` or `-p`), or -piped to `node` via `STDIN`, will be treated as ES modules when the -`--input-type=module` flag is set. - -```bash -node --input-type=module --eval "import { sep } from 'path'; console.log(sep);" - -echo "import { sep } from 'path'; console.log(sep);" | node --input-type=module -``` - -For completeness there is also `--input-type=commonjs`, for explicitly running -string input as CommonJS. This is the default behavior if `--input-type` is -unspecified. +Node.js treats JavaScript code as CommonJS modules by default. +Authors can tell Node.js to treat JavaScript code as ECMAScript modules +via the `.mjs` file extension, the `package.json` [`"type"`][] field, or the +`--input-type` flag. See +[Modules: Packages](packages.html#packages_determining_module_system) for more +details. + + + + + + + + + + + + + + + + ## Packages -### Package entry points - -In a package’s `package.json` file, two fields can define entry points for a -package: `"main"` and `"exports"`. The `"main"` field is supported in all -versions of Node.js, but its capabilities are limited: it only defines the main -entry point of the package. - -The `"exports"` field provides an alternative to `"main"` where the package -main entry point can be defined while also encapsulating the package, -**preventing any other entry points besides those defined in `"exports"`**. -This encapsulation allows module authors to define a public interface for -their package. - -If both `"exports"` and `"main"` are defined, the `"exports"` field takes -precedence over `"main"`. `"exports"` are not specific to ES modules or -CommonJS; `"main"` will be overridden by `"exports"` if it exists. As such -`"main"` cannot be used as a fallback for CommonJS but it can be used as a -fallback for legacy versions of Node.js that do not support the `"exports"` -field. - -[Conditional exports][] can be used within `"exports"` to define different -package entry points per environment, including whether the package is -referenced via `require` or via `import`. For more information about supporting -both CommonJS and ES Modules in a single package please consult -[the dual CommonJS/ES module packages section][]. - -**Warning**: Introducing the `"exports"` field prevents consumers of a package -from using any entry points that are not defined, including the `package.json` -(e.g. `require('your-package/package.json')`. **This will likely be a breaking -change.** - -To make the introduction of `"exports"` non-breaking, ensure that every -previously supported entry point is exported. It is best to explicitly specify -entry points so that the package’s public API is well-defined. For example, -a project that previous exported `main`, `lib`, -`feature`, and the `package.json` could use the following `package.exports`: - -```json -{ - "name": "my-mod", - "exports": { - ".": "./lib/index.js", - "./lib": "./lib/index.js", - "./lib/index": "./lib/index.js", - "./lib/index.js": "./lib/index.js", - "./feature": "./feature/index.js", - "./feature/index.js": "./feature/index.js", - "./package.json": "./package.json" - } -} -``` - -Alternatively a project could choose to export entire folders: - -```json -{ - "name": "my-mod", - "exports": { - ".": "./lib/index.js", - "./lib": "./lib/index.js", - "./lib/": "./lib/", - "./feature": "./feature/index.js", - "./feature/": "./feature/", - "./package.json": "./package.json" - } -} -``` - -As a last resort, package encapsulation can be disabled entirely by creating an -export for the root of the package `"./": "./"`. This will expose every file in -the package at the cost of disabling the encapsulation and potential tooling -benefits this provides. As the ES Module loader in Node.js enforces the use of -[the full specifier path][], exporting the root rather than being explicit -about entry is less expressive than either of the prior examples. Not only -will encapsulation be lost but module consumers will be unable to -`import feature from 'my-mod/feature'` as they will need to provide the full -path `import feature from 'my-mod/feature/index.js`. - -#### Main entry point export - -To set the main entry point for a package, it is advisable to define both -`"exports"` and `"main"` in the package’s `package.json` file: - - -```js -{ - "main": "./main.js", - "exports": "./main.js" -} -``` - -The benefit of doing this is that when using the `"exports"` field all -subpaths of the package will no longer be available to importers under -`require('pkg/subpath.js')`, and instead they will get a new error, -`ERR_PACKAGE_PATH_NOT_EXPORTED`. - -This encapsulation of exports provides more reliable guarantees -about package interfaces for tools and when handling semver upgrades for a -package. It is not a strong encapsulation since a direct require of any -absolute subpath of the package such as -`require('/path/to/node_modules/pkg/subpath.js')` will still load `subpath.js`. - -#### Subpath exports - -When using the `"exports"` field, custom subpaths can be defined along -with the main entry point by treating the main entry point as the -`"."` subpath: - - -```js -{ - "main": "./main.js", - "exports": { - ".": "./main.js", - "./submodule": "./src/submodule.js" - } -} -``` - -Now only the defined subpath in `"exports"` can be imported by a -consumer: - -```js -import submodule from 'es-module-package/submodule'; -// Loads ./node_modules/es-module-package/src/submodule.js -``` - -While other subpaths will error: - -```js -import submodule from 'es-module-package/private-module.js'; -// Throws ERR_PACKAGE_PATH_NOT_EXPORTED -``` - -Entire folders can also be mapped with package exports: - - -```js -// ./node_modules/es-module-package/package.json -{ - "exports": { - "./features/": "./src/features/" - } -} -``` - -With the above, all modules within the `./src/features/` folder -are exposed deeply to `import` and `require`: - -```js -import feature from 'es-module-package/features/x.js'; -// Loads ./node_modules/es-module-package/src/features/x.js -``` - -When using folder mappings, ensure that you do want to expose every -module inside the subfolder. Any modules which are not public -should be moved to another folder to retain the encapsulation -benefits of exports. - -#### Package exports fallbacks - -For possible new specifier support in future, array fallbacks are -supported for all invalid specifiers: - - -```js -{ - "exports": { - "./submodule": ["not:valid", "./submodule.js"] - } -} -``` - -Since `"not:valid"` is not a valid specifier, `"./submodule.js"` is used -instead as the fallback, as if it were the only target. - -#### Exports sugar - -If the `"."` export is the only export, the `"exports"` field provides sugar -for this case being the direct `"exports"` field value. - -If the `"."` export has a fallback array or string value, then the `"exports"` -field can be set to this value directly. - - -```js -{ - "exports": { - ".": "./main.js" - } -} -``` - -can be written: - - -```js -{ - "exports": "./main.js" -} -``` - -#### Conditional exports - -Conditional exports provide a way to map to different paths depending on -certain conditions. They are supported for both CommonJS and ES module imports. - -For example, a package that wants to provide different ES module exports for -`require()` and `import` can be written: - - -```js -// package.json -{ - "main": "./main-require.cjs", - "exports": { - "import": "./main-module.js", - "require": "./main-require.cjs" - }, - "type": "module" -} -``` - -Node.js supports the following conditions out of the box: - -* `"import"` - matched when the package is loaded via `import` or - `import()`. Can reference either an ES module or CommonJS file, as both - `import` and `import()` can load either ES module or CommonJS sources. - _Always matched when the `"require"` condition is not matched._ -* `"require"` - matched when the package is loaded via `require()`. - As `require()` only supports CommonJS, the referenced file must be CommonJS. - _Always matched when the `"import"` condition is not matched._ -* `"node"` - matched for any Node.js environment. Can be a CommonJS or ES - module file. _This condition should always come after `"import"` or - `"require"`._ -* `"default"` - the generic fallback that will always match. Can be a CommonJS - or ES module file. _This condition should always come last._ - -Within the `"exports"` object, key order is significant. During condition -matching, earlier entries have higher priority and take precedence over later -entries. _The general rule is that conditions should be from most specific to -least specific in object order_. - -Other conditions such as `"browser"`, `"electron"`, `"deno"`, `"react-native"`, -etc. are unknown to, and thus ignored by Node.js. Runtimes or tools other than -Node.js may use them at their discretion. Further restrictions, definitions, or -guidance on condition names may occur in the future. - -Using the `"import"` and `"require"` conditions can lead to some hazards, -which are further explained in [the dual CommonJS/ES module packages section][]. - -Conditional exports can also be extended to exports subpaths, for example: - - -```js -{ - "main": "./main.js", - "exports": { - ".": "./main.js", - "./feature": { - "node": "./feature-node.js", - "default": "./feature.js" - } - } -} -``` - -Defines a package where `require('pkg/feature')` and `import 'pkg/feature'` -could provide different implementations between Node.js and other JS -environments. - -When using environment branches, always include a `"default"` condition where -possible. Providing a `"default"` condition ensures that any unknown JS -environments are able to use this universal implementation, which helps avoid -these JS environments from having to pretend to be existing environments in -order to support packages with conditional exports. For this reason, using -`"node"` and `"default"` condition branches is usually preferable to using -`"node"` and `"browser"` condition branches. - -#### Nested conditions - -In addition to direct mappings, Node.js also supports nested condition objects. - -For example, to define a package that only has dual mode entry points for -use in Node.js but not the browser: - - -```js -{ - "main": "./main.js", - "exports": { - "node": { - "import": "./feature-node.mjs", - "require": "./feature-node.cjs" - }, - "default": "./feature.mjs", - } -} -``` - -Conditions continue to be matched in order as with flat conditions. If -a nested conditional does not have any mapping it will continue checking -the remaining conditions of the parent condition. In this way nested -conditions behave analogously to nested JavaScript `if` statements. - -#### Resolving user conditions - -When running Node.js, custom user conditions can be added with the -`--conditions` or `-u` flag: - -```bash -node --conditions=development main.js -``` - -which would then resolve the `"development"` condition in package imports and -exports, while resolving the existing `"node"`, `"default"`, `"import"`, and -`"require"` conditions as appropriate. - -Any number of custom conditions can be set with repeat flags. - -#### Self-referencing a package using its name - -Within a package, the values defined in the package’s -`package.json` `"exports"` field can be referenced via the package’s name. -For example, assuming the `package.json` is: - -```json -// package.json -{ - "name": "a-package", - "exports": { - ".": "./main.mjs", - "./foo": "./foo.js" - } -} -``` - -Then any module _in that package_ can reference an export in the package itself: - -```js -// ./a-module.mjs -import { something } from 'a-package'; // Imports "something" from ./main.mjs. -``` - -Self-referencing is available only if `package.json` has `exports`, and will -allow importing only what that `exports` (in the `package.json`) allows. -So the code below, given the previous package, will generate a runtime error: - -```js -// ./another-module.mjs - -// Imports "another" from ./m.mjs. Fails because -// the "package.json" "exports" field -// does not provide an export named "./m.mjs". -import { another } from 'a-package/m.mjs'; -``` - -Self-referencing is also available when using `require`, both in an ES module, -and in a CommonJS one. For example, this code will also work: - -```js -// ./a-module.js -const { something } = require('a-package/foo'); // Loads from ./foo.js. -``` - -### Internal package imports - -In addition to the `"exports"` field it is possible to define internal package -import maps that only apply to import specifiers from within the package itself. - -Entries in the imports field must always start with `#` to ensure they are -clearly disambiguated from package specifiers. - -For example, the imports field can be used to gain the benefits of conditional -exports for internal modules: - -```json -// package.json -{ - "imports": { - "#dep": { - "node": "dep-node-native", - "default": "./dep-polyfill.js" - } - }, - "dependencies": { - "dep-node-native": "^1.0.0" - } -} -``` - -where `import '#dep'` would now get the resolution of the external package -`dep-node-native` (including its exports in turn), and instead get the local -file `./dep-polyfill.js` relative to the package in other environments. - -Unlike the exports field, import maps permit mapping to external packages -because this provides an important use case for conditional loading and also can -be done without the risk of cycles, unlike for exports. - -Apart from the above, the resolution rules for the imports field are otherwise -analogous to the exports field. - -### Dual CommonJS/ES module packages - -Prior to the introduction of support for ES modules in Node.js, it was a common -pattern for package authors to include both CommonJS and ES module JavaScript -sources in their package, with `package.json` `"main"` specifying the CommonJS -entry point and `package.json` `"module"` specifying the ES module entry point. -This enabled Node.js to run the CommonJS entry point while build tools such as -bundlers used the ES module entry point, since Node.js ignored (and still -ignores) the top-level `"module"` field. - -Node.js can now run ES module entry points, and a package can contain both -CommonJS and ES module entry points (either via separate specifiers such as -`'pkg'` and `'pkg/es-module'`, or both at the same specifier via [Conditional -exports][]). Unlike in the scenario where `"module"` is only used by bundlers, -or ES module files are transpiled into CommonJS on the fly before evaluation by -Node.js, the files referenced by the ES module entry point are evaluated as ES -modules. - -#### Dual package hazard - -When an application is using a package that provides both CommonJS and ES module -sources, there is a risk of certain bugs if both versions of the package get -loaded. This potential comes from the fact that the `pkgInstance` created by -`const pkgInstance = require('pkg')` is not the same as the `pkgInstance` -created by `import pkgInstance from 'pkg'` (or an alternative main path like -`'pkg/module'`). This is the “dual package hazard,” where two versions of the -same package can be loaded within the same runtime environment. While it is -unlikely that an application or package would intentionally load both versions -directly, it is common for an application to load one version while a dependency -of the application loads the other version. This hazard can happen because -Node.js supports intermixing CommonJS and ES modules, and can lead to unexpected -behavior. - -If the package main export is a constructor, an `instanceof` comparison of -instances created by the two versions returns `false`, and if the export is an -object, properties added to one (like `pkgInstance.foo = 3`) are not present on -the other. This differs from how `import` and `require` statements work in -all-CommonJS or all-ES module environments, respectively, and therefore is -surprising to users. It also differs from the behavior users are familiar with -when using transpilation via tools like [Babel][] or [`esm`][]. - -#### Writing dual packages while avoiding or minimizing hazards - -First, the hazard described in the previous section occurs when a package -contains both CommonJS and ES module sources and both sources are provided for -use in Node.js, either via separate main entry points or exported paths. A -package could instead be written where any version of Node.js receives only -CommonJS sources, and any separate ES module sources the package may contain -could be intended only for other environments such as browsers. Such a package -would be usable by any version of Node.js, since `import` can refer to CommonJS -files; but it would not provide any of the advantages of using ES module syntax. - -A package could also switch from CommonJS to ES module syntax in a breaking -change version bump. This has the disadvantage that the newest version -of the package would only be usable in ES module-supporting versions of Node.js. - -Every pattern has tradeoffs, but there are two broad approaches that satisfy the -following conditions: - -1. The package is usable via both `require` and `import`. -1. The package is usable in both current Node.js and older versions of Node.js - that lack support for ES modules. -1. The package main entry point, e.g. `'pkg'` can be used by both `require` to - resolve to a CommonJS file and by `import` to resolve to an ES module file. - (And likewise for exported paths, e.g. `'pkg/feature'`.) -1. The package provides named exports, e.g. `import { name } from 'pkg'` rather - than `import pkg from 'pkg'; pkg.name`. -1. The package is potentially usable in other ES module environments such as - browsers. -1. The hazards described in the previous section are avoided or minimized. - -##### Approach #1: Use an ES module wrapper - -Write the package in CommonJS or transpile ES module sources into CommonJS, and -create an ES module wrapper file that defines the named exports. Using -[Conditional exports][], the ES module wrapper is used for `import` and the -CommonJS entry point for `require`. - - -```js -// ./node_modules/pkg/package.json -{ - "type": "module", - "main": "./index.cjs", - "exports": { - "import": "./wrapper.mjs", - "require": "./index.cjs" - } -} -``` - -The preceding example uses explicit extensions `.mjs` and `.cjs`. -If your files use the `.js` extension, `"type": "module"` will cause such files -to be treated as ES modules, just as `"type": "commonjs"` would cause them -to be treated as CommonJS. -See [Enabling](#esm_enabling). - -```js -// ./node_modules/pkg/index.cjs -exports.name = 'value'; -``` - -```js -// ./node_modules/pkg/wrapper.mjs -import cjsModule from './index.cjs'; -export const name = cjsModule.name; -``` - -In this example, the `name` from `import { name } from 'pkg'` is the same -singleton as the `name` from `const { name } = require('pkg')`. Therefore `===` -returns `true` when comparing the two `name`s and the divergent specifier hazard -is avoided. - -If the module is not simply a list of named exports, but rather contains a -unique function or object export like `module.exports = function () { ... }`, -or if support in the wrapper for the `import pkg from 'pkg'` pattern is desired, -then the wrapper would instead be written to export the default optionally -along with any named exports as well: - -```js -import cjsModule from './index.cjs'; -export const name = cjsModule.name; -export default cjsModule; -``` - -This approach is appropriate for any of the following use cases: -* The package is currently written in CommonJS and the author would prefer not - to refactor it into ES module syntax, but wishes to provide named exports for - ES module consumers. -* The package has other packages that depend on it, and the end user might - install both this package and those other packages. For example a `utilities` - package is used directly in an application, and a `utilities-plus` package - adds a few more functions to `utilities`. Because the wrapper exports - underlying CommonJS files, it doesn’t matter if `utilities-plus` is written in - CommonJS or ES module syntax; it will work either way. -* The package stores internal state, and the package author would prefer not to - refactor the package to isolate its state management. See the next section. - -A variant of this approach not requiring conditional exports for consumers could -be to add an export, e.g. `"./module"`, to point to an all-ES module-syntax -version of the package. This could be used via `import 'pkg/module'` by users -who are certain that the CommonJS version will not be loaded anywhere in the -application, such as by dependencies; or if the CommonJS version can be loaded -but doesn’t affect the ES module version (for example, because the package is -stateless): - - -```js -// ./node_modules/pkg/package.json -{ - "type": "module", - "main": "./index.cjs", - "exports": { - ".": "./index.cjs", - "./module": "./wrapper.mjs" - } -} -``` - -##### Approach #2: Isolate state - -A `package.json` file can define the separate CommonJS and ES module entry -points directly: - - -```js -// ./node_modules/pkg/package.json -{ - "type": "module", - "main": "./index.cjs", - "exports": { - "import": "./index.mjs", - "require": "./index.cjs" - } -} -``` - -This can be done if both the CommonJS and ES module versions of the package are -equivalent, for example because one is the transpiled output of the other; and -the package’s management of state is carefully isolated (or the package is -stateless). - -The reason that state is an issue is because both the CommonJS and ES module -versions of the package may get used within an application; for example, the -user’s application code could `import` the ES module version while a dependency -`require`s the CommonJS version. If that were to occur, two copies of the -package would be loaded in memory and therefore two separate states would be -present. This would likely cause hard-to-troubleshoot bugs. - -Aside from writing a stateless package (if JavaScript’s `Math` were a package, -for example, it would be stateless as all of its methods are static), there are -some ways to isolate state so that it’s shared between the potentially loaded -CommonJS and ES module instances of the package: - -1. If possible, contain all state within an instantiated object. JavaScript’s - `Date`, for example, needs to be instantiated to contain state; if it were a - package, it would be used like this: - - ```js - import Date from 'date'; - const someDate = new Date(); - // someDate contains state; Date does not - ``` - - The `new` keyword isn’t required; a package’s function can return a new - object, or modify a passed-in object, to keep the state external to the - package. - -1. Isolate the state in one or more CommonJS files that are shared between the - CommonJS and ES module versions of the package. For example, if the CommonJS - and ES module entry points are `index.cjs` and `index.mjs`, respectively: - - ```js - // ./node_modules/pkg/index.cjs - const state = require('./state.cjs'); - module.exports.state = state; - ``` - - ```js - // ./node_modules/pkg/index.mjs - import state from './state.cjs'; - export { - state - }; - ``` - - Even if `pkg` is used via both `require` and `import` in an application (for - example, via `import` in application code and via `require` by a dependency) - each reference of `pkg` will contain the same state; and modifying that - state from either module system will apply to both. - -Any plugins that attach to the package’s singleton would need to separately -attach to both the CommonJS and ES module singletons. - -This approach is appropriate for any of the following use cases: -* The package is currently written in ES module syntax and the package author - wants that version to be used wherever such syntax is supported. -* The package is stateless or its state can be isolated without too much - difficulty. -* The package is unlikely to have other public packages that depend on it, or if - it does, the package is stateless or has state that need not be shared between - dependencies or with the overall application. - -Even with isolated state, there is still the cost of possible extra code -execution between the CommonJS and ES module versions of a package. - -As with the previous approach, a variant of this approach not requiring -conditional exports for consumers could be to add an export, e.g. -`"./module"`, to point to an all-ES module-syntax version of the package: - - -```js -// ./node_modules/pkg/package.json -{ - "type": "module", - "main": "./index.cjs", - "exports": { - ".": "./index.cjs", - "./module": "./index.mjs" - } -} -``` +This section was moved to [Modules: Packages](packages.html). ## `import` Specifiers @@ -1022,64 +261,124 @@ To include an ES module into CommonJS, use [`import()`][]. ### `import` statements -An `import` statement can reference an ES module or a CommonJS module. Other -file types such as JSON or native modules are not supported. For those, use -[`module.createRequire()`][]. - +An `import` statement can reference an ES module or a CommonJS module. `import` statements are permitted only in ES modules. For similar functionality in CommonJS, see [`import()`][]. +When importing [CommonJS modules](#esm_commonjs_namespaces), the +`module.exports` object is provided as the default export. Named exports may be +available, provided by static analysis as a convenience for better ecosystem +compatibility. + +Additional experimental flags are available for importing +[Wasm modules](#esm_experimental_wasm_modules) or +[JSON modules](#esm_experimental_json_modules). For importing native modules or +JSON modules unflagged, see [`module.createRequire()`][]. + The _specifier_ of an `import` statement (the string after the `from` keyword) can either be an URL-style relative path like `'./file.mjs'` or a package name like `'fs'`. Like in CommonJS, files within packages can be accessed by appending a path to -the package name; unless the package’s `package.json` contains an `"exports"` -field, in which case files within packages need to be accessed via the path -defined in `"exports"`. +the package name; unless the package’s [`package.json`][] contains an +[`"exports"`][] field, in which case files within packages need to be accessed +via the path defined in [`"exports"`][]. ```js import { sin, cos } from 'geometry/trigonometry-functions.mjs'; ``` -Only the “default export” is supported for CommonJS files or packages: +### `import()` expressions + +[Dynamic `import()`][] is supported in both CommonJS and ES modules. It can be +used to include ES module files from CommonJS code. + +## CommonJS Namespaces + +CommonJS modules consist of a `module.exports` object which can be of any type. + +When importing a CommonJS module, it can be reliably imported using the ES +module default import or its corresponding sugar syntax: ```js -import packageMain from 'commonjs-package'; // Works +import { default as cjs } from 'cjs'; + +// The following import statement is "syntax sugar" (equivalent but sweeter) +// for `{ default as cjsSugar }` in the above import statement: +import cjsSugar from 'cjs'; -import { method } from 'commonjs-package'; // Errors +console.log(cjs); +console.log(cjs === cjsSugar); +// Prints: +// +// true ``` -It is also possible to -[import an ES or CommonJS module for its side effects only][]. +The ECMAScript Module Namespace representation of a CommonJS module will always +be a namespace with a `default` export key pointing to the CommonJS +`module.exports` value. -### `import()` expressions +This Module Namespace Exotic Object can be directly observed either when using +`import * as m from 'cjs'` or a dynamic import: -[Dynamic `import()`][] is supported in both CommonJS and ES modules. It can be -used to include ES module files from CommonJS code. + +```js +import * as m from 'cjs'; +console.log(m); +console.log(m === await import('cjs')); +// Prints: +// [Module] { default: } +// true +``` -## CommonJS, JSON, and native modules +For better compatibility with existing usage in the JS ecosystem, Node.js will +in addition attempt to determine the CommonJS named exports of every imported +CommonJS module to provide them as separate ES module exports using a static +analysis process. -CommonJS, JSON, and native modules can be used with -[`module.createRequire()`][]. +For example, a CommonJS module written: ```js // cjs.cjs -module.exports = 'cjs'; +exports.name = 'exported'; +``` -// esm.mjs -import { createRequire } from 'module'; +will support named imports in ES modules: -const require = createRequire(import.meta.url); + +```js +import { name } from './cjs.cjs'; +console.log(name); +// Prints: 'exported' -const cjs = require('./cjs.cjs'); -cjs === 'cjs'; // true +import cjs from './cjs.cjs'; +console.log(cjs); +// Prints: { name: 'exported' } + +import * as m from './cjs.cjs'; +console.log(m); +// Prints: [Module] { default: { name: 'exported' }, name: 'exported' } ``` +As can be seen from the last example of the Module Namespace Exotic Object being +logged, the `name` export is copied off of the `module.exports` object and set +directly on the ES module namespace when the module is imported. + +Live binding updates or new exports added to `module.exports` are not detected +for these named exports. + +The detection of named exports is based on common syntax patterns but will not +always correctly detect named exports, in these cases using the default +import form described above can be a better option. + +Named exports detection covers many common export patterns, reexport patterns +and build tool and transpiler outputs. See [cjs-module-lexer][] for the exact +semantics implemented. + ## Builtin modules -Builtin modules will provide named exports of their public API. A +[Core modules][] will provide named exports of their public API. A default export is also provided which is the value of the CommonJS exports. The default export can be used for, among other things, modifying the named exports. Named exports of builtin modules are updated only by calling @@ -1111,6 +410,24 @@ syncBuiltinESMExports(); fs.readFileSync === readFileSync; ``` +## CommonJS, JSON, and native modules + +CommonJS, JSON, and native modules can be used with +[`module.createRequire()`][]. + +```js +// cjs.cjs +module.exports = 'cjs'; + +// esm.mjs +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); + +const cjs = require('./cjs.cjs'); +cjs === 'cjs'; // true +``` + ## Experimental JSON modules Currently importing JSON modules are only supported in the `commonjs` mode @@ -1690,7 +1007,7 @@ The resolver can throw the following errors: > 1. If _selfUrl_ is not **undefined**, return _selfUrl_. > 1. If _packageSubpath_ is _"."_ and _packageName_ is a Node.js builtin > module, then -> 1. Return the string _"nodejs:"_ concatenated with _packageSpecifier_. +> 1. Return the string _"node:"_ concatenated with _packageSpecifier_. > 1. While _parentURL_ is not the file system root, > 1. Let _packageURL_ be the URL resolution of _"node_modules/"_ > concatenated with _packageSpecifier_, relative to _parentURL_. @@ -1741,7 +1058,8 @@ The resolver can throw the following errors: > 1. Set _mainExport_ to _exports_\[_"."_\]. > 1. If _mainExport_ is not **undefined**, then > 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( -> _packageURL_, _mainExport_, _""_, **false**, _conditions_). +> _packageURL_, _mainExport_, _""_, **false**, **false**, +> _conditions_). > 1. If _resolved_ is not **null** or **undefined**, then > 1. Return _resolved_. > 1. Otherwise, if _exports_ is an Object and all keys of _exports_ start with @@ -1775,29 +1093,43 @@ _isImports_, _conditions_) > 1. If _matchKey_ is a key of _matchObj_, and does not end in _"*"_, then > 1. Let _target_ be the value of _matchObj_\[_matchKey_\]. > 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( -> _packageURL_, _target_, _""_, _isImports_, _conditions_). +> _packageURL_, _target_, _""_, **false**, _isImports_, _conditions_). > 1. Return the object _{ resolved, exact: **true** }_. -> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_, -> sorted by length descending. +> 1. Let _expansionKeys_ be the list of keys of _matchObj_ ending in _"/"_ +> or _"*"_, sorted by length descending. > 1. For each key _expansionKey_ in _expansionKeys_, do +> 1. If _expansionKey_ ends in _"*"_ and _matchKey_ starts with but is +> not equal to the substring of _expansionKey_ excluding the last _"*"_ +> character, then +> 1. Let _target_ be the value of _matchObj_\[_expansionKey_\]. +> 1. Let _subpath_ be the substring of _matchKey_ starting at the +> index of the length of _expansionKey_ minus one. +> 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( +> _packageURL_, _target_, _subpath_, **true**, _isImports_, +> _conditions_). +> 1. Return the object _{ resolved, exact: **true** }_. > 1. If _matchKey_ starts with _expansionKey_, then > 1. Let _target_ be the value of _matchObj_\[_expansionKey_\]. > 1. Let _subpath_ be the substring of _matchKey_ starting at the > index of the length of _expansionKey_. > 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( -> _packageURL_, _target_, _subpath_, _isImports_, _conditions_). +> _packageURL_, _target_, _subpath_, **false**, _isImports_, +> _conditions_). > 1. Return the object _{ resolved, exact: **false** }_. > 1. Return the object _{ resolved: **null**, exact: **true** }_. -**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _internal_, -_conditions_) +**PACKAGE_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _pattern_, +_internal_, _conditions_) > 1. If _target_ is a String, then -> 1. If _subpath_ has non-zero length and _target_ does not end with _"/"_, -> throw an _Invalid Module Specifier_ error. +> 1. If _pattern_ is **false**, _subpath_ has non-zero length and _target_ +> does not end with _"/"_, throw an _Invalid Module Specifier_ error. > 1. If _target_ does not start with _"./"_, then > 1. If _internal_ is **true** and _target_ does not start with _"../"_ or > _"/"_ and is not a valid URL, then +> 1. If _pattern_ is **true**, then +> 1. Return **PACKAGE_RESOLVE**(_target_ with every instance of +> _"*"_ replaced by _subpath_, _packageURL_ + _"/"_)_. > 1. Return **PACKAGE_RESOLVE**(_target_ + _subpath_, > _packageURL_ + _"/"_)_. > 1. Otherwise, throw an _Invalid Package Target_ error. @@ -1809,8 +1141,12 @@ _conditions_) > 1. Assert: _resolvedTarget_ is contained in _packageURL_. > 1. If _subpath_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_ or > _"node_modules"_ segments, throw an _Invalid Module Specifier_ error. -> 1. Return the URL resolution of the concatenation of _subpath_ and -> _resolvedTarget_. +> 1. If _pattern_ is **true**, then +> 1. Return the URL resolution of _resolvedTarget_ with every instance of +> _"*"_ replaced with _subpath_. +> 1. Otherwise, +> 1. Return the URL resolution of the concatenation of _subpath_ and +> _resolvedTarget_. > 1. Otherwise, if _target_ is a non-null Object, then > 1. If _exports_ contains any index property keys, as defined in ECMA-262 > [6.1.7 Array Index][], throw an _Invalid Package Configuration_ error. @@ -1819,7 +1155,8 @@ _conditions_) > then > 1. Let _targetValue_ be the value of the _p_ property in _target_. > 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( -> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_). +> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_, +> _conditions_). > 1. If _resolved_ is equal to **undefined**, continue the loop. > 1. Return _resolved_. > 1. Return **undefined**. @@ -1827,8 +1164,9 @@ _conditions_) > 1. If _target.length is zero, return **null**. > 1. For each item _targetValue_ in _target_, do > 1. Let _resolved_ be the result of **PACKAGE_TARGET_RESOLVE**( -> _packageURL_, _targetValue_, _subpath_, _internal_, _conditions_), -> continuing the loop on any _Invalid Package Target_ error. +> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_, +> _conditions_), continuing the loop on any _Invalid Package Target_ +> error. > 1. If _resolved_ is **undefined**, continue the loop. > 1. Return _resolved_. > 1. Return or throw the last fallback resolution **null** return or error. @@ -1894,9 +1232,9 @@ $ node --experimental-specifier-resolution=node index success! ``` -[Babel]: https://babeljs.io/ + [CommonJS]: modules.html -[Conditional exports]: #esm_conditional_exports +[Conditional exports]: packages.html#packages_conditional_exports [Dynamic `import()`]: https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports [ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md [ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration @@ -1904,7 +1242,6 @@ success! [Terminology]: #esm_terminology [WHATWG JSON modules specification]: https://html.spec.whatwg.org/#creating-a-json-module-script [`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs -[`esm`]: https://github.com/standard-things/esm#readme [`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export [`getFormat` hook]: #esm_code_getformat_code_hook [`import()`]: #esm_import_expressions @@ -1919,12 +1256,14 @@ success! [TypedArray]: https://www.ecma-international.org/ecma-262/6.0/#sec-typedarray-objects [Uint8Array]: https://www.ecma-international.org/ecma-262/6.0/#sec-uint8array [`util.TextDecoder`]: util.html#util_class_util_textdecoder +[cjs-module-lexer]: https://github.com/guybedford/cjs-module-lexer/tree/0.4.0 [dynamic instantiate hook]: #esm_code_dynamicinstantiate_code_hook -[import an ES or CommonJS module for its side effects only]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Import_a_module_for_its_side_effects_only [special scheme]: https://url.spec.whatwg.org/#special-scheme -[the full specifier path]: #esm_mandatory_file_extensions [the official standard format]: https://tc39.github.io/ecma262/#sec-modules -[the dual CommonJS/ES module packages section]: #esm_dual_commonjs_es_module_packages [transpiler loader example]: #esm_transpiler_loader [6.1.7 Array Index]: https://tc39.es/ecma262/#integer-index [Top-Level Await]: https://github.com/tc39/proposal-top-level-await +[Core modules]: modules.html#modules_core_modules +[`package.json`]: packages.html#packages_node_js_package_json_field_definitions +[`"exports"`]: packages.html#packages_exports +[`"type"`]: packages.html#packages_type diff --git a/doc/api/index.md b/doc/api/index.md index a6d54e1601c152..ce1af23c6b3bb9 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -25,7 +25,6 @@ * [Deprecated APIs](deprecations.html) * [DNS](dns.html) * [Domain](domain.html) -* [ECMAScript modules](esm.html) * [Errors](errors.html) * [Events](events.html) * [File system](fs.html) @@ -35,7 +34,9 @@ * [HTTPS](https.html) * [Inspector](inspector.html) * [Internationalization](intl.html) -* [Modules](modules.html) +* [Modules: CommonJS modules](modules.html) +* [Modules: ECMAScript modules](esm.html) +* [Modules: Packages](packages.html) * [Net](net.html) * [OS](os.html) * [Path](path.html) diff --git a/doc/api/modules.md b/doc/api/modules.md index b4f1abc5a24e10..477a2e533c7b03 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -1,4 +1,4 @@ -# Modules +# Modules: CommonJS modules @@ -391,8 +391,8 @@ directories, and then provide a single entry point to those directories. There are three ways in which a folder may be passed to `require()` as an argument. -The first is to create a `package.json` file in the root of the folder, -which specifies a `main` module. An example `package.json` file might +The first is to create a [`package.json`][] file in the root of the folder, +which specifies a `main` module. An example [`package.json`][] file might look like this: ```json @@ -406,10 +406,10 @@ If this was in a folder at `./some-library`, then This is the extent of the awareness of `package.json` files within Node.js. -If there is no `package.json` file present in the directory, or if the -`'main'` entry is missing or cannot be resolved, then Node.js +If there is no [`package.json`][] file present in the directory, or if the +[`"main"`][] entry is missing or cannot be resolved, then Node.js will attempt to load an `index.js` or `index.node` file out of that -directory. For example, if there was no `package.json` file in the above +directory. For example, if there was no [`package.json`][] file in the previous example, then `require('./some-library')` would attempt to load: * `./some-library/index.js` @@ -1154,3 +1154,5 @@ consists of the following keys: [`Error.prepareStackTrace(error, trace)`]: https://v8.dev/docs/stack-trace-api#customizing-stack-traces [`SourceMap`]: modules.html#modules_class_module_sourcemap [Source map v3 format]: https://sourcemaps.info/spec.html#h.mofvlxcwqzej +[`package.json`]: packages.html#packages_node_js_package_json_field_definitions +[`"main"`]: packages.html#packages_main diff --git a/doc/api/packages.md b/doc/api/packages.md new file mode 100644 index 00000000000000..df7383d2532abf --- /dev/null +++ b/doc/api/packages.md @@ -0,0 +1,1002 @@ +# Modules: Packages + + + + +## Introduction + +A package is a folder tree described by a `package.json` file. The package +consists of the folder containing the `package.json` file and all subfolders +until the next folder containing another `package.json` file, or a folder +named `node_modules`. + +This page provides guidance for package authors writing `package.json` files +along with a reference for the [`package.json`][] fields defined by Node.js. + +## Determining module system + +Node.js will treat the following as [ES modules][] when passed to `node` as the +initial input, or when referenced by `import` statements within ES module code: + +* Files ending in `.mjs`. + +* Files ending in `.js` when the nearest parent `package.json` file contains a + top-level [`"type"`][] field with a value of `"module"`. + +* Strings passed in as an argument to `--eval`, or piped to `node` via `STDIN`, + with the flag `--input-type=module`. + +Node.js will treat as [CommonJS][] all other forms of input, such as `.js` files +where the nearest parent `package.json` file contains no top-level `"type"` +field, or string input without the flag `--input-type`. This behavior is to +preserve backward compatibility. However, now that Node.js supports both +CommonJS and ES modules, it is best to be explicit whenever possible. Node.js +will treat the following as CommonJS when passed to `node` as the initial input, +or when referenced by `import` statements within ES module code: + +* Files ending in `.cjs`. + +* Files ending in `.js` when the nearest parent `package.json` file contains a + top-level field [`"type"`][] with a value of `"commonjs"`. + +* Strings passed in as an argument to `--eval` or `--print`, or piped to `node` + via `STDIN`, with the flag `--input-type=commonjs`. + +Package authors should include the [`"type"`][] field, even in packages where +all sources are CommonJS. Being explicit about the `type` of the package will +future-proof the package in case the default type of Node.js ever changes, and +it will also make things easier for build tools and loaders to determine how the +files in the package should be interpreted. + +### `package.json` and file extensions + +Within a package, the [`package.json`][] [`"type"`][] field defines how +Node.js should interpret `.js` files. If a `package.json` file does not have a +`"type"` field, `.js` files are treated as [CommonJS][]. + +A `package.json` `"type"` value of `"module"` tells Node.js to interpret `.js` +files within that package as using [ES module][] syntax. + +The `"type"` field applies not only to initial entry points (`node my-app.js`) +but also to files referenced by `import` statements and `import()` expressions. + +```js +// my-app.js, treated as an ES module because there is a package.json +// file in the same folder with "type": "module". + +import './startup/init.js'; +// Loaded as ES module since ./startup contains no package.json file, +// and therefore inherits the "type" value from one level up. + +import 'commonjs-package'; +// Loaded as CommonJS since ./node_modules/commonjs-package/package.json +// lacks a "type" field or contains "type": "commonjs". + +import './node_modules/commonjs-package/index.js'; +// Loaded as CommonJS since ./node_modules/commonjs-package/package.json +// lacks a "type" field or contains "type": "commonjs". +``` + +Files ending with `.mjs` are always loaded as [ES modules][] regardless of +the nearest parent `package.json`. + +Files ending with `.cjs` are always loaded as [CommonJS][] regardless of the +nearest parent `package.json`. + +```js +import './legacy-file.cjs'; +// Loaded as CommonJS since .cjs is always loaded as CommonJS. + +import 'commonjs-package/src/index.mjs'; +// Loaded as ES module since .mjs is always loaded as ES module. +``` + +The `.mjs` and `.cjs` extensions can be used to mix types within the same +package: + +* Within a `"type": "module"` package, Node.js can be instructed to + interpret a particular file as [CommonJS][] by naming it with a `.cjs` + extension (since both `.js` and `.mjs` files are treated as ES modules within + a `"module"` package). + +* Within a `"type": "commonjs"` package, Node.js can be instructed to + interpret a particular file as an [ES module][] by naming it with an `.mjs` + extension (since both `.js` and `.cjs` files are treated as CommonJS within a + `"commonjs"` package). + +### `--input-type` flag + +Strings passed in as an argument to `--eval` (or `-e`), or piped to `node` via +`STDIN`, are treated as [ES modules][] when the `--input-type=module` flag +is set. + +```bash +node --input-type=module --eval "import { sep } from 'path'; console.log(sep);" + +echo "import { sep } from 'path'; console.log(sep);" | node --input-type=module +``` + +For completeness there is also `--input-type=commonjs`, for explicitly running +string input as CommonJS. This is the default behavior if `--input-type` is +unspecified. + +## Package entry points + +In a package’s `package.json` file, two fields can define entry points for a +package: [`"main"`][] and [`"exports"`][]. The [`"main"`][] field is supported +in all versions of Node.js, but its capabilities are limited: it only defines +the main entry point of the package. + +The [`"exports"`][] field provides an alternative to [`"main"`][] where the +package main entry point can be defined while also encapsulating the package, +**preventing any other entry points besides those defined in [`"exports"`][]**. +This encapsulation allows module authors to define a public interface for +their package. + +If both [`"exports"`][] and [`"main"`][] are defined, the [`"exports"`][] field +takes precedence over [`"main"`][]. [`"exports"`][] are not specific to ES +modules or CommonJS; [`"main"`][] is overridden by [`"exports"`][] if it +exists. As such [`"main"`][] cannot be used as a fallback for CommonJS but it +can be used as a fallback for legacy versions of Node.js that do not support the +[`"exports"`][] field. + +[Conditional exports][] can be used within [`"exports"`][] to define different +package entry points per environment, including whether the package is +referenced via `require` or via `import`. For more information about supporting +both CommonJS and ES Modules in a single package please consult +[the dual CommonJS/ES module packages section][]. + +**Warning**: Introducing the [`"exports"`][] field prevents consumers of a +package from using any entry points that are not defined, including the +[`package.json`][] (e.g. `require('your-package/package.json')`. **This will +likely be a breaking change.** + +To make the introduction of [`"exports"`][] non-breaking, ensure that every +previously supported entry point is exported. It is best to explicitly specify +entry points so that the package’s public API is well-defined. For example, +a project that previous exported `main`, `lib`, +`feature`, and the `package.json` could use the following `package.exports`: + +```json +{ + "name": "my-mod", + "exports": { + ".": "./lib/index.js", + "./lib": "./lib/index.js", + "./lib/index": "./lib/index.js", + "./lib/index.js": "./lib/index.js", + "./feature": "./feature/index.js", + "./feature/index.js": "./feature/index.js", + "./package.json": "./package.json" + } +} +``` + +Alternatively a project could choose to export entire folders: + +```json +{ + "name": "my-mod", + "exports": { + ".": "./lib/index.js", + "./lib": "./lib/index.js", + "./lib/*": "./lib/*.js", + "./feature": "./feature/index.js", + "./feature/*": "./feature/*.js", + "./package.json": "./package.json" + } +} +``` + +As a last resort, package encapsulation can be disabled entirely by creating an +export for the root of the package `"./*": "./*"`. This exposes every file +in the package at the cost of disabling the encapsulation and potential tooling +benefits this provides. As the ES Module loader in Node.js enforces the use of +[the full specifier path][], exporting the root rather than being explicit +about entry is less expressive than either of the prior examples. Not only +is encapsulation lost but module consumers are unable to +`import feature from 'my-mod/feature'` as they need to provide the full +path `import feature from 'my-mod/feature/index.js`. + +### Main entry point export + +To set the main entry point for a package, it is advisable to define both +[`"exports"`][] and [`"main"`][] in the package’s [`package.json`][] file: + +```json +{ + "main": "./main.js", + "exports": "./main.js" +} +``` + +When the [`"exports"`][] field is defined, all subpaths of the package are +encapsulated and no longer available to importers. For example, +`require('pkg/subpath.js')` throws an [`ERR_PACKAGE_PATH_NOT_EXPORTED`][] +error. + +This encapsulation of exports provides more reliable guarantees +about package interfaces for tools and when handling semver upgrades for a +package. It is not a strong encapsulation since a direct require of any +absolute subpath of the package such as +`require('/path/to/node_modules/pkg/subpath.js')` will still load `subpath.js`. + +### Subpath exports + +> Stability: 1 - Experimental + +When using the [`"exports"`][] field, custom subpaths can be defined along +with the main entry point by treating the main entry point as the +`"."` subpath: + +```json +{ + "main": "./main.js", + "exports": { + ".": "./main.js", + "./submodule": "./src/submodule.js" + } +} +``` + +Now only the defined subpath in [`"exports"`][] can be imported by a consumer: + +```js +import submodule from 'es-module-package/submodule'; +// Loads ./node_modules/es-module-package/src/submodule.js +``` + +While other subpaths will error: + +```js +import submodule from 'es-module-package/private-module.js'; +// Throws ERR_PACKAGE_PATH_NOT_EXPORTED +``` + +### Subpath export patterns + +> Stability: 1 - Experimental + +For packages with a small number of exports, we recommend explicitly listing +each exports subpath entry. But for packages that have large numbers of +subpaths, this might cause `package.json` bloat and maintenance issues. + +For these use cases, subpath export patterns can be used instead: + +```json +// ./node_modules/es-module-package/package.json +{ + "exports": { + "./features/*": "./src/features/*.js" + } +} +``` + +The left hand matching pattern must always end in `*`. All instances of `*` on +the right hand side will then be replaced with this value, including if it +contains any `/` separators. + +```js +import featureX from 'es-module-package/features/x'; +// Loads ./node_modules/es-module-package/src/features/x.js + +import featureY from 'es-module-package/features/y/y'; +// Loads ./node_modules/es-module-package/src/features/y/y.js +``` + +This is a direct static replacement without any special handling for file +extensions. In the previous example, `pkg/features/x.json` would be resolved to +`./src/features/x.json.js` in the mapping. + +The property of exports being statically enumerable is maintained with exports +patterns since the individual exports for a package can be determined by +treating the right hand side target pattern as a `**` glob against the list of +files within the package. Because `node_modules` paths are forbidden in exports +targets, this expansion is dependent on only the files of the package itself. + +### Exports sugar + +> Stability: 1 - Experimental + +If the `"."` export is the only export, the [`"exports"`][] field provides sugar +for this case being the direct [`"exports"`][] field value. + +If the `"."` export has a fallback array or string value, then the +[`"exports"`][] field can be set to this value directly. + +```json +{ + "exports": { + ".": "./main.js" + } +} +``` + +can be written: + +```json +{ + "exports": "./main.js" +} +``` + +### Conditional exports + +> Stability: 1 - Experimental + +Conditional exports provide a way to map to different paths depending on +certain conditions. They are supported for both CommonJS and ES module imports. + +For example, a package that wants to provide different ES module exports for +`require()` and `import` can be written: + +```json +// package.json +{ + "main": "./main-require.cjs", + "exports": { + "import": "./main-module.js", + "require": "./main-require.cjs" + }, + "type": "module" +} +``` + +Node.js supports the following conditions out of the box: + +* `"import"` - matches when the package is loaded via `import` or + `import()`, or via any top-level import or resolve operation by the + ECMAScript module loader. Applies regardless of the module format of the + target file. _Always mutually exclusive with `"require"`._ +* `"require"` - matches when the package is loaded via `require()`. The + referenced file should be loadable with `require()` although the condition + matches regardless of the module format of the target file. Expected + formats include CommonJS, JSON, and native addons but not ES modules as + `require()` doesn't support them. _Always mutually exclusive with + `"import"`._ +* `"node"` - matches for any Node.js environment. Can be a CommonJS or ES + module file. _This condition should always come after `"import"` or + `"require"`._ +* `"default"` - the generic fallback that always matches. Can be a CommonJS + or ES module file. _This condition should always come last._ + +Within the [`"exports"`][] object, key order is significant. During condition +matching, earlier entries have higher priority and take precedence over later +entries. _The general rule is that conditions should be from most specific to +least specific in object order_. + +Other conditions such as `"browser"`, `"electron"`, `"deno"`, `"react-native"`, +etc., are unknown to Node.js, and thus ignored. Runtimes or tools other than +Node.js can use them at their discretion. Further restrictions, definitions, or +guidance on condition names might occur in the future. + +Using the `"import"` and `"require"` conditions can lead to some hazards, +which are further explained in [the dual CommonJS/ES module packages section][]. + +Conditional exports can also be extended to exports subpaths, for example: + +```json +{ + "main": "./main.js", + "exports": { + ".": "./main.js", + "./feature": { + "node": "./feature-node.js", + "default": "./feature.js" + } + } +} +``` + +Defines a package where `require('pkg/feature')` and `import 'pkg/feature'` +could provide different implementations between Node.js and other JS +environments. + +When using environment branches, always include a `"default"` condition where +possible. Providing a `"default"` condition ensures that any unknown JS +environments are able to use this universal implementation, which helps avoid +these JS environments from having to pretend to be existing environments in +order to support packages with conditional exports. For this reason, using +`"node"` and `"default"` condition branches is usually preferable to using +`"node"` and `"browser"` condition branches. + +### Nested conditions + +> Stability: 1 - Experimental + +In addition to direct mappings, Node.js also supports nested condition objects. + +For example, to define a package that only has dual mode entry points for +use in Node.js but not the browser: + +```json +{ + "main": "./main.js", + "exports": { + "node": { + "import": "./feature-node.mjs", + "require": "./feature-node.cjs" + }, + "default": "./feature.mjs", + } +} +``` + +Conditions continue to be matched in order as with flat conditions. If +a nested conditional does not have any mapping it will continue checking +the remaining conditions of the parent condition. In this way nested +conditions behave analogously to nested JavaScript `if` statements. + +### Resolving user conditions + +When running Node.js, custom user conditions can be added with the +`--conditions` flag: + +```bash +node --conditions=development main.js +``` + +which would then resolve the `"development"` condition in package imports and +exports, while resolving the existing `"node"`, `"default"`, `"import"`, and +`"require"` conditions as appropriate. + +Any number of custom conditions can be set with repeat flags. + +### Self-referencing a package using its name + +Within a package, the values defined in the package’s +`package.json` [`"exports"`][] field can be referenced via the package’s name. +For example, assuming the `package.json` is: + +```json +// package.json +{ + "name": "a-package", + "exports": { + ".": "./main.mjs", + "./foo": "./foo.js" + } +} +``` + +Then any module _in that package_ can reference an export in the package itself: + +```js +// ./a-module.mjs +import { something } from 'a-package'; // Imports "something" from ./main.mjs. +``` + +Self-referencing is available only if `package.json` has [`"exports"`][], and +will allow importing only what that [`"exports"`][] (in the `package.json`) +allows. So the code below, given the previous package, will generate a runtime +error: + +```js +// ./another-module.mjs + +// Imports "another" from ./m.mjs. Fails because +// the "package.json" "exports" field +// does not provide an export named "./m.mjs". +import { another } from 'a-package/m.mjs'; +``` + +Self-referencing is also available when using `require`, both in an ES module, +and in a CommonJS one. For example, this code will also work: + +```js +// ./a-module.js +const { something } = require('a-package/foo'); // Loads from ./foo.js. +``` + +## Dual CommonJS/ES module packages + +Prior to the introduction of support for ES modules in Node.js, it was a common +pattern for package authors to include both CommonJS and ES module JavaScript +sources in their package, with `package.json` [`"main"`][] specifying the +CommonJS entry point and `package.json` `"module"` specifying the ES module +entry point. +This enabled Node.js to run the CommonJS entry point while build tools such as +bundlers used the ES module entry point, since Node.js ignored (and still +ignores) the top-level `"module"` field. + +Node.js can now run ES module entry points, and a package can contain both +CommonJS and ES module entry points (either via separate specifiers such as +`'pkg'` and `'pkg/es-module'`, or both at the same specifier via [Conditional +exports][]). Unlike in the scenario where `"module"` is only used by bundlers, +or ES module files are transpiled into CommonJS on the fly before evaluation by +Node.js, the files referenced by the ES module entry point are evaluated as ES +modules. + +### Dual package hazard + +When an application is using a package that provides both CommonJS and ES module +sources, there is a risk of certain bugs if both versions of the package get +loaded. This potential comes from the fact that the `pkgInstance` created by +`const pkgInstance = require('pkg')` is not the same as the `pkgInstance` +created by `import pkgInstance from 'pkg'` (or an alternative main path like +`'pkg/module'`). This is the “dual package hazard,” where two versions of the +same package can be loaded within the same runtime environment. While it is +unlikely that an application or package would intentionally load both versions +directly, it is common for an application to load one version while a dependency +of the application loads the other version. This hazard can happen because +Node.js supports intermixing CommonJS and ES modules, and can lead to unexpected +behavior. + +If the package main export is a constructor, an `instanceof` comparison of +instances created by the two versions returns `false`, and if the export is an +object, properties added to one (like `pkgInstance.foo = 3`) are not present on +the other. This differs from how `import` and `require` statements work in +all-CommonJS or all-ES module environments, respectively, and therefore is +surprising to users. It also differs from the behavior users are familiar with +when using transpilation via tools like [Babel][] or [`esm`][]. + +### Writing dual packages while avoiding or minimizing hazards + +First, the hazard described in the previous section occurs when a package +contains both CommonJS and ES module sources and both sources are provided for +use in Node.js, either via separate main entry points or exported paths. A +package might instead be written where any version of Node.js receives only +CommonJS sources, and any separate ES module sources the package might contain +are intended only for other environments such as browsers. Such a package +would be usable by any version of Node.js, since `import` can refer to CommonJS +files; but it would not provide any of the advantages of using ES module syntax. + +A package might also switch from CommonJS to ES module syntax in a [breaking +change](https://semver.org/) version bump. This has the disadvantage that the +newest version of the package would only be usable in ES module-supporting +versions of Node.js. + +Every pattern has tradeoffs, but there are two broad approaches that satisfy the +following conditions: + +1. The package is usable via both `require` and `import`. +1. The package is usable in both current Node.js and older versions of Node.js + that lack support for ES modules. +1. The package main entry point, e.g. `'pkg'` can be used by both `require` to + resolve to a CommonJS file and by `import` to resolve to an ES module file. + (And likewise for exported paths, e.g. `'pkg/feature'`.) +1. The package provides named exports, e.g. `import { name } from 'pkg'` rather + than `import pkg from 'pkg'; pkg.name`. +1. The package is potentially usable in other ES module environments such as + browsers. +1. The hazards described in the previous section are avoided or minimized. + +#### Approach #1: Use an ES module wrapper + +Write the package in CommonJS or transpile ES module sources into CommonJS, and +create an ES module wrapper file that defines the named exports. Using +[Conditional exports][], the ES module wrapper is used for `import` and the +CommonJS entry point for `require`. + +```json +// ./node_modules/pkg/package.json +{ + "type": "module", + "main": "./index.cjs", + "exports": { + "import": "./wrapper.mjs", + "require": "./index.cjs" + } +} +``` + +The preceding example uses explicit extensions `.mjs` and `.cjs`. +If your files use the `.js` extension, `"type": "module"` will cause such files +to be treated as ES modules, just as `"type": "commonjs"` would cause them +to be treated as CommonJS. +See [Enabling](#esm_enabling). + +```js +// ./node_modules/pkg/index.cjs +exports.name = 'value'; +``` + +```js +// ./node_modules/pkg/wrapper.mjs +import cjsModule from './index.cjs'; +export const name = cjsModule.name; +``` + +In this example, the `name` from `import { name } from 'pkg'` is the same +singleton as the `name` from `const { name } = require('pkg')`. Therefore `===` +returns `true` when comparing the two `name`s and the divergent specifier hazard +is avoided. + +If the module is not simply a list of named exports, but rather contains a +unique function or object export like `module.exports = function () { ... }`, +or if support in the wrapper for the `import pkg from 'pkg'` pattern is desired, +then the wrapper would instead be written to export the default optionally +along with any named exports as well: + +```js +import cjsModule from './index.cjs'; +export const name = cjsModule.name; +export default cjsModule; +``` + +This approach is appropriate for any of the following use cases: +* The package is currently written in CommonJS and the author would prefer not + to refactor it into ES module syntax, but wishes to provide named exports for + ES module consumers. +* The package has other packages that depend on it, and the end user might + install both this package and those other packages. For example a `utilities` + package is used directly in an application, and a `utilities-plus` package + adds a few more functions to `utilities`. Because the wrapper exports + underlying CommonJS files, it doesn’t matter if `utilities-plus` is written in + CommonJS or ES module syntax; it will work either way. +* The package stores internal state, and the package author would prefer not to + refactor the package to isolate its state management. See the next section. + +A variant of this approach not requiring conditional exports for consumers could +be to add an export, e.g. `"./module"`, to point to an all-ES module-syntax +version of the package. This could be used via `import 'pkg/module'` by users +who are certain that the CommonJS version will not be loaded anywhere in the +application, such as by dependencies; or if the CommonJS version can be loaded +but doesn’t affect the ES module version (for example, because the package is +stateless): + +```json +// ./node_modules/pkg/package.json +{ + "type": "module", + "main": "./index.cjs", + "exports": { + ".": "./index.cjs", + "./module": "./wrapper.mjs" + } +} +``` + +#### Approach #2: Isolate state + +A [`package.json`][] file can define the separate CommonJS and ES module entry +points directly: + +```json +// ./node_modules/pkg/package.json +{ + "type": "module", + "main": "./index.cjs", + "exports": { + "import": "./index.mjs", + "require": "./index.cjs" + } +} +``` + +This can be done if both the CommonJS and ES module versions of the package are +equivalent, for example because one is the transpiled output of the other; and +the package’s management of state is carefully isolated (or the package is +stateless). + +The reason that state is an issue is because both the CommonJS and ES module +versions of the package might get used within an application; for example, the +user’s application code could `import` the ES module version while a dependency +`require`s the CommonJS version. If that were to occur, two copies of the +package would be loaded in memory and therefore two separate states would be +present. This would likely cause hard-to-troubleshoot bugs. + +Aside from writing a stateless package (if JavaScript’s `Math` were a package, +for example, it would be stateless as all of its methods are static), there are +some ways to isolate state so that it’s shared between the potentially loaded +CommonJS and ES module instances of the package: + +1. If possible, contain all state within an instantiated object. JavaScript’s + `Date`, for example, needs to be instantiated to contain state; if it were a + package, it would be used like this: + + ```js + import Date from 'date'; + const someDate = new Date(); + // someDate contains state; Date does not + ``` + + The `new` keyword isn’t required; a package’s function can return a new + object, or modify a passed-in object, to keep the state external to the + package. + +1. Isolate the state in one or more CommonJS files that are shared between the + CommonJS and ES module versions of the package. For example, if the CommonJS + and ES module entry points are `index.cjs` and `index.mjs`, respectively: + + ```js + // ./node_modules/pkg/index.cjs + const state = require('./state.cjs'); + module.exports.state = state; + ``` + + ```js + // ./node_modules/pkg/index.mjs + import state from './state.cjs'; + export { + state + }; + ``` + + Even if `pkg` is used via both `require` and `import` in an application (for + example, via `import` in application code and via `require` by a dependency) + each reference of `pkg` will contain the same state; and modifying that + state from either module system will apply to both. + +Any plugins that attach to the package’s singleton would need to separately +attach to both the CommonJS and ES module singletons. + +This approach is appropriate for any of the following use cases: +* The package is currently written in ES module syntax and the package author + wants that version to be used wherever such syntax is supported. +* The package is stateless or its state can be isolated without too much + difficulty. +* The package is unlikely to have other public packages that depend on it, or if + it does, the package is stateless or has state that need not be shared between + dependencies or with the overall application. + +Even with isolated state, there is still the cost of possible extra code +execution between the CommonJS and ES module versions of a package. + +As with the previous approach, a variant of this approach not requiring +conditional exports for consumers could be to add an export, e.g. +`"./module"`, to point to an all-ES module-syntax version of the package: + +```json +// ./node_modules/pkg/package.json +{ + "type": "module", + "main": "./index.cjs", + "exports": { + ".": "./index.cjs", + "./module": "./index.mjs" + } +} +``` + +## Node.js `package.json` field definitions + +This section describes the fields used by the Node.js runtime. Other tools (such +as [npm](https://docs.npmjs.com/creating-a-package-json-file)) use +additional fields which are ignored by Node.js and not documented here. + +The following fields in `package.json` files are used in Node.js: + +* [`"name"`][] - Relevant when using named imports within a package. Also used + by package managers as the name of the package. +* [`"type"`][] - The package type determining whether to load `.js` files as + CommonJS or ES modules. +* [`"exports"`][] - Package exports and conditional exports. When present, + limits which submodules can be loaded from within the package. +* [`"main"`][] - The default module when loading the package, if exports is not + specified, and in versions of Node.js prior to the introduction of exports. +* [`"imports"`][] - Package imports, for use by modules within the package + itself. + +### `"name"` + + +* Type: {string} + +```json +{ + "name": "package-name" +} +``` + +The `"name"` field defines your package’s name. Publishing to the +_npm_ registry requires a name that satisfies +[certain requirements](https://docs.npmjs.com/files/package.json#name). + +The `"name"` field can be used in addition to the [`"exports"`][] field to +[self-reference][] a package using its name. + +### `"type"` + + +* Type: {string} + +The `"type"` field defines the module format that Node.js uses for all +`.js` files that have that `package.json` file as their nearest parent. + +Files ending with `.js` are loaded as ES modules when the nearest parent +`package.json` file contains a top-level field `"type"` with a value of +`"module"`. + +The nearest parent `package.json` is defined as the first `package.json` found +when searching in the current folder, that folder’s parent, and so on up +until a node_modules folder or the volume root is reached. + +```json +// package.json +{ + "type": "module" +} +``` + +```bash +# In same folder as preceding package.json +node my-app.js # Runs as ES module +``` + +If the nearest parent `package.json` lacks a `"type"` field, or contains +`"type": "commonjs"`, `.js` files are treated as [CommonJS][]. If the volume +root is reached and no `package.json` is found, `.js` files are treated as +[CommonJS][]. + +`import` statements of `.js` files are treated as ES modules if the nearest +parent `package.json` contains `"type": "module"`. + +```js +// my-app.js, part of the same example as above +import './startup.js'; // Loaded as ES module because of package.json +``` + +Regardless of the value of the `"type"` field, `.mjs` files are always treated +as ES modules and `.cjs` files are always treated as CommonJS. + +### `"exports"` + + +* Type: {Object} | {string} | {string[]} + +```json +{ + "exports": "./index.js" +} +``` + +The `"exports"` field allows defining the [entry points][] of a package when +imported by name loaded either via a `node_modules` lookup or a +[self-reference][] to its own name. It is supported in Node.js 12+ as an +alternative to the [`"main"`][] that can support defining [subpath exports][] +and [conditional exports][] while encapsulating internal unexported modules. + +[Conditional Exports][] can also be used within `"exports"` to define different +package entry points per environment, including whether the package is +referenced via `require` or via `import`. + +All paths defined in the `"exports"` must be relative file URLs starting with +`./`. + +### `"main"` + + +* Type: {string} + +```json +{ + "main": "./main.js" +} +``` + +The `"main"` field defines the script that is used when the [package directory +is loaded via `require()`](modules.html#modules_folders_as_modules). Its value +is interpreted as a path. + +```js +require('./path/to/directory'); // This resolves to ./path/to/directory/main.js. +``` + +When a package has an [`"exports"`][] field, this will take precedence over the +`"main"` field when importing the package by name. + +### `"imports"` + + +> Stability: 1 - Experimental + +* Type: {Object} + +In addition to the [`"exports"`][] field it is possible to define internal +package import maps that only apply to import specifiers from within the package +itself. + +Entries in the imports field must always start with `#` to ensure they are +clearly disambiguated from package specifiers. + +For example, the imports field can be used to gain the benefits of conditional +exports for internal modules: + +```json +// package.json +{ + "imports": { + "#dep": { + "node": "dep-node-native", + "default": "./dep-polyfill.js" + } + }, + "dependencies": { + "dep-node-native": "^1.0.0" + } +} +``` + +where `import '#dep'` would now get the resolution of the external package +`dep-node-native` (including its exports in turn), and instead get the local +file `./dep-polyfill.js` relative to the package in other environments. + +Unlike the `"exports"` field, import maps permit mapping to external packages, +providing an important use case for conditional loading scenarios. + +Apart from the above, the resolution rules for the imports field are otherwise +analogous to the exports field. + +[Babel]: https://babeljs.io/ +[Conditional exports]: #packages_conditional_exports +[CommonJS]: modules.html +[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.html#errors_err_package_path_not_exported +[ES modules]: esm.html +[ES module]: esm.html +[`esm`]: https://github.com/standard-things/esm#readme +[`"exports"`]: #packages_exports +[`"main"`]: #packages_main +[`"name"`]: #packages_name +[`"imports"`]: #packages_imports +[`"type"`]: #packages_type +[entry points]: #packages_package_entry_points +[`package.json`]: #packages_node_js_package_json_field_definitions +[self-reference]: #packages_self_referencing_a_package_using_its_name +[subpath exports]: #packages_subpath_exports +[the full specifier path]: modules_esm.html#modules_esm_mandatory_file_extensions +[the dual CommonJS/ES module packages section]: #packages_dual_commonjs_es_module_packages diff --git a/doc/api/vm.md b/doc/api/vm.md index 6703921c712e6a..61aee4042b4eff 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -1177,7 +1177,7 @@ queues. [`vm.runInContext()`]: #vm_vm_runincontext_code_contextifiedobject_options [`vm.runInThisContext()`]: #vm_vm_runinthiscontext_code_options [Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records -[ECMAScript Module Loader]: esm.html#esm_ecmascript_modules +[ECMAScript Module Loader]: esm.html#esm_modules_ecmascript_modules [Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation [GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace [HostResolveImportedModule]: https://tc39.es/ecma262/#sec-hostresolveimportedmodule diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 4a0bfed2aa0fdb..99579d6ad62c81 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -21,12 +21,6 @@ 'use strict'; -// Set first due to cycle with ESM loader functions. -module.exports = { - wrapSafe, Module, toRealPath, readPackageScope, - get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } -}; - const { ArrayIsArray, ArrayPrototypeJoin, @@ -41,6 +35,7 @@ const { RegExpPrototypeTest, SafeMap, SafeSet, + SafeWeakMap, StringPrototypeEndsWith, StringPrototypeIndexOf, StringPrototypeLastIndexOf, @@ -49,6 +44,15 @@ const { StringPrototypeStartsWith, } = primordials; +// Map used to store CJS parsing data. +const cjsParseCache = new SafeWeakMap(); + +// Set first due to cycle with ESM loader functions. +module.exports = { + wrapSafe, Module, toRealPath, readPackageScope, cjsParseCache, + get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } +}; + const { NativeModule } = require('internal/bootstrap/loaders'); const { maybeCacheSourceMap, @@ -101,6 +105,7 @@ const { } = require('internal/constants'); const asyncESM = require('internal/process/esm_loader'); +const { enrichCJSError } = require('internal/modules/esm/translators'); const { kEvaluated } = internalBinding('module_wrap'); const { encodedSepRegEx, @@ -115,31 +120,6 @@ const relativeResolveCache = ObjectCreate(null); let requireDepth = 0; let statCache = null; -function enrichCJSError(err) { - const stack = err.stack.split('\n'); - - const lineWithErr = stack[1]; - - /* - The regular expression below targets the most common import statement - usage. However, some cases are not matching, cases like import statement - after a comment block and/or after a variable definition. - */ - if (err.message.startsWith('Unexpected token \'export\'') || - (RegExpPrototypeTest(/^\s*import(?=[ {'"*])\s*(?![ (])/, lineWithErr))) { - // Emit the warning synchronously because we are in the middle of handling - // a SyntaxError that will throw and likely terminate the process before an - // asynchronous warning would be emitted. - process.emitWarning( - 'To load an ES module, set "type": "module" in the package.json or use ' + - 'the .mjs extension.', - undefined, - undefined, - undefined, - true); - } -} - function stat(filename) { filename = path.toNamespacedPath(filename); if (statCache !== null) { @@ -689,14 +669,18 @@ Module._load = function(request, parent, isMain) { const cachedModule = Module._cache[filename]; if (cachedModule !== undefined) { updateChildren(parent, cachedModule, true); - return cachedModule.exports; + const parseCachedModule = cjsParseCache.get(cachedModule); + if (parseCachedModule && !parseCachedModule.loaded) + parseCachedModule.loaded = true; + else + return cachedModule.exports; } const mod = loadNativeModule(filename, request); if (mod && mod.canBeRequiredByUsers) return mod.exports; // Don't call updateChildren(), Module constructor already does. - const module = new Module(filename, parent); + const module = cachedModule || new Module(filename, parent); if (isMain) { process.mainModule = module; @@ -1031,7 +1015,15 @@ Module._extensions['.js'] = function(module, filename) { throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath); } } - const content = fs.readFileSync(filename, 'utf8'); + // If already analyzed the source, then it will be cached. + const cached = cjsParseCache.get(module); + let content; + if (cached && cached.source) { + content = cached.source; + cached.source = undefined; + } else { + content = fs.readFileSync(filename, 'utf8'); + } module._compile(content, filename); }; diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index 616b2cf52309ea..16e2ad5e2d5c3e 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -34,7 +34,7 @@ if (experimentalJsonModules) extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json'; function defaultGetFormat(url, context, defaultGetFormatUnused) { - if (StringPrototypeStartsWith(url, 'nodejs:')) { + if (StringPrototypeStartsWith(url, 'node:')) { return { format: 'builtin' }; } const parsed = new URL(url); diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index ed681541d12723..e2f7523ea25402 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -102,28 +102,27 @@ class ModuleJob { ' does not provide an export named')) { const splitStack = StringPrototypeSplit(e.stack, '\n'); const parentFileUrl = splitStack[0]; - const childSpecifier = StringPrototypeMatch(e.message, /module '(.*)' does/)[1]; + const [, childSpecifier, name] = StringPrototypeMatch(e.message, + /module '(.*)' does not provide an export named '(.+)'/); const childFileURL = - await this.loader.resolve(childSpecifier, parentFileUrl); + await this.loader.resolve(childSpecifier, parentFileUrl); const format = await this.loader.getFormat(childFileURL); if (format === 'commonjs') { - e.message = `The requested module '${childSpecifier}' is expected ` + - 'to be of type CommonJS, which does not support named exports. ' + - 'CommonJS modules can be imported by importing the default ' + - 'export.'; + const importStatement = splitStack[1]; // TODO(@ctavan): The original error stack only provides the single // line which causes the error. For multi-line import statements we // cannot generate an equivalent object descructuring assignment by // just parsing the error stack. - const importStatement = splitStack[1]; const oneLineNamedImports = StringPrototypeMatch(importStatement, /{.*}/); - if (oneLineNamedImports) { - const destructuringAssignment = - StringPrototypeReplace(oneLineNamedImports[0], /\s+as\s+/g, ': '); - e.message += '\nFor example:\n' + - `import pkg from '${childSpecifier}';\n` + - `const ${destructuringAssignment} = pkg;`; - } + const destructuringAssignment = oneLineNamedImports && + StringPrototypeReplace(oneLineNamedImports, /\s+as\s+/g, ': '); + e.message = `Named export '${name}' not found. The requested module` + + ` '${childSpecifier}' is a CommonJS module, which may not support` + + ' all module.exports as named exports.\nCommonJS modules can ' + + 'always be imported via the default export, for example using:' + + `\n\nimport pkg from '${childSpecifier}';\n${ + destructuringAssignment ? + `const ${destructuringAssignment} = pkg;\n` : ''}`; const newStack = StringPrototypeSplit(e.stack, '\n'); newStack[3] = `SyntaxError: ${e.message}`; e.stack = ArrayPrototypeJoin(newStack, '\n'); diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index dd24019351a72e..4576cfebf4d124 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -307,10 +307,11 @@ function throwInvalidPackageTarget( } const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/; +const patternRegEx = /\*/g; function resolvePackageTargetString( - target, subpath, match, packageJSONUrl, base, internal, conditions) { - if (subpath !== '' && target[target.length - 1] !== '/') + target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) { + if (subpath !== '' && !pattern && target[target.length - 1] !== '/') throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); if (!StringPrototypeStartsWith(target, './')) { @@ -321,8 +322,12 @@ function resolvePackageTargetString( new URL(target); isURL = true; } catch {} - if (!isURL) - return packageResolve(target + subpath, packageJSONUrl, conditions); + if (!isURL) { + const exportTarget = pattern ? + StringPrototypeReplace(target, patternRegEx, subpath) : + target + subpath; + return packageResolve(exportTarget, packageJSONUrl, conditions); + } } throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); } @@ -342,6 +347,9 @@ function resolvePackageTargetString( if (RegExpPrototypeTest(invalidSegmentRegEx, subpath)) throwInvalidSubpath(match + subpath, packageJSONUrl, internal, base); + if (pattern) + return new URL(StringPrototypeReplace(resolved.href, patternRegEx, + subpath)); return new URL(subpath, resolved); } @@ -356,10 +364,10 @@ function isArrayIndex(key) { } function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, - base, internal, conditions) { + base, pattern, internal, conditions) { if (typeof target === 'string') { return resolvePackageTargetString( - target, subpath, packageSubpath, packageJSONUrl, base, internal, + target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal, conditions); } else if (ArrayIsArray(target)) { if (target.length === 0) @@ -371,8 +379,8 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, let resolved; try { resolved = resolvePackageTarget( - packageJSONUrl, targetItem, subpath, packageSubpath, base, internal, - conditions); + packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern, + internal, conditions); } catch (e) { lastException = e; if (e.code === 'ERR_INVALID_PACKAGE_TARGET') @@ -406,7 +414,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, const conditionalTarget = target[key]; const resolved = resolvePackageTarget( packageJSONUrl, conditionalTarget, subpath, packageSubpath, base, - internal, conditions); + pattern, internal, conditions); if (resolved === undefined) continue; return resolved; @@ -460,7 +468,7 @@ function packageExportsResolve( if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) { const target = exports[packageSubpath]; const resolved = resolvePackageTarget( - packageJSONUrl, target, '', packageSubpath, base, false, conditions + packageJSONUrl, target, '', packageSubpath, base, false, false, conditions ); if (resolved === null || resolved === undefined) throwExportsNotFound(packageSubpath, packageJSONUrl, base); @@ -471,7 +479,13 @@ function packageExportsResolve( const keys = ObjectGetOwnPropertyNames(exports); for (let i = 0; i < keys.length; i++) { const key = keys[i]; - if (key[key.length - 1] === '/' && + if (key[key.length - 1] === '*' && + StringPrototypeStartsWith(packageSubpath, + StringPrototypeSlice(key, 0, -1)) && + packageSubpath.length >= key.length && + key.length > bestMatch.length) { + bestMatch = key; + } else if (key[key.length - 1] === '/' && StringPrototypeStartsWith(packageSubpath, key) && key.length > bestMatch.length) { bestMatch = key; @@ -480,12 +494,15 @@ function packageExportsResolve( if (bestMatch) { const target = exports[bestMatch]; - const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length); + const pattern = bestMatch[bestMatch.length - 1] === '*'; + const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length - + (pattern ? 1 : 0)); const resolved = resolvePackageTarget(packageJSONUrl, target, subpath, - bestMatch, base, false, conditions); + bestMatch, base, pattern, false, + conditions); if (resolved === null || resolved === undefined) throwExportsNotFound(packageSubpath, packageJSONUrl, base); - return { resolved, exact: false }; + return { resolved, exact: pattern }; } throwExportsNotFound(packageSubpath, packageJSONUrl, base); @@ -504,7 +521,7 @@ function packageImportsResolve(name, base, conditions) { if (imports) { if (ObjectPrototypeHasOwnProperty(imports, name)) { const resolved = resolvePackageTarget( - packageJSONUrl, imports[name], '', name, base, true, conditions + packageJSONUrl, imports[name], '', name, base, false, true, conditions ); if (resolved !== null) return { resolved, exact: true }; @@ -513,7 +530,13 @@ function packageImportsResolve(name, base, conditions) { const keys = ObjectGetOwnPropertyNames(imports); for (let i = 0; i < keys.length; i++) { const key = keys[i]; - if (key[key.length - 1] === '/' && + if (key[key.length - 1] === '*' && + StringPrototypeStartsWith(name, + StringPrototypeSlice(key, 0, -1)) && + name.length >= key.length && + key.length > bestMatch.length) { + bestMatch = key; + } else if (key[key.length - 1] === '/' && StringPrototypeStartsWith(name, key) && key.length > bestMatch.length) { bestMatch = key; @@ -522,11 +545,14 @@ function packageImportsResolve(name, base, conditions) { if (bestMatch) { const target = imports[bestMatch]; - const subpath = StringPrototypeSubstr(name, bestMatch.length); + const pattern = bestMatch[bestMatch.length - 1] === '*'; + const subpath = StringPrototypeSubstr(name, bestMatch.length - + (pattern ? 1 : 0)); const resolved = resolvePackageTarget( - packageJSONUrl, target, subpath, bestMatch, base, true, conditions); + packageJSONUrl, target, subpath, bestMatch, base, pattern, true, + conditions); if (resolved !== null) - return { resolved, exact: false }; + return { resolved, exact: pattern }; } } } @@ -724,13 +750,13 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) { }; } } catch {} - if (parsed && parsed.protocol === 'nodejs:') + if (parsed && parsed.protocol === 'node:') return { url: specifier }; if (parsed && parsed.protocol !== 'file:' && parsed.protocol !== 'data:') throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed); if (NativeModule.canBeRequiredByUsers(specifier)) { return { - url: 'nodejs:' + specifier + url: 'node:' + specifier }; } if (parentURL && StringPrototypeStartsWith(parentURL, 'data:')) { diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index bb095446bc27eb..bb58859e05c2b0 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -3,12 +3,18 @@ /* global WebAssembly */ const { + Boolean, JSONParse, + ObjectPrototypeHasOwnProperty, ObjectKeys, PromisePrototypeCatch, PromiseReject, + RegExpPrototypeTest, SafeMap, + SafeSet, StringPrototypeReplace, + StringPrototypeSplit, + StringPrototypeStartsWith, } = primordials; let _TYPES = null; @@ -17,11 +23,16 @@ function lazyTypes() { return _TYPES = require('internal/util/types'); } +const { readFileSync } = require('fs'); +const { extname } = require('path'); const { stripBOM, loadNativeModule } = require('internal/modules/cjs/helpers'); -const CJSModule = require('internal/modules/cjs/loader').Module; +const { + Module: CJSModule, + cjsParseCache +} = require('internal/modules/cjs/loader'); const internalURLModule = require('internal/url'); const { defaultGetSource } = require( 'internal/modules/esm/get_source'); @@ -44,11 +55,12 @@ const { ModuleWrap } = moduleWrap; const { getOptionValue } = require('internal/options'); const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta-resolve'); +const asyncESM = require('internal/process/esm_loader'); +const cjsParse = require('internal/deps/cjs-module-lexer/lexer'); const translators = new SafeMap(); exports.translators = translators; - -const asyncESM = require('internal/process/esm_loader'); +exports.enrichCJSError = enrichCJSError; let DECODER = null; function assertBufferSource(body, allowString, hookName) { @@ -104,7 +116,7 @@ function initializeImportMeta(meta, { url }) { meta.url = url; } -// Strategy for loading a standard JavaScript module +// Strategy for loading a standard JavaScript module. translators.set('module', async function moduleStrategy(url) { let { source } = await this._getSource( url, { format: 'module' }, defaultGetSource); @@ -122,35 +134,138 @@ translators.set('module', async function moduleStrategy(url) { return module; }); + +function enrichCJSError(err) { + const stack = StringPrototypeSplit(err.stack, '\n'); + /* + The regular expression below targets the most common import statement + usage. However, some cases are not matching, cases like import statement + after a comment block and/or after a variable definition. + */ + if (StringPrototypeStartsWith(err.message, 'Unexpected token \'export\'') || + (RegExpPrototypeTest(/^\s*import(?=[ {'"*])\s*(?![ (])/, stack[1]))) { + // Emit the warning synchronously because we are in the middle of handling + // a SyntaxError that will throw and likely terminate the process before an + // asynchronous warning would be emitted. + process.emitWarning( + 'To load an ES module, set "type": "module" in the package.json or use ' + + 'the .mjs extension.', + undefined, + undefined, + undefined, + true); + } +} + // Strategy for loading a node-style CommonJS module const isWindows = process.platform === 'win32'; const winSepRegEx = /\//g; -translators.set('commonjs', function commonjsStrategy(url, isMain) { +translators.set('commonjs', async function commonjsStrategy(url, isMain) { debug(`Translating CJSModule ${url}`); - return new ModuleWrap(url, undefined, ['default'], function() { + + let filename = internalURLModule.fileURLToPath(new URL(url)); + if (isWindows) + filename = StringPrototypeReplace(filename, winSepRegEx, '\\'); + + const { module, exportNames } = cjsPreparseModuleExports(filename); + const namesWithDefault = exportNames.has('default') ? + [...exportNames] : ['default', ...exportNames]; + + return new ModuleWrap(url, undefined, namesWithDefault, function() { debug(`Loading CJSModule ${url}`); - const pathname = internalURLModule.fileURLToPath(new URL(url)); + let exports; - const cachedModule = CJSModule._cache[pathname]; - if (cachedModule && asyncESM.ESMLoader.cjsCache.has(cachedModule)) { - exports = asyncESM.ESMLoader.cjsCache.get(cachedModule); - asyncESM.ESMLoader.cjsCache.delete(cachedModule); + if (asyncESM.ESMLoader.cjsCache.has(module)) { + exports = asyncESM.ESMLoader.cjsCache.get(module); + asyncESM.ESMLoader.cjsCache.delete(module); } else { - exports = CJSModule._load(pathname, undefined, isMain); + try { + exports = CJSModule._load(filename, undefined, isMain); + } catch (err) { + enrichCJSError(err); + throw err; + } + } + + for (const exportName of exportNames) { + if (!ObjectPrototypeHasOwnProperty(exports, exportName) || + exportName === 'default') + continue; + // We might trigger a getter -> dont fail. + let value; + try { + value = exports[exportName]; + } catch {} + this.setExport(exportName, value); } this.setExport('default', exports); }); }); +function cjsPreparseModuleExports(filename) { + let module = CJSModule._cache[filename]; + if (module) { + const cached = cjsParseCache.get(module); + if (cached) + return { module, exportNames: cached.exportNames }; + } + const loaded = Boolean(module); + if (!loaded) { + module = new CJSModule(filename); + module.filename = filename; + module.paths = CJSModule._nodeModulePaths(module.path); + CJSModule._cache[filename] = module; + } + + let source; + try { + source = readFileSync(filename, 'utf8'); + } catch {} + + let exports, reexports; + try { + ({ exports, reexports } = cjsParse(source || '')); + } catch { + exports = []; + reexports = []; + } + + const exportNames = new SafeSet(exports); + + // Set first for cycles. + cjsParseCache.set(module, { source, exportNames, loaded }); + + if (reexports.length) { + module.filename = filename; + module.paths = CJSModule._nodeModulePaths(module.path); + } + for (const reexport of reexports) { + let resolved; + try { + resolved = CJSModule._resolveFilename(reexport, module); + } catch { + continue; + } + const ext = extname(resolved); + if (ext === '.js' || ext === '.cjs' || !CJSModule._extensions[ext]) { + const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved); + for (const name of reexportNames) + exportNames.add(name); + } + } + + return { module, exportNames }; +} + // Strategy for loading a node builtin CommonJS module that isn't // through normal resolution translators.set('builtin', async function builtinStrategy(url) { debug(`Translating BuiltinModule ${url}`); - // Slice 'nodejs:' scheme - const id = url.slice(7); + // Slice 'node:' scheme + const id = url.slice(5); const module = loadNativeModule(id, url, true); - if (!url.startsWith('nodejs:') || !module) { - throw new ERR_UNKNOWN_BUILTIN_MODULE(id); + if (!url.startsWith('node:') || !module) { + throw new ERR_UNKNOWN_BUILTIN_MODULE(url); } debug(`Loading BuiltinModule ${url}`); return module.getESMFacade(); diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index 7788e7db7d0f91..15e3a2cafda9b6 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -3,7 +3,6 @@ const { ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, } = require('internal/errors').codes; -const assert = require('internal/assert'); const { Loader } = require('internal/modules/esm/loader'); const { hasUncaughtExceptionCaptureCallback, @@ -26,13 +25,6 @@ exports.initializeImportMetaObject = function(wrap, meta) { }; exports.importModuleDynamicallyCallback = async function(wrap, specifier) { - assert(calledInitialize === true || !userLoader); - if (!calledInitialize) { - process.emitWarning( - 'The ESM module loader is experimental.', - 'ExperimentalWarning', undefined); - calledInitialize = true; - } const { callbackMap } = internalBinding('module_wrap'); if (callbackMap.has(wrap)) { const { importModuleDynamically } = callbackMap.get(wrap); @@ -47,15 +39,7 @@ exports.importModuleDynamicallyCallback = async function(wrap, specifier) { let ESMLoader = new Loader(); exports.ESMLoader = ESMLoader; -let calledInitialize = false; -async function initializeLoader(emitWarning) { - assert(calledInitialize === false); - if (emitWarning) { - process.emitWarning( - 'The ESM module loader is experimental.', - 'ExperimentalWarning', undefined); - } - calledInitialize = true; +async function initializeLoader() { if (!userLoader) return; let cwd; @@ -78,9 +62,9 @@ async function initializeLoader(emitWarning) { })(); } -exports.loadESM = async function loadESM(callback, emitWarning = true) { +exports.loadESM = async function loadESM(callback) { try { - await initializeLoader(emitWarning); + await initializeLoader(); await callback(ESMLoader); } catch (err) { if (hasUncaughtExceptionCaptureCallback()) { diff --git a/node.gyp b/node.gyp index 1ecd8756610d98..21f4aab7f0adee 100644 --- a/node.gyp +++ b/node.gyp @@ -249,6 +249,7 @@ 'deps/acorn-plugins/acorn-private-class-elements/index.js', 'deps/acorn-plugins/acorn-private-methods/index.js', 'deps/acorn-plugins/acorn-static-class-features/index.js', + 'deps/cjs-module-lexer/lexer.js', ], 'node_mksnapshot_exec': '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)node_mksnapshot<(EXECUTABLE_SUFFIX)', 'mkcodecache_exec': '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)mkcodecache<(EXECUTABLE_SUFFIX)', diff --git a/src/node_native_module.cc b/src/node_native_module.cc index 74729c412674be..4c3633e06c6026 100644 --- a/src/node_native_module.cc +++ b/src/node_native_module.cc @@ -78,6 +78,9 @@ void NativeModuleLoader::InitializeModuleCategories() { "internal/main/" }; + module_categories_.can_be_required.emplace( + "internal/deps/cjs-module-lexer/lexer"); + module_categories_.cannot_be_required = std::set { #if !HAVE_INSPECTOR "inspector", @@ -115,7 +118,8 @@ void NativeModuleLoader::InitializeModuleCategories() { if (prefix.length() > id.length()) { continue; } - if (id.find(prefix) == 0) { + if (id.find(prefix) == 0 && + module_categories_.can_be_required.count(id) == 0) { module_categories_.cannot_be_required.emplace(id); } } diff --git a/test/es-module/test-esm-cjs-exports.js b/test/es-module/test-esm-cjs-exports.js new file mode 100644 index 00000000000000..7db2c6fdb5971b --- /dev/null +++ b/test/es-module/test-esm-cjs-exports.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const { spawn } = require('child_process'); +const assert = require('assert'); + +const entry = fixtures.path('/es-modules/cjs-exports.mjs'); + +let child = spawn(process.execPath, [entry]); +child.stderr.setEncoding('utf8'); +let stdout = ''; +child.stdout.setEncoding('utf8'); +child.stdout.on('data', (data) => { + stdout += data; +}); +child.on('close', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + assert.strictEqual(stdout, 'ok\n'); +})); + +const entryInvalid = fixtures.path('/es-modules/cjs-exports-invalid.mjs'); +child = spawn(process.execPath, [entryInvalid]); +let stderr = ''; +child.stderr.setEncoding('utf8'); +child.stderr.on('data', (data) => { + stderr += data; +}); +child.on('close', common.mustCall((code, signal) => { + assert.strictEqual(code, 1); + assert.strictEqual(signal, null); + assert.ok(stderr.includes('Warning: To load an ES module')); + assert.ok(stderr.includes('Unexpected token \'export\'')); +})); diff --git a/test/es-module/test-esm-cjs-named-error.mjs b/test/es-module/test-esm-cjs-named-error.mjs index e9ddc67c0fbcea..4ef75a22f92674 100644 --- a/test/es-module/test-esm-cjs-named-error.mjs +++ b/test/es-module/test-esm-cjs-named-error.mjs @@ -3,37 +3,25 @@ import { rejects } from 'assert'; const fixtureBase = '../fixtures/es-modules/package-cjs-named-error'; -const expectedRelative = 'The requested module \'./fail.cjs\' is expected to ' + - 'be of type CommonJS, which does not support named exports. CommonJS ' + - 'modules can be imported by importing the default export.\n' + - 'For example:\n' + - 'import pkg from \'./fail.cjs\';\n' + - 'const { comeOn } = pkg;'; +const errTemplate = (specifier, name, namedImports) => + `Named export '${name}' not found. The requested module` + + ` '${specifier}' is a CommonJS module, which may not support ` + + 'all module.exports as named exports.\nCommonJS modules can ' + + 'always be imported via the default export, for example using:' + + `\n\nimport pkg from '${specifier}';\n` + (namedImports ? + `const ${namedImports} = pkg;\n` : ''); -const expectedWithoutExample = 'The requested module \'./fail.cjs\' is ' + - 'expected to be of type CommonJS, which does not support named exports. ' + - 'CommonJS modules can be imported by importing the default export.'; +const expectedWithoutExample = errTemplate('./fail.cjs', 'comeOn'); -const expectedRenamed = 'The requested module \'./fail.cjs\' is expected to ' + - 'be of type CommonJS, which does not support named exports. CommonJS ' + - 'modules can be imported by importing the default export.\n' + - 'For example:\n' + - 'import pkg from \'./fail.cjs\';\n' + - 'const { comeOn: comeOnRenamed } = pkg;'; +const expectedRelative = errTemplate('./fail.cjs', 'comeOn', '{ comeOn }'); -const expectedPackageHack = 'The requested module \'./json-hack/fail.js\' is ' + - 'expected to be of type CommonJS, which does not support named exports. ' + - 'CommonJS modules can be imported by importing the default export.\n' + - 'For example:\n' + - 'import pkg from \'./json-hack/fail.js\';\n' + - 'const { comeOn } = pkg;'; +const expectedRenamed = errTemplate('./fail.cjs', 'comeOn', + '{ comeOn: comeOnRenamed }'); -const expectedBare = 'The requested module \'deep-fail\' is expected to ' + - 'be of type CommonJS, which does not support named exports. CommonJS ' + - 'modules can be imported by importing the default export.\n' + - 'For example:\n' + - 'import pkg from \'deep-fail\';\n' + - 'const { comeOn } = pkg;'; +const expectedPackageHack = + errTemplate('./json-hack/fail.js', 'comeOn', '{ comeOn }'); + +const expectedBare = errTemplate('deep-fail', 'comeOn', '{ comeOn }'); rejects(async () => { await import(`${fixtureBase}/single-quote.mjs`); diff --git a/test/es-module/test-esm-dynamic-import.js b/test/es-module/test-esm-dynamic-import.js index 4e87866b2bad3b..6f8757da1b914e 100644 --- a/test/es-module/test-esm-dynamic-import.js +++ b/test/es-module/test-esm-dynamic-import.js @@ -38,9 +38,6 @@ function expectFsNamespace(result) { // For direct use of import expressions inside of CJS or ES modules, including // via eval, all kinds of specifiers should work without issue. (function testScriptOrModuleImport() { - common.expectWarning('ExperimentalWarning', - 'The ESM module loader is experimental.'); - // Importing another file, both direct & via eval // expectOkNamespace(import(relativePath)); expectOkNamespace(eval(`import("${relativePath}")`)); @@ -51,9 +48,9 @@ function expectFsNamespace(result) { expectFsNamespace(import('fs')); expectFsNamespace(eval('import("fs")')); expectFsNamespace(eval('import("fs")')); - expectFsNamespace(import('nodejs:fs')); + expectFsNamespace(import('node:fs')); - expectModuleError(import('nodejs:unknown'), + expectModuleError(import('node:unknown'), 'ERR_UNKNOWN_BUILTIN_MODULE'); expectModuleError(import('./not-an-existing-module.mjs'), 'ERR_MODULE_NOT_FOUND'); diff --git a/test/es-module/test-esm-exports.mjs b/test/es-module/test-esm-exports.mjs index a4cced41f897b0..d234099732e3aa 100644 --- a/test/es-module/test-esm-exports.mjs +++ b/test/es-module/test-esm-exports.mjs @@ -33,6 +33,9 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; { default: 'self-cjs' } : { default: 'self-mjs' }], // Resolve self sugar ['pkgexports-sugar', { default: 'main' }], + // Path patterns + ['pkgexports/subpath/sub-dir1', { default: 'main' }], + ['pkgexports/features/dir1', { default: 'main' }] ]); if (isRequire) { diff --git a/test/es-module/test-esm-nowarn-exports.mjs b/test/es-module/test-esm-nowarn-exports.mjs index 13bfaf9b4f3527..5e0927f4674f84 100644 --- a/test/es-module/test-esm-nowarn-exports.mjs +++ b/test/es-module/test-esm-nowarn-exports.mjs @@ -16,7 +16,7 @@ child.stderr.on('data', (data) => { child.on('close', (code, signal) => { strictEqual(code, 0); strictEqual(signal, null); - ok(stderr.toString().includes( + ok(!stderr.toString().includes( 'ExperimentalWarning: The ESM module loader is experimental' )); ok(!stderr.toString().includes( diff --git a/test/fixtures/es-module-loaders/example-loader.mjs b/test/fixtures/es-module-loaders/example-loader.mjs index 1ed18bda51070d..be4808738035f9 100644 --- a/test/fixtures/es-module-loaders/example-loader.mjs +++ b/test/fixtures/es-module-loaders/example-loader.mjs @@ -11,7 +11,7 @@ baseURL.pathname = process.cwd() + '/'; export function resolve(specifier, { parentURL = baseURL }, defaultResolve) { if (builtinModules.includes(specifier)) { return { - url: 'nodejs:' + specifier + url: 'node:' + specifier }; } if (/^\.{1,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) { @@ -27,7 +27,7 @@ export function resolve(specifier, { parentURL = baseURL }, defaultResolve) { } export function getFormat(url, context, defaultGetFormat) { - if (url.startsWith('nodejs:') && builtinModules.includes(url.slice(7))) { + if (url.startsWith('node:') && builtinModules.includes(url.slice(5))) { return { format: 'builtin' }; diff --git a/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs b/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs index e976343e47e9bc..4ef089fd6eb3fd 100644 --- a/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs +++ b/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs @@ -1,14 +1,14 @@ export async function resolve(specifier, { parentURL }, defaultResolve) { if (specifier === 'unknown-builtin-module') { return { - url: 'nodejs:unknown-builtin-module' + url: 'node:unknown-builtin-module' }; } return defaultResolve(specifier, {parentURL}, defaultResolve); } export async function getFormat(url, context, defaultGetFormat) { - if (url === 'nodejs:unknown-builtin-module') { + if (url === 'node:unknown-builtin-module') { return { format: 'builtin' }; diff --git a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs index 2130bad5f52698..9a2cd735a2fd66 100644 --- a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs +++ b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs @@ -14,7 +14,7 @@ export async function resolve(specifier, { parentURL }, defaultResolve) { catch (e) { assert.strictEqual(e.code, 'ERR_MODULE_NOT_FOUND'); return { - url: 'nodejs:fs' + url: 'node:fs' }; } assert.fail(`Module resolution for ${specifier} should be throw ERR_MODULE_NOT_FOUND`); diff --git a/test/fixtures/es-modules/cjs-exports-invalid.mjs b/test/fixtures/es-modules/cjs-exports-invalid.mjs new file mode 100644 index 00000000000000..67428fc27c277d --- /dev/null +++ b/test/fixtures/es-modules/cjs-exports-invalid.mjs @@ -0,0 +1 @@ +import cjs from './invalid-cjs.js'; diff --git a/test/fixtures/es-modules/cjs-exports.mjs b/test/fixtures/es-modules/cjs-exports.mjs new file mode 100644 index 00000000000000..47bb4926af03aa --- /dev/null +++ b/test/fixtures/es-modules/cjs-exports.mjs @@ -0,0 +1,34 @@ +import { strictEqual, deepEqual } from 'assert'; + +import m, { π, z } from './exports-cases.js'; +import * as ns from './exports-cases.js'; + +deepEqual(Object.keys(ns), ['default', 'isObject', 'z', 'π']); +strictEqual(π, 'yes'); +strictEqual(z, 'yes'); +strictEqual(typeof m.isObject, 'undefined'); +strictEqual(m.π, 'yes'); +strictEqual(m.z, 'yes'); + +import m2, { __esModule as __esModule2, name as name2 } from './exports-cases2.js'; +import * as ns2 from './exports-cases2.js'; + +strictEqual(__esModule2, true); +strictEqual(name2, 'name'); +strictEqual(typeof m2, 'object'); +strictEqual(m2.default, 'the default'); +strictEqual(ns2.__esModule, true); +strictEqual(ns2.name, 'name'); +deepEqual(Object.keys(ns2), ['__esModule', 'case2', 'default', 'name', 'pi']); + +import m3, { __esModule as __esModule3, name as name3 } from './exports-cases3.js'; +import * as ns3 from './exports-cases3.js'; + +strictEqual(__esModule3, true); +strictEqual(name3, 'name'); +deepEqual(Object.keys(m3), ['name', 'default', 'pi', 'case2']); +strictEqual(ns3.__esModule, true); +strictEqual(ns3.name, 'name'); +strictEqual(ns3.case2, 'case2'); + +console.log('ok'); diff --git a/test/fixtures/es-modules/exports-cases.js b/test/fixtures/es-modules/exports-cases.js new file mode 100644 index 00000000000000..eec3d31bc7290c --- /dev/null +++ b/test/fixtures/es-modules/exports-cases.js @@ -0,0 +1,7 @@ +if (global.maybe) + module.exports = require('../is-object'); +exports['invalid identifier'] = 'no'; +module.exports['?invalid'] = 'no'; +module.exports['π'] = 'yes'; +exports.package = 10; // reserved word -> not used +Object.defineProperty(exports, 'z', { value: 'yes' }); diff --git a/test/fixtures/es-modules/exports-cases2.js b/test/fixtures/es-modules/exports-cases2.js new file mode 100644 index 00000000000000..189eebb9f3b1b7 --- /dev/null +++ b/test/fixtures/es-modules/exports-cases2.js @@ -0,0 +1,29 @@ +/* + * Transpiled with Babel from: + * + * export { π as pi } from './exports-cases.js'; + * export default 'the default'; + * export const name = 'name'; + */ + +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "pi", { + enumerable: true, + get: function () { + return _exportsCases.π; + } +}); +exports.name = exports.default = void 0; + +var _exportsCases = require("./exports-cases.js"); + +var _default = 'the default'; +exports.default = _default; +const name = 'name'; +exports.name = name; + +exports.case2 = 'case2'; diff --git a/test/fixtures/es-modules/exports-cases3.js b/test/fixtures/es-modules/exports-cases3.js new file mode 100644 index 00000000000000..c48b78cc4106be --- /dev/null +++ b/test/fixtures/es-modules/exports-cases3.js @@ -0,0 +1,25 @@ +/* + * Transpiled with TypeScript from: + * + * export { π as pi } from './exports-cases.js'; + * export default 'the default'; + * export const name = 'name'; + */ + +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.name = void 0; +exports.default = 'the default'; +exports.name = 'name'; + +var _external = require("./exports-cases2.js"); + +Object.keys(_external).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _external[key]; + } + }); +}); \ No newline at end of file diff --git a/test/fixtures/es-modules/invalid-cjs.js b/test/fixtures/es-modules/invalid-cjs.js new file mode 100644 index 00000000000000..15cae6690f1362 --- /dev/null +++ b/test/fixtures/es-modules/invalid-cjs.js @@ -0,0 +1 @@ +export var name = 5; diff --git a/test/fixtures/es-modules/pkgimports/package.json b/test/fixtures/es-modules/pkgimports/package.json index 7cd179631fa618..a2224b39ddd2ac 100644 --- a/test/fixtures/es-modules/pkgimports/package.json +++ b/test/fixtures/es-modules/pkgimports/package.json @@ -5,9 +5,9 @@ "import": "./importbranch.js", "require": "./requirebranch.js" }, - "#subpath/": "./sub/", + "#subpath/*": "./sub/*", "#external": "pkgexports/valid-cjs", - "#external/subpath/": "pkgexports/sub/", + "#external/subpath/*": "pkgexports/sub/*", "#external/invalidsubpath/": "pkgexports/sub", "#belowbase": "../belowbase", "#url": "some:url", diff --git a/test/fixtures/node_modules/pkgexports/package.json b/test/fixtures/node_modules/pkgexports/package.json index 71406a407c453d..240122d4aaec95 100644 --- a/test/fixtures/node_modules/pkgexports/package.json +++ b/test/fixtures/node_modules/pkgexports/package.json @@ -47,6 +47,8 @@ "require": "./resolve-self-invalid.js", "import": "./resolve-self-invalid.mjs" }, - "./subpath/": "./subpath/" + "./subpath/": "./subpath/", + "./subpath/sub-*": "./subpath/dir1/*.js", + "./features/*": "./subpath/*/*.js" } } diff --git a/test/message/async_error_sync_esm.out b/test/message/async_error_sync_esm.out index 6577fff6944723..6054fc7cc22de0 100644 --- a/test/message/async_error_sync_esm.out +++ b/test/message/async_error_sync_esm.out @@ -1,4 +1,3 @@ -(node:*) ExperimentalWarning: The ESM module loader is experimental. Error: test at one (*fixtures*async-error.js:4:9) at two (*fixtures*async-error.js:17:9) diff --git a/test/message/esm_display_syntax_error.out b/test/message/esm_display_syntax_error.out index 778d901129fa95..b7d2008540adf3 100644 --- a/test/message/esm_display_syntax_error.out +++ b/test/message/esm_display_syntax_error.out @@ -1,4 +1,3 @@ -(node:*) ExperimentalWarning: The ESM module loader is experimental. file:///*/test/message/esm_display_syntax_error.mjs:2 await async () => 0; ^^^^^ diff --git a/test/message/esm_display_syntax_error_import.out b/test/message/esm_display_syntax_error_import.out index 8ca1165766bf77..fe174d54a5c49f 100644 --- a/test/message/esm_display_syntax_error_import.out +++ b/test/message/esm_display_syntax_error_import.out @@ -1,4 +1,3 @@ -(node:*) ExperimentalWarning: The ESM module loader is experimental. file:///*/test/message/esm_display_syntax_error_import.mjs:5 notfound ^^^^^^^^ diff --git a/test/message/esm_display_syntax_error_import_module.out b/test/message/esm_display_syntax_error_import_module.out index 56035bc0340cc8..d220627bd02654 100644 --- a/test/message/esm_display_syntax_error_import_module.out +++ b/test/message/esm_display_syntax_error_import_module.out @@ -1,4 +1,3 @@ -(node:*) ExperimentalWarning: The ESM module loader is experimental. file:///*/test/fixtures/es-module-loaders/syntax-error-import.mjs:1 import { foo, notfound } from './module-named-exports.mjs'; ^^^^^^^^ diff --git a/test/message/esm_display_syntax_error_module.out b/test/message/esm_display_syntax_error_module.out index a1498f72c94d57..708257fcaf5792 100644 --- a/test/message/esm_display_syntax_error_module.out +++ b/test/message/esm_display_syntax_error_module.out @@ -1,4 +1,3 @@ -(node:*) ExperimentalWarning: The ESM module loader is experimental. file:///*/test/fixtures/es-module-loaders/syntax-error.mjs:2 await async () => 0; ^^^^^ diff --git a/test/message/esm_loader_not_found.out b/test/message/esm_loader_not_found.out index 4fe916b315f9d8..97400699e644b7 100644 --- a/test/message/esm_loader_not_found.out +++ b/test/message/esm_loader_not_found.out @@ -1,4 +1,3 @@ -(node:*) ExperimentalWarning: The ESM module loader is experimental. (node:*) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time internal/process/esm_loader.js:* internalBinding('errors').triggerUncaughtException( diff --git a/test/message/esm_loader_not_found_cjs_hint_bare.out b/test/message/esm_loader_not_found_cjs_hint_bare.out index edf71b42719979..c3f758577eb4d6 100644 --- a/test/message/esm_loader_not_found_cjs_hint_bare.out +++ b/test/message/esm_loader_not_found_cjs_hint_bare.out @@ -1,4 +1,3 @@ -(node:*) ExperimentalWarning: The ESM module loader is experimental. internal/process/esm_loader.js:* internalBinding('errors').triggerUncaughtException( ^ diff --git a/test/message/esm_loader_not_found_cjs_hint_relative.out b/test/message/esm_loader_not_found_cjs_hint_relative.out index 63a3a4aff92def..eeb4480ee19144 100644 --- a/test/message/esm_loader_not_found_cjs_hint_relative.out +++ b/test/message/esm_loader_not_found_cjs_hint_relative.out @@ -1,4 +1,3 @@ -(node:*) ExperimentalWarning: The ESM module loader is experimental. (node:*) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time internal/process/esm_loader.js:* internalBinding('errors').triggerUncaughtException( diff --git a/test/message/esm_loader_syntax_error.out b/test/message/esm_loader_syntax_error.out index 9767a9c86c6bc2..7dd1c01fbcc1bf 100644 --- a/test/message/esm_loader_syntax_error.out +++ b/test/message/esm_loader_syntax_error.out @@ -1,4 +1,3 @@ -(node:*) ExperimentalWarning: The ESM module loader is experimental. (node:*) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time file://*/test/fixtures/es-module-loaders/syntax-error.mjs:2 await async () => 0; diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index e6bf26ee0ab016..4778128cb09ae0 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -54,6 +54,7 @@ const expectedModules = new Set([ 'NativeModule internal/modules/cjs/helpers', 'NativeModule internal/modules/cjs/loader', 'NativeModule internal/modules/esm/create_dynamic_module', + 'NativeModule internal/deps/cjs-module-lexer/lexer', 'NativeModule internal/modules/esm/get_format', 'NativeModule internal/modules/esm/get_source', 'NativeModule internal/modules/esm/loader', diff --git a/test/parallel/test-loaders-unknown-builtin-module.mjs b/test/parallel/test-loaders-unknown-builtin-module.mjs index 464dbeb22a9b31..85181a2b73b54c 100644 --- a/test/parallel/test-loaders-unknown-builtin-module.mjs +++ b/test/parallel/test-loaders-unknown-builtin-module.mjs @@ -2,7 +2,7 @@ import { expectsError, mustCall } from '../common/index.mjs'; import assert from 'assert'; -const unknownBuiltinModule = 'unknown-builtin-module'; +const unknownBuiltinModule = 'node:unknown-builtin-module'; import(unknownBuiltinModule) .then(assert.fail, expectsError({ diff --git a/tools/license-builder.sh b/tools/license-builder.sh index 8f58a8f2986660..e9f4e1fb954021 100755 --- a/tools/license-builder.sh +++ b/tools/license-builder.sh @@ -33,6 +33,7 @@ addlicense "Acorn" "deps/acorn" "$(cat ${rootdir}/deps/acorn/acorn/LICENSE)" addlicense "Acorn plugins" "deps/acorn-plugins" "$(cat ${rootdir}/deps/acorn-plugins/acorn-class-fields/LICENSE)" addlicense "c-ares" "deps/cares" "$(tail -n +3 ${rootdir}/deps/cares/LICENSE.md)" addlicense "HTTP Parser" "deps/http_parser" "$(cat deps/http_parser/LICENSE-MIT)" +addlicense "cjs-module-lexer" "deps/cjs-module-lexer" "$(cat ${rootdir}/deps/cjs-module-lexer/LICENSE)" if [ -f "${rootdir}/deps/icu/LICENSE" ]; then # ICU 57 and following. Drop the BOM addlicense "ICU" "deps/icu" \