summaryrefslogtreecommitdiff
path: root/doc/api/esm.md
diff options
context:
space:
mode:
Diffstat (limited to 'doc/api/esm.md')
-rw-r--r--doc/api/esm.md560
1 files changed, 515 insertions, 45 deletions
diff --git a/doc/api/esm.md b/doc/api/esm.md
index e81a69c6ed..ce616e1f42 100644
--- a/doc/api/esm.md
+++ b/doc/api/esm.md
@@ -5,39 +5,233 @@
> Stability: 1 - Experimental
+## Introduction
+
<!--name=esm-->
+ECMAScript modules are [the official standard format][] to package JavaScript
+code for reuse. Modules are defined using a variety of [`import`][] and
+[`export`][] statements.
+
+Node.js fully supports ECMAScript modules as they are currently specified and
+provides limited interoperability between them and the existing module format,
+[CommonJS][].
+
Node.js contains support for ES Modules based upon the
-[Node.js EP for ES Modules][].
+[Node.js EP for ES Modules][] and the [ECMAScript-modules implementation][].
-Not all features of the EP are complete and will be landing as both VM support
-and implementation is ready. Error messages are still being polished.
+Expect major changes in the implementation including interoperability support,
+specifier resolution, and default behavior.
## Enabling
<!-- type=misc -->
-The `--experimental-modules` flag can be used to enable features for loading
-ESM modules.
+The `--experimental-modules` flag can be used to enable support for
+ECMAScript modules (ES modules).
+
+## Running Node.js with an ECMAScript Module
-Once this has been set, files ending with `.mjs` will be able to be loaded
-as ES Modules.
+There are a few ways to start Node.js with an ES module as its input.
+
+### Initial entry point with an <code>.mjs</code> extension
+
+A file ending with `.mjs` passed to Node.js as an initial entry point will be
+loaded as an ES module.
```sh
node --experimental-modules my-app.mjs
```
-## Features
+### <code>--entry-type=module</code> flag
-<!-- type=misc -->
+Files ending with `.js` or `.mjs`, or lacking any extension,
+will be loaded as ES modules when the `--entry-type=module` flag is set.
+
+```sh
+node --experimental-modules --entry-type=module my-app.js
+```
+
+For completeness there is also `--entry-type=commonjs`, for explicitly running
+a `.js` file as CommonJS. This is the default behavior if `--entry-type` is
+unspecified.
+
+The `--entry-type=module` flag can also be used to configure Node.js to treat
+as an ES module input sent in via `--eval` or `--print` (or `-e` or `-p`) or
+piped to Node.js via `STDIN`.
+
+```sh
+node --experimental-modules --entry-type=module --eval \
+ "import { sep } from 'path'; console.log(sep);"
+
+echo "import { sep } from 'path'; console.log(sep);" | \
+ node --experimental-modules --entry-type=module
+```
+
+### <code>package.json</code> <code>"type"</code> field
+
+Files ending with `.js` or `.mjs`, or lacking any extension,
+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.
+
+<!-- eslint-skip -->
+```js
+// package.json
+{
+ "type": "module"
+}
+```
+
+```sh
+# In same folder as above package.json
+node --experimental-modules my-app.js # Runs as ES module
+```
+
+If the nearest parent `package.json` lacks a `"type"` field, or contains
+`"type": "commonjs"`, extensionless and `.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.
+
+## Package Scope and File Extensions
+
+A folder containing a `package.json` file, and all subfolders below that
+folder down until the next folder containing another `package.json`, is
+considered a _package scope_. The `"type"` field defines how `.js` and
+extensionless files should be treated within a particular `package.json` file’s
+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. A `package.json` lacking a `"type"` field is treated as if it contained
+`"type": "commonjs"`.
+
+The package scope applies not only to initial entry points (`node
+--experimental-modules 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).
+
+## Package Entry Points
+
+The `package.json` `"main"` field defines the entry point for a package,
+whether the package is included into CommonJS via `require` or into an ES
+module via `import`.
+
+<!-- eslint-skip -->
+```js
+// ./node_modules/es-module-package/package.json
+{
+ "type": "module",
+ "main": "./src/index.js"
+}
+```
+```js
+// ./my-app.mjs
+
+import { something } from 'es-module-package';
+// Loads from ./node_modules/es-module-package/src/index.js
+```
+
+An attempt to `require` the above `es-module-package` would attempt to load
+`./node_modules/es-module-package/src/index.js` as CommonJS, which would throw
+an error as Node.js would not be able to parse the `export` statement in
+CommonJS.
+
+As with `import` statements, for ES module usage the value of `"main"` must be
+a full path including extension: `"./index.mjs"`, not `"./index"`.
+
+If the `package.json` `"type"` field is omitted, a `.js` file in `"main"` will
+be interpreted as CommonJS.
+
+> Currently a package can define _either_ a CommonJS entry point **or** an ES
+> module entry point; there is no way to specify separate entry points for
+> CommonJS and ES module usage. This means that a package entry point can be
+> included via `require` or via `import` but not both.
+>
+> Such a limitation makes it difficult for packages to support both new versions
+> of Node.js that understand ES modules and older versions of Node.js that
+> understand only CommonJS. There is work ongoing to remove this limitation, and
+> it will very likely entail changes to the behavior of `"main"` as defined
+> here.
+
+## <code>import</code> Specifiers
+
+### Terminology
+
+The _specifier_ of an `import` statement is the string after the `from` keyword,
+e.g. `'path'` in `import { sep } from 'path'`. Specifiers are also used in
+`export from` statements, and as the argument to an `import()` expression.
+
+There are four types of specifiers:
+
+- _Bare specifiers_ like `'some-package'`. They refer to an entry point of a
+ package by the package name.
+
+- _Deep import specifiers_ like `'some-package/lib/shuffle.mjs'`. They refer to
+ a path within a package prefixed by the package name.
+
+- _Relative specifiers_ like `'./startup.js'` or `'../config.mjs'`. They refer
+ to a path relative to the location of the importing file.
+
+- _Absolute specifiers_ like `'file:///opt/nodejs/config.js'`. They refer
+ directly and explicitly to a full path.
+
+Bare specifiers, and the bare specifier portion of deep import specifiers, are
+strings; but everything else in a specifier is a URL.
-### Supported
+Only `file://` URLs are supported. A specifier like
+`'https://example.com/app.js'` may be supported by browsers but it is not
+supported in Node.js.
-Only the CLI argument for the main entry point to the program can be an entry
-point into an ESM graph. Dynamic import can also be used to create entry points
-into ESM graphs at runtime.
+Specifiers may not begin with `/` or `//`. These are reserved for potential
+future use. The root of the current volume may be referenced via `file:///`.
-#### import.meta
+## import.meta
* {Object}
@@ -46,63 +240,128 @@ property:
* `url` {string} The absolute `file:` URL of the module.
-### Unsupported
+## Differences Between ES Modules and CommonJS
-| Feature | Reason |
-| --- | --- |
-| `require('./foo.mjs')` | ES Modules have differing resolution and timing, use dynamic import |
+### Mandatory file extensions
+
+A file extension must be provided when using the `import` keyword. Directory
+indexes (e.g. `'./startup/index.js'`) must also be fully specified.
-## Notable differences between `import` and `require`
+This behavior matches how `import` behaves in browser environments, assuming a
+typically configured server.
-### No NODE_PATH
+### No <code>NODE_PATH</code>
`NODE_PATH` is not part of resolving `import` specifiers. Please use symlinks
if this behavior is desired.
-### No `require.extensions`
+### No <code>require</code>, <code>exports</code>, <code>module.exports</code>, <code>\_\_filename</code>, <code>\_\_dirname</code>
+
+These CommonJS variables are not available in ES modules.
+
+`require` can be imported into an ES module using
+[`module.createRequireFromPath()`][].
+
+An equivalent for `__filename` and `__dirname` is [`import.meta.url`][].
+
+### No <code>require.extensions</code>
`require.extensions` is not used by `import`. The expectation is that loader
hooks can provide this workflow in the future.
-### No `require.cache`
+### No <code>require.cache</code>
`require.cache` is not used by `import`. It has a separate cache.
-### URL based paths
+### URL-based paths
-ESM are resolved and cached based upon [URL](https://url.spec.whatwg.org/)
-semantics. This means that files containing special characters such as `#` and
-`?` need to be escaped.
+ES modules are resolved and cached based upon
+[URL](https://url.spec.whatwg.org/) semantics. This means that files containing
+special characters such as `#` and `?` need to be escaped.
Modules will be loaded multiple times if the `import` specifier used to resolve
them have a different query or fragment.
```js
-import './foo?query=1'; // loads ./foo with query of "?query=1"
-import './foo?query=2'; // loads ./foo with query of "?query=2"
+import './foo.mjs?query=1'; // loads ./foo.mjs with query of "?query=1"
+import './foo.mjs?query=2'; // loads ./foo.mjs with query of "?query=2"
```
For now, only modules using the `file:` protocol can be loaded.
-## Interop with existing modules
+## Interoperability with CommonJS
+
+### <code>require</code>
-All CommonJS, JSON, and C++ modules can be used with `import`.
+`require` always treats the files it references as CommonJS. This applies
+whether `require` is used the traditional way within a CommonJS environment, or
+in an ES module environment using [`module.createRequireFromPath()`][].
-Modules loaded this way will only be loaded once, even if their query
-or fragment string differs between `import` statements.
+To include an ES module into CommonJS, use [`import()`][].
-When loaded via `import` these modules will provide a single `default` export
-representing the value of `module.exports` at the time they finished evaluating.
+### <code>import</code> statements
+
+An `import` statement can reference either ES module or CommonJS JavaScript.
+Other file types such as JSON and Native modules are not supported. For those,
+use [`module.createRequireFromPath()`][].
+
+`import` statements are permitted only in ES modules. For similar functionality
+in CommonJS, see [`import()`][].
+
+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.
```js
-// foo.js
-module.exports = { one: 1 };
+import { sin, cos } from 'geometry/trigonometry-functions.mjs';
+```
+
+> Currently only the “default export” is supported for CommonJS files or
+> packages:
+>
+> <!-- eslint-disable no-duplicate-imports -->
+> ```js
+> import packageMain from 'commonjs-package'; // Works
+>
+> import { method } from 'commonjs-package'; // Errors
+> ```
+>
+> There are ongoing efforts to make the latter code possible.
-// bar.mjs
-import foo from './foo.js';
-foo.one === 1; // true
+### <code>import()</code> expressions
+
+Dynamic `import()` is supported in both CommonJS and ES modules. It can be used
+to include ES module files from CommonJS code.
+
+```js
+(async () => {
+ await import('./my-app.mjs');
+})();
```
+## CommonJS, JSON, and Native Modules
+
+CommonJS, JSON, and Native modules can be used with [`module.createRequireFromPath()`][].
+
+```js
+// cjs.js
+module.exports = 'cjs';
+
+// esm.mjs
+import { createRequireFromPath as createRequire } from 'module';
+import { fileURLToPath as fromURL } from 'url';
+
+const require = createRequire(fromURL(import.meta.url));
+
+const cjs = require('./cjs');
+cjs === 'cjs'; // true
+```
+
+## Builtin modules
+
Builtin modules will provide named exports of their public API, as well as a
default export which can be used for, among other things, modifying the named
exports. Named exports of builtin modules are updated when the corresponding
@@ -132,7 +391,41 @@ fs.readFileSync = () => Buffer.from('Hello, ESM');
fs.readFileSync === readFileSync;
```
-## Loader hooks
+## Experimental JSON Modules
+
+**Note: This API is still being designed and is subject to change.**
+
+Currently importing JSON modules are only supported in the `commonjs` mode
+and are loaded using the CJS loader. [WHATWG JSON modules][] are currently
+being standardized, and are experimentally supported by including the
+additional flag `--experimental-json-modules` when running Node.js.
+
+When the `--experimental-json-modules` flag is included both the
+`commonjs` and `module` mode will use the new experimental JSON
+loader. The imported JSON only exposes a `default`, there is no
+support for named exports. A cache entry is created in the CommonJS
+cache, to avoid duplication. The same object will be returned in
+CommonJS if the JSON module has already been imported from the
+same path.
+
+Assuming an `index.js` with
+
+<!-- eslint-skip -->
+```js
+import packageConfig from './package.json';
+```
+
+The `--experimental-json-modules` flag is needed for the module
+to work.
+
+```bash
+node --experimental-modules --entry-type=module index.js # fails
+node --experimental-modules --entry-type=module --experimental-json-modules index.js # works
+```
+
+## Experimental Loader hooks
+
+**Note: This API is currently being redesigned and will still change.**
<!-- type=misc -->
@@ -173,11 +466,10 @@ module. This can be one of the following:
| `format` | Description |
| --- | --- |
-| `'esm'` | Load a standard JavaScript module |
-| `'cjs'` | Load a node-style CommonJS module |
-| `'builtin'` | Load a node builtin CommonJS module |
+| `'module'` | Load a standard JavaScript module |
+| `'commonjs'` | Load a Node.js CommonJS module |
+| `'builtin'` | Load a Node.js builtin module |
| `'json'` | Load a JSON file |
-| `'addon'` | Load a [C++ Addon][addons] |
| `'dynamic'` | Use a [dynamic instantiate hook][] |
For example, a dummy loader to load JavaScript restricted to browser resolution
@@ -253,6 +545,184 @@ With the list of module exports provided upfront, the `execute` function will
then be called at the exact point of module evaluation order for that module
in the import tree.
+## Resolution Algorithm
+
+### Features
+
+The resolver has the following properties:
+
+* FileURL-based resolution as is used by ES modules
+* Support for builtin module loading
+* Relative and absolute URL resolution
+* No default extensions
+* No folder mains
+* Bare specifier package resolution lookup through node_modules
+
+### Resolver Algorithm
+
+The algorithm to load an ES module specifier is given through the
+**ESM_RESOLVE** method below. It returns the resolved URL for a
+module specifier relative to a parentURL, in addition to the unique module
+format for that resolved URL given by the **ESM_FORMAT** routine.
+
+The _"module"_ format is returned for an ECMAScript Module, while the
+_"commonjs"_ format is used to indicate loading through the legacy
+CommonJS loader. Additional formats such as _"wasm"_ or _"addon"_ can be
+extended in future updates.
+
+In the following algorithms, all subroutine errors are propagated as errors
+of these top-level routines.
+
+_isMain_ is **true** when resolving the Node.js application entry point.
+
+When using the `--entry-type` flag, it overrides the ESM_FORMAT result while
+providing errors in the case of explicit conflicts.
+
+<details>
+<summary>Resolver algorithm specification</summary>
+
+**ESM_RESOLVE(_specifier_, _parentURL_, _isMain_)**
+> 1. Let _resolvedURL_ be **undefined**.
+> 1. If _specifier_ is a valid URL, then
+> 1. Set _resolvedURL_ to the result of parsing and reserializing
+> _specifier_ as a URL.
+> 1. Otherwise, if _specifier_ starts with _"/"_, then
+> 1. Throw an _Invalid Specifier_ error.
+> 1. Otherwise, if _specifier_ starts with _"./"_ or _"../"_, then
+> 1. Set _resolvedURL_ to the URL resolution of _specifier_ relative to
+> _parentURL_.
+> 1. Otherwise,
+> 1. Note: _specifier_ is now a bare specifier.
+> 1. Set _resolvedURL_ the result of
+> **PACKAGE_RESOLVE**(_specifier_, _parentURL_).
+> 1. If the file at _resolvedURL_ does not exist, then
+> 1. Throw a _Module Not Found_ error.
+> 1. Set _resolvedURL_ to the real path of _resolvedURL_.
+> 1. Let _format_ be the result of **ESM_FORMAT**(_resolvedURL_, _isMain_).
+> 1. Load _resolvedURL_ as module format, _format_.
+
+PACKAGE_RESOLVE(_packageSpecifier_, _parentURL_)
+> 1. Let _packageName_ be *undefined*.
+> 1. Let _packageSubpath_ be *undefined*.
+> 1. If _packageSpecifier_ is an empty string, then
+> 1. Throw an _Invalid Specifier_ error.
+> 1. If _packageSpecifier_ does not start with _"@"_, then
+> 1. Set _packageName_ to the substring of _packageSpecifier_ until the
+> first _"/"_ separator or the end of the string.
+> 1. Otherwise,
+> 1. If _packageSpecifier_ does not contain a _"/"_ separator, then
+> 1. Throw an _Invalid Specifier_ error.
+> 1. Set _packageName_ to the substring of _packageSpecifier_
+> until the second _"/"_ separator or the end of the string.
+> 1. Let _packageSubpath_ be the substring of _packageSpecifier_ from the
+> position at the length of _packageName_ plus one, if any.
+> 1. Assert: _packageName_ is a valid package name or scoped package name.
+> 1. Assert: _packageSubpath_ is either empty, or a path without a leading
+> separator.
+> 1. If _packageSubpath_ contains any _"."_ or _".."_ segments or percent
+> encoded strings for _"/"_ or _"\\"_ then,
+> 1. Throw an _Invalid Specifier_ error.
+> 1. If _packageSubpath_ is empty and _packageName_ is a Node.js builtin
+> module, then
+> 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_.
+> 1. Set _parentURL_ to the parent folder URL of _parentURL_.
+> 1. If the folder at _packageURL_ does not exist, then
+> 1. Set _parentURL_ to the parent URL path of _parentURL_.
+> 1. Continue the next loop iteration.
+> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_packageURL_).
+> 1. If _packageSubpath_ is empty, then
+> 1. Return the result of **PACKAGE_MAIN_RESOLVE**(_packageURL_,
+> _pjson_).
+> 1. Otherwise,
+> 1. Return the URL resolution of _packageSubpath_ in _packageURL_.
+> 1. Throw a _Module Not Found_ error.
+
+PACKAGE_MAIN_RESOLVE(_packageURL_, _pjson_)
+> 1. If _pjson_ is **null**, then
+> 1. Throw a _Module Not Found_ error.
+> 1. If _pjson.main_ is a String, then
+> 1. Let _resolvedMain_ be the concatenation of _packageURL_, "/", and
+> _pjson.main_.
+> 1. If the file at _resolvedMain_ exists, then
+> 1. Return _resolvedMain_.
+> 1. If _pjson.type_ is equal to _"module"_, then
+> 1. Throw a _Module Not Found_ error.
+> 1. Let _legacyMainURL_ be the result applying the legacy
+> **LOAD_AS_DIRECTORY** CommonJS resolver to _packageURL_, throwing a
+> _Module Not Found_ error for no resolution.
+> 1. If _legacyMainURL_ does not end in _".js"_ then,
+> 1. Throw an _Unsupported File Extension_ error.
+> 1. Return _legacyMainURL_.
+
+**ESM_FORMAT(_url_, _isMain_)**
+> 1. Assert: _url_ corresponds to an existing file.
+> 1. Let _pjson_ be the result of **READ_PACKAGE_SCOPE**(_url_).
+> 1. If _url_ ends in _".mjs"_, then
+> 1. Return _"module"_.
+> 1. If _url_ ends in _".cjs"_, then
+> 1. Return _"commonjs"_.
+> 1. If _pjson?.type_ exists and is _"module"_, then
+> 1. If _isMain_ is **true** or _url_ ends in _".js"_, then
+> 1. Return _"module"_.
+> 1. Throw an _Unsupported File Extension_ error.
+> 1. Otherwise,
+> 1. If _isMain_ is **true** or _url_ ends in _".js"_, _".json"_ or
+> _".node"_, then
+> 1. Return _"commonjs"_.
+> 1. Throw an _Unsupported File Extension_ error.
+
+READ_PACKAGE_SCOPE(_url_)
+> 1. Let _scopeURL_ be _url_.
+> 1. While _scopeURL_ is not the file system root,
+> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_scopeURL_).
+> 1. If _pjson_ is not **null**, then
+> 1. Return _pjson_.
+> 1. Set _scopeURL_ to the parent URL of _scopeURL_.
+> 1. Return **null**.
+
+READ_PACKAGE_JSON(_packageURL_)
+> 1. Let _pjsonURL_ be the resolution of _"package.json"_ within _packageURL_.
+> 1. If the file at _pjsonURL_ does not exist, then
+> 1. Return **null**.
+> 1. If the file at _packageURL_ does not parse as valid JSON, then
+> 1. Throw an _Invalid Package Configuration_ error.
+> 1. Return the parsed JSON source of the file at _pjsonURL_.
+
+</details>
+
+### Customizing ESM specifier resolution algorithm
+
+The current specifier resolution does not support all default behavior of
+the CommonJS loader. One of the behavior differences is automatic resolution
+of file extensions and the ability to import directories that have an index
+file.
+
+The `--es-module-specifier-resolution=[mode]` flag can be used to customize
+the extension resolution algorithm. The default mode is `explicit`, which
+requires the full path to a module be provided to the loader. To enable the
+automatic extension resolution and importing from directories that include an
+index file use the `node` mode.
+
+```bash
+$ node --experimental-modules index.mjs
+success!
+$ node --experimental-modules index #Failure!
+Error: Cannot find module
+$ node --experimental-modules --es-module-specifier-resolution=node index
+success!
+```
+
+[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
+[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
+[`import()`]: #esm_import-expressions
+[`import.meta.url`]: #esm_import_meta
+[`module.createRequireFromPath()`]: modules.html#modules_module_createrequirefrompath_filename
+[CommonJS]: modules.html
+[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
-[addons]: addons.html
+[WHATWG JSON modules]: https://github.com/whatwg/html/issues/4315
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
+[the official standard format]: https://tc39.github.io/ecma262/#sec-modules