diff --git a/.eslintrc.js b/.eslintrc.js index 9eaa7e3f47b0e2..7b075ab83463aa 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -58,6 +58,8 @@ module.exports = { 'test/es-module/test-esm-example-loader.js', 'test/es-module/test-esm-type-flag.js', 'test/es-module/test-esm-type-flag-alias.js', + 'test/es-module/test-require-module-detect-entry-point.js', + 'test/es-module/test-require-module-detect-entry-point-aou.js', ], parserOptions: { sourceType: 'module' }, }, diff --git a/benchmark/misc/startup-core.js b/benchmark/misc/startup-core.js index 62ead40742f61d..053a1ec0cbff8f 100644 --- a/benchmark/misc/startup-core.js +++ b/benchmark/misc/startup-core.js @@ -9,6 +9,7 @@ const bench = common.createBenchmark(main, { script: [ 'benchmark/fixtures/require-builtins', 'test/fixtures/semicolon', + 'test/fixtures/snapshot/typescript', ], mode: ['process', 'worker'], n: [30], diff --git a/doc/api/cli.md b/doc/api/cli.md index df7031f65d44f9..2fd8a5bc01202f 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -887,38 +887,6 @@ files with no extension will be treated as WebAssembly if they begin with the WebAssembly magic number (`\0asm`); otherwise they will be treated as ES module JavaScript. -### `--experimental-detect-module` - - - -> Stability: 1.1 - Active development - -Node.js will inspect the source code of ambiguous input to determine whether it -contains ES module syntax; if such syntax is detected, the input will be treated -as an ES module. - -Ambiguous input is defined as: - -* Files with a `.js` extension or no extension; and either no controlling - `package.json` file or one that lacks a `type` field; and - `--experimental-default-type` is not specified. -* String input (`--eval` or STDIN) when neither `--input-type` nor - `--experimental-default-type` are specified. - -ES module syntax is defined as syntax that would throw when evaluated as -CommonJS. This includes the following: - -* `import` statements (but _not_ `import()` expressions, which are valid in - CommonJS). -* `export` statements. -* `import.meta` references. -* `await` at the top level of a module. -* Lexical redeclarations of the CommonJS wrapper variables (`require`, `module`, - `exports`, `__dirname`, `__filename`). - ### `--experimental-eventsource` > Stability: 1.1 - Active Development @@ -1540,6 +1514,21 @@ added: v0.8.0 Silence deprecation warnings. +### `--no-experimental-detect-module` + + + +Disable using [syntax detection][] to determine module type. + ### `--no-experimental-fetch` + +> Stability: 1.1 - Active Development + +Disable support for loading a synchronous ES module graph in `require()`. + +See [Loading ECMAScript modules using `require()`][]. + ### `--no-extra-info-on-fatal-exception` -This flag is only useful when `--experimental-require-module` is enabled. - -If the ES module being `require()`'d contains top-level await, this flag +If the ES module being `require()`'d contains top-level `await`, this flag allows Node.js to evaluate the module, try to locate the top-level awaits, and print their location to help users find them. @@ -2382,6 +2387,18 @@ added: Prints a stack trace whenever an environment is exited proactively, i.e. invoking `process.exit()`. +### `--trace-require-module=mode` + + + +Prints information about usage of [Loading ECMAScript modules using `require()`][]. + +When `mode` is `all`, all usage is printed. When `mode` is `no-node-modules`, usage +from the `node_modules` folder is excluded. + ### `--trace-sigint` + +> Stability: 0 - Deprecated An attempt was made to `require()` an [ES Module][]. -To enable `require()` for synchronous module graphs (without -top-level `await`), use `--experimental-require-module`. +This error has been deprecated since `require()` now supports loading synchronous +ES modules. When `require()` encounters an ES module that contains top-level +`await`, it will throw [`ERR_REQUIRE_ASYNC_MODULE`][] instead. @@ -3908,6 +3916,7 @@ An error occurred trying to allocate memory. This should never happen. [`ERR_INVALID_ARG_TYPE`]: #err_invalid_arg_type [`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]: #err_missing_message_port_in_transfer_list [`ERR_MISSING_TRANSFERABLE_IN_TRANSFER_LIST`]: #err_missing_transferable_in_transfer_list +[`ERR_REQUIRE_ASYNC_MODULE`]: #err_require_async_module [`EventEmitter`]: events.md#class-eventemitter [`MessagePort`]: worker_threads.md#class-messageport [`Object.getPrototypeOf`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf diff --git a/doc/api/esm.md b/doc/api/esm.md index 2419c64c2084f0..3100d5756cd0c2 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -450,7 +450,7 @@ compatibility. ### `require` The CommonJS module `require` currently only supports loading synchronous ES -modules when `--experimental-require-module` is enabled. +modules (that is, ES modules that do not use top-level `await`). See [Loading ECMAScript modules using `require()`][] for details. @@ -1071,8 +1071,7 @@ _isImports_, _conditions_) > 10. If _url_ ends in _".js"_, then > 1. If _packageType_ is not **null**, then > 1. Return _packageType_. -> 2. If `--experimental-detect-module` is enabled and the result of -> **DETECT\_MODULE\_SYNTAX**(_source_) is true, then +> 2. If the result of **DETECT\_MODULE\_SYNTAX**(_source_) is true, then > 1. Return _"module"_. > 3. Return _"commonjs"_. > 11. If _url_ does not have any extension, then @@ -1082,8 +1081,7 @@ _isImports_, _conditions_) > 1. Return _"wasm"_. > 2. If _packageType_ is not **null**, then > 1. Return _packageType_. -> 3. If `--experimental-detect-module` is enabled and the source of -> module contains static import or export syntax, then +> 3. If the result of **DETECT\_MODULE\_SYNTAX**(_source_) is true, then > 1. Return _"module"_. > 4. Return _"commonjs"_. > 12. Return **undefined** (will throw during load phase). diff --git a/doc/api/modules.md b/doc/api/modules.md index 8b5840d88778e4..6c2e5d0c027fca 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -171,64 +171,161 @@ relative, and based on the real path of the files making the calls to ## Loading ECMAScript modules using `require()` -> Stability: 1.1 - Active Development. Enable this API with the -> [`--experimental-require-module`][] CLI flag. +> Stability: 1.2 - Release candidate The `.mjs` extension is reserved for [ECMAScript Modules][]. -Currently, if the flag `--experimental-require-module` is not used, loading -an ECMAScript module using `require()` will throw a [`ERR_REQUIRE_ESM`][] -error, and users need to use [`import()`][] instead. See -[Determining module system][] section for more info +See [Determining module system][] section for more info regarding which files are parsed as ECMAScript modules. -If `--experimental-require-module` is enabled, and the ECMAScript module being -loaded by `require()` meets the following requirements: +`require()` only supports loading ECMAScript modules that meet the following requirements: -* Explicitly marked as an ES module with a `"type": "module"` field in - the closest package.json or a `.mjs` extension. -* Fully synchronous (contains no top-level `await`). +* The module is fully synchronous (contains no top-level `await`); and +* One of these conditions are met: + 1. The file has a `.mjs` extension. + 2. The file has a `.js` extension, and the closest `package.json` contains `"type": "module"` + 3. The file has a `.js` extension, the closest `package.json` does not contain + `"type": "commonjs"`, and the module contains ES module syntax. -`require()` will load the requested module as an ES Module, and return -the module name space object. In this case it is similar to dynamic +If the ES Module being loaded meet the requirements, `require()` can load it and +return the module namespace object. In this case it is similar to dynamic `import()` but is run synchronously and returns the name space object directly. +With the following ES Modules: + ```mjs -// point.mjs +// distance.mjs export function distance(a, b) { return (b.x - a.x) ** 2 + (b.y - a.y) ** 2; } -class Point { +``` + +```mjs +// point.mjs +export default class Point { constructor(x, y) { this.x = x; this.y = y; } } -export default Point; ``` +A CommonJS module can load them with `require()`: + ```cjs -const required = require('./point.mjs'); +const distance = require('./distance.mjs'); +console.log(distance); // [Module: null prototype] { -// default: [class Point], // distance: [Function: distance] // } -console.log(required); -(async () => { - const imported = await import('./point.mjs'); - console.log(imported === required); // true -})(); +const point = require('./point.mjs'); +console.log(point); +// [Module: null prototype] { +// default: [class Point], +// __esModule: true, +// } +``` + +For interoperability with existing tools that convert ES Modules into CommonJS, +which could then load real ES Modules through `require()`, the returned namespace +would contain a `__esModule: true` property if it has a `default` export so that +consuming code generated by tools can recognize the default exports in real +ES Modules. If the namespace already defines `__esModule`, this would not be added. +This property is experimental and can change in the future. It should only be used +by tools converting ES modules into CommonJS modules, following existing ecosystem +conventions. Code authored directly in CommonJS should avoid depending on it. + +When a ES Module contains both named exports and a default export, the result returned by `require()` +is the module namespace object, which places the default export in the `.default` property, similar to +the results returned by `import()`. +To customize what should be returned by `require(esm)` directly, the ES Module can export the +desired value using the string name `"module.exports"`. + + + +```mjs +// point.mjs +export default class Point { + constructor(x, y) { this.x = x; this.y = y; } +} + +// `distance` is lost to CommonJS consumers of this module, unless it's +// added to `Point` as a static property. +export function distance(a, b) { return (b.x - a.x) ** 2 + (b.y - a.y) ** 2; } +export { Point as 'module.exports' } +``` + + + +```cjs +const Point = require('./point.mjs'); +console.log(Point); // [class Point] + +// Named exports are lost when 'module.exports' is used +const { distance } = require('./point.mjs'); +console.log(distance); // undefined +``` + +Notice in the example above, when the `module.exports` export name is used, named exports +will be lost to CommonJS consumers. To allow CommonJS consumers to continue accessing +named exports, the module can make sure that the default export is an object with the +named exports attached to it as properties. For example with the example above, +`distance` can be attached to the default export, the `Point` class, as a static method. + + + +```mjs +export function distance(a, b) { return (b.x - a.x) ** 2 + (b.y - a.y) ** 2; } + +export default class Point { + constructor(x, y) { this.x = x; this.y = y; } + static distance = distance; +} + +export { Point as 'module.exports' } +``` + + + +```cjs +const Point = require('./point.mjs'); +console.log(Point); // [class Point] + +const { distance } = require('./point.mjs'); +console.log(distance); // [Function: distance] ``` If the module being `require()`'d contains top-level `await`, or the module graph it `import`s contains top-level `await`, [`ERR_REQUIRE_ASYNC_MODULE`][] will be thrown. In this case, users should -load the asynchronous module using `import()`. +load the asynchronous module using [`import()`][]. If `--experimental-print-required-tla` is enabled, instead of throwing `ERR_REQUIRE_ASYNC_MODULE` before evaluation, Node.js will evaluate the module, try to locate the top-level awaits, and print their location to help users fix them. +Support for loading ES modules using `require()` is currently +experimental and can be disabled using `--no-experimental-require-module`. +To print where this feature is used, use [`--trace-require-module`][]. + +This feature can be detected by checking if +[`process.features.require_module`][] is `true`. + ## All together @@ -256,18 +353,26 @@ require(X) from module at path Y 6. LOAD_NODE_MODULES(X, dirname(Y)) 7. THROW "not found" +MAYBE_DETECT_AND_LOAD(X) +1. If X parses as a CommonJS module, load X as a CommonJS module. STOP. +2. Else, if the source code of X can be parsed as ECMAScript module using + DETECT_MODULE_SYNTAX defined in + the ESM resolver, + a. Load X as an ECMAScript module. STOP. +3. THROW the SyntaxError from attempting to parse X as CommonJS in 1. STOP. + LOAD_AS_FILE(X) 1. If X is a file, load X as its file extension format. STOP 2. If X.js is a file, a. Find the closest package scope SCOPE to X. - b. If no scope was found, load X.js as a CommonJS module. STOP. + b. If no scope was found + 1. MAYBE_DETECT_AND_LOAD(X.js) c. If the SCOPE/package.json contains "type" field, 1. If the "type" field is "module", load X.js as an ECMAScript module. STOP. - 2. Else, load X.js as an CommonJS module. STOP. + 2. If the "type" field is "commonjs", load X.js as an CommonJS module. STOP. + d. MAYBE_DETECT_AND_LOAD(X.js) 3. If X.json is a file, load X.json to a JavaScript Object. STOP 4. If X.node is a file, load X.node as binary addon. STOP -5. If X.mjs is a file, and `--experimental-require-module` is enabled, - load X.mjs as an ECMAScript module. STOP LOAD_INDEX(X) 1. If X/index.js is a file @@ -312,9 +417,12 @@ LOAD_PACKAGE_IMPORTS(X, DIR) 1. Find the closest package scope SCOPE to DIR. 2. If no scope was found, return. 3. If the SCOPE/package.json "imports" is null or undefined, return. -4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE), - ["node", "require"]) defined in the ESM resolver. -5. RESOLVE_ESM_MATCH(MATCH). +4. If `--experimental-require-module` is enabled + a. let CONDITIONS = ["node", "require", "module-sync"] + b. Else, let CONDITIONS = ["node", "require"] +5. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE), + CONDITIONS) defined in the ESM resolver. +6. RESOLVE_ESM_MATCH(MATCH). LOAD_PACKAGE_EXPORTS(X, DIR) 1. Try to interpret X as a combination of NAME and SUBPATH where the name @@ -323,9 +431,12 @@ LOAD_PACKAGE_EXPORTS(X, DIR) return. 3. Parse DIR/NAME/package.json, and look for "exports" field. 4. If "exports" is null or undefined, return. -5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH, - `package.json` "exports", ["node", "require"]) defined in the ESM resolver. -6. RESOLVE_ESM_MATCH(MATCH) +5. If `--experimental-require-module` is enabled + a. let CONDITIONS = ["node", "require", "module-sync"] + b. Else, let CONDITIONS = ["node", "require"] +6. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH, + `package.json` "exports", CONDITIONS) defined in the ESM resolver. +7. RESOLVE_ESM_MATCH(MATCH) LOAD_PACKAGE_SELF(X, DIR) 1. Find the closest package scope SCOPE to DIR. @@ -1160,9 +1271,8 @@ This section was moved to [GLOBAL_FOLDERS]: #loading-from-the-global-folders [`"main"`]: packages.md#main [`"type"`]: packages.md#type -[`--experimental-require-module`]: cli.md#--experimental-require-module +[`--trace-require-module`]: cli.md#--trace-require-modulemode [`ERR_REQUIRE_ASYNC_MODULE`]: errors.md#err_require_async_module -[`ERR_REQUIRE_ESM`]: errors.md#err_require_esm [`ERR_UNSUPPORTED_DIR_IMPORT`]: errors.md#err_unsupported_dir_import [`MODULE_NOT_FOUND`]: errors.md#module_not_found [`__dirname`]: #__dirname @@ -1178,6 +1288,7 @@ This section was moved to [`node:test`]: test.md [`package.json`]: packages.md#nodejs-packagejson-field-definitions [`path.dirname()`]: path.md#pathdirnamepath +[`process.features.require_module`]: process.md#processfeaturesrequire_module [`require.main`]: #requiremain [exports shortcut]: #exports-shortcut [module resolution]: #all-together diff --git a/doc/api/packages.md b/doc/api/packages.md index 8e07bb60c1e7f7..05f676e7f4ee3d 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -69,14 +69,14 @@ expressions: * Strings passed in as an argument to `--eval`, or piped to `node` via `STDIN`, with the flag `--input-type=module`. -* When using [`--experimental-detect-module`][], code containing syntax only - successfully parsed as [ES modules][], such as `import` or `export` - statements or `import.meta`, having no explicit marker of how it should be - interpreted. Explicit markers are `.mjs` or `.cjs` extensions, `package.json` - `"type"` fields with either `"module"` or `"commonjs"` values, or - `--input-type` or `--experimental-default-type` flags. Dynamic `import()` - expressions are supported in either CommonJS or ES modules and would not - cause a file to be treated as an ES module. +* Code containing syntax only successfully parsed as [ES modules][], such as + `import` or `export` statements or `import.meta`, with no explicit marker of + how it should be interpreted. Explicit markers are `.mjs` or `.cjs` + extensions, `package.json` `"type"` fields with either `"module"` or + `"commonjs"` values, or `--input-type` or `--experimental-default-type` flags. + Dynamic `import()` expressions are supported in either CommonJS or ES modules + and would not force a file to be treated as an ES module. See + [Syntax detection][]. Node.js will treat the following as [CommonJS][] when passed to `node` as the initial input, or when referenced by `import` statements or `import()` @@ -115,6 +115,44 @@ 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. +### Syntax detection + + + +> Stability: 1.2 - Release candidate + +Node.js will inspect the source code of ambiguous input to determine whether it +contains ES module syntax; if such syntax is detected, the input will be treated +as an ES module. + +Ambiguous input is defined as: + +* Files with a `.js` extension or no extension; and either no controlling + `package.json` file or one that lacks a `type` field; and + `--experimental-default-type` is not specified. +* String input (`--eval` or STDIN) when neither `--input-type` nor + `--experimental-default-type` are specified. + +ES module syntax is defined as syntax that would throw when evaluated as +CommonJS. This includes the following: + +* `import` statements (but _not_ `import()` expressions, which are valid in + CommonJS). +* `export` statements. +* `import.meta` references. +* `await` at the top level of a module. +* Lexical redeclarations of the CommonJS wrapper variables (`require`, `module`, + `exports`, `__dirname`, `__filename`). + ### Modules loaders Node.js has two systems for resolving a specifier and loading modules. @@ -134,8 +172,7 @@ There is the CommonJS module loader: * It treats all files that lack `.json` or `.node` extensions as JavaScript text files. * It can only be used to [load ECMASCript modules from CommonJS modules][] if - the module graph is synchronous (that contains no top-level `await`) when - `--experimental-require-module` is enabled. + the module graph is synchronous (that contains no top-level `await`). When used to load a JavaScript text file that is not an ECMAScript module, the file will be loaded as a CommonJS module. @@ -624,9 +661,12 @@ specific to least specific as conditions should be defined: * `"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, native addons, and ES modules - if `--experimental-require-module` is enabled. _Always mutually + formats include CommonJS, JSON, native addons, and ES modules. _Always mutually exclusive with `"import"`._ +* `"module-sync"` - matches no matter the package is loaded via `import`, + `import()` or `require()`. The format is expected to be ES modules that does + not contain top-level await in its module graph - if it does, + `ERR_REQUIRE_ASYNC_MODULE` will be thrown when the module is `require()`-ed. * `"default"` - the generic fallback that always matches. Can be a CommonJS or ES module file. _This condition should always come last._ @@ -731,7 +771,7 @@ In node, conditions have very few restrictions, but specifically these include: ### Community Conditions Definitions -Condition strings other than the `"import"`, `"require"`, `"node"`, +Condition strings other than the `"import"`, `"require"`, `"node"`, `"module-sync"`, `"node-addons"` and `"default"` conditions [implemented in Node.js core](#conditional-exports) are ignored by default. @@ -862,6 +902,17 @@ $ node other.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 @@ -874,7 +925,7 @@ 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, +exports][]). Unlike in the scenario where top-level `"module"` field 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. @@ -1369,6 +1420,7 @@ This field defines [subpath imports][] for the current package. [ES modules]: esm.md [Node.js documentation for this section]: https://github.com/nodejs/node/blob/HEAD/doc/api/packages.md#conditions-definitions [Runtime Keys]: https://runtime-keys.proposal.wintercg.org/ +[Syntax detection]: #syntax-detection [WinterCG]: https://wintercg.org/ [`"exports"`]: #exports [`"imports"`]: #imports @@ -1378,7 +1430,6 @@ This field defines [subpath imports][] for the current package. [`"type"`]: #type [`--conditions` / `-C` flag]: #resolving-user-conditions [`--experimental-default-type`]: cli.md#--experimental-default-typetype -[`--experimental-detect-module`]: cli.md#--experimental-detect-module [`--no-addons` flag]: cli.md#--no-addons [`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported [`esm`]: https://github.com/standard-things/esm#readme diff --git a/doc/api/process.md b/doc/api/process.md index 68727c2bb61a0c..58df034fc46c52 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -1917,6 +1917,17 @@ added: v0.5.3 A boolean value that is `true` if the current Node.js build includes support for IPv6. +## `process.features.require_module` + + + +* {boolean} + +A boolean value that is `true` if the current Node.js build supports +[loading ECMAScript modules using `require()`][]. + ## `process.features.tls`