diff --git a/doc/api/cli.md b/doc/api/cli.md
index 62e13496c0..4939574e88 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -513,13 +513,13 @@ Track heap object allocations for heap snapshots.
### `-m`, `--type=type`
-When using `--experimental-modules`, this informs the module resolution type
-to interpret the top-level entry into Node.js.
+Used with `--experimental-modules`, this configures Node.js to interpret the
+initial entry point as CommonJS or as an ES module.
-Works with stdin, `--eval`, `--print` as well as standard execution.
+Valid values are `"commonjs"` and `"module"`. The default is to infer from
+the file extension and the `"type"` field in the nearest parent `package.json`.
-Valid values are `"commonjs"` and `"module"`, where the default is to infer
-from the file extension and package type boundary.
+Works for executing a file as well as `--eval`, `--print`, `STDIN`.
`-m` is an alias for `--type=module`.
diff --git a/doc/api/esm.md b/doc/api/esm.md
index 25c2e82673..5a144b09a6 100644
--- a/doc/api/esm.md
+++ b/doc/api/esm.md
@@ -8,37 +8,223 @@
Node.js contains support for ES Modules based upon the
-[Node.js EP for ES Modules][] and the [ESM Minimal Kernel][].
+[Node.js EP for ES Modules][] and the [ecmascript-modules implementation][].
-The minimal feature set is designed to be compatible with all potential
-future implementations. Expect major changes in the implementation including
-interoperability support, specifier resolution, and default behavior.
+Expect major changes in the implementation including interoperability support,
+specifier resolution, and default behavior.
## Enabling
-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).
-Once this has been set, files ending with `.mjs` will be able to be loaded
-as ES Modules.
+## Running Node.js with an ECMAScript Module
+
+There are a few ways to start Node.js with an ES module as its input.
+
+### Initial entry point with an .mjs
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
+### --type=module
/ -m
flag
-
+Files ending with `.js` or `.mjs`, or lacking any extension,
+will be loaded as ES modules when the `--type=module` flag is set.
+This flag also has a shorthand alias `-m`.
+
+```sh
+node --experimental-modules --type=module my-app.js
+# or
+node --experimental-modules -m my-app.js
+```
+
+For completeness there is also `--type=commonjs`, for explicitly running a `.js`
+file as CommonJS. This is the default behavior if `--type` or `-m` is
+unspecified.
+
+The `--type=module` or `-m` flags 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 --type=module --eval \
+ "import { sep } from 'path'; console.log(sep);"
+
+echo "import { sep } from 'path'; console.log(sep);" | \
+ node --experimental-modules --type=module
+```
+
+### package.json
"type"
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.
+
+
+```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 deafult, 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.
-### Supported
+```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"
+```
-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.
+Files ending with `.mjs` are always loaded as ES modules regardless of package
+scope.
-#### import.meta
+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
+```
+
+You can use the `.mjs` and `.cjs` extensions 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`.
+
+
+```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.
+
+## import
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.
+
+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.
+
+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
* {Object}
@@ -47,37 +233,44 @@ property:
* `url` {string} The absolute `file:` URL of the module.
-### Unsupported
-
-| Feature | Reason |
-| --- | --- |
-| `require('./foo.mjs')` | ES Modules have differing resolution and timing, use dynamic import |
-
-## Notable differences between `import` and `require`
+## Differences Between ES Modules and CommonJS
### Mandatory file extensions
-You must provide a file extension when using the `import` keyword.
+You must provide a file extension when using the `import` keyword. Directory
+indexes (e.g. `'./startup/index.js'`) must also be fully specified.
+
+This behavior matches how `import` behaves in browser environments, assuming a
+typically configured server.
-### No NODE_PATH
+### No NODE_PATH
`NODE_PATH` is not part of resolving `import` specifiers. Please use symlinks
if this behavior is desired.
-### No `require.extensions`
+### No require
, exports
, module.exports
, \_\_filename
, \_\_dirname
+
+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 require.extensions
`require.extensions` is not used by `import`. The expectation is that loader
hooks can provide this workflow in the future.
-### No `require.cache`
+### No require.cache
`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.
@@ -89,6 +282,59 @@ import './foo.mjs?query=2'; // loads ./foo.mjs with query of "?query=2"
For now, only modules using the `file:` protocol can be loaded.
+## Interoperability with CommonJS
+
+### require
+
+`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()`][].
+
+To include an ES module into CommonJS, use [`import()`][].
+
+### import
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
+import { sin, cos } from 'geometry/trigonometry-functions.mjs';
+```
+
+> Currently only the “default export” is supported for CommonJS files or
+> packages:
+>
+>
+> ```js
+> import packageMain from 'commonjs-package'; // Works
+>
+> import { method } from 'commonjs-package'; // Errors
+> ```
+>
+> There are ongoing efforts to make the latter code possible.
+
+### import()
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()`][].
@@ -99,9 +345,9 @@ module.exports = 'cjs';
// esm.mjs
import { createRequireFromPath as createRequire } from 'module';
-import { fileURLToPath as fromPath } from 'url';
+import { fileURLToPath as fromURL } from 'url';
-const require = createRequire(fromPath(import.meta.url));
+const require = createRequire(fromURL(import.meta.url));
const cjs = require('./cjs');
cjs === 'cjs'; // true
@@ -138,6 +384,127 @@ fs.readFileSync = () => Buffer.from('Hello, ESM');
fs.readFileSync === readFileSync;
```
+## Experimental Loader hooks
+
+**Note: This API is currently being redesigned and will still change.**.
+
+
+
+To customize the default module resolution, loader hooks can optionally be
+provided via a `--loader ./loader-name.mjs` argument to Node.js.
+
+When hooks are used they only apply to ES module loading and not to any
+CommonJS modules loaded.
+
+### Resolve hook
+
+The resolve hook returns the resolved file URL and module format for a
+given module specifier and parent file URL:
+
+```js
+const baseURL = new URL('file://');
+baseURL.pathname = `${process.cwd()}/`;
+
+export async function resolve(specifier,
+ parentModuleURL = baseURL,
+ defaultResolver) {
+ return {
+ url: new URL(specifier, parentModuleURL).href,
+ format: 'esm'
+ };
+}
+```
+
+The `parentModuleURL` is provided as `undefined` when performing main Node.js
+load itself.
+
+The default Node.js ES module resolution function is provided as a third
+argument to the resolver for easy compatibility workflows.
+
+In addition to returning the resolved file URL value, the resolve hook also
+returns a `format` property specifying the module format of the resolved
+module. This can be one of the following:
+
+| `format` | Description |
+| --- | --- |
+| `'module'` | Load a standard JavaScript module |
+| `'commonjs'` | Load a Node.js CommonJS module |
+| `'builtin'` | Load a Node.js builtin module |
+| `'dynamic'` | Use a [dynamic instantiate hook][] |
+
+For example, a dummy loader to load JavaScript restricted to browser resolution
+rules with only JS file extension and Node.js builtin modules support could
+be written:
+
+```js
+import path from 'path';
+import process from 'process';
+import Module from 'module';
+
+const builtins = Module.builtinModules;
+const JS_EXTENSIONS = new Set(['.js', '.mjs']);
+
+const baseURL = new URL('file://');
+baseURL.pathname = `${process.cwd()}/`;
+
+export function resolve(specifier, parentModuleURL = baseURL, defaultResolve) {
+ if (builtins.includes(specifier)) {
+ return {
+ url: specifier,
+ format: 'builtin'
+ };
+ }
+ if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
+ // For node_modules support:
+ // return defaultResolve(specifier, parentModuleURL);
+ throw new Error(
+ `imports must begin with '/', './', or '../'; '${specifier}' does not`);
+ }
+ const resolved = new URL(specifier, parentModuleURL);
+ const ext = path.extname(resolved.pathname);
+ if (!JS_EXTENSIONS.has(ext)) {
+ throw new Error(
+ `Cannot load file with non-JavaScript file extension ${ext}.`);
+ }
+ return {
+ url: resolved.href,
+ format: 'esm'
+ };
+}
+```
+
+With this loader, running:
+
+```console
+NODE_OPTIONS='--experimental-modules --loader ./custom-loader.mjs' node x.js
+```
+
+would load the module `x.js` as an ES module with relative resolution support
+(with `node_modules` loading skipped in this example).
+
+### Dynamic instantiate hook
+
+To create a custom dynamic module that doesn't correspond to one of the
+existing `format` interpretations, the `dynamicInstantiate` hook can be used.
+This hook is called only for modules that return `format: 'dynamic'` from
+the `resolve` hook.
+
+```js
+export async function dynamicInstantiate(url) {
+ return {
+ exports: ['customExportName'],
+ execute: (exports) => {
+ // Get and set functions provided for pre-allocated export names
+ exports.customExportName.set('value');
+ }
+ };
+}
+```
+
+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
@@ -174,6 +541,9 @@ entirely for the CommonJS loader.
If the top-level `--type` is _"module"_, then the ESM resolver is used
as described here, with the conditional `--type` check in **ESM_FORMAT**.
+
+Resolver algorithm psuedocode
+
**ESM_RESOLVE(_specifier_, _parentURL_, _isMain_)**
> 1. Let _resolvedURL_ be **undefined**.
> 1. If _specifier_ is a valid URL, then
@@ -256,7 +626,7 @@ PACKAGE_MAIN_RESOLVE(_packageURL_, _pjson_)
> 1. If _url_ ends with _".cjs"_, then
> 1. Throw a _Type Mismatch_ error.
> 1. Return _"module"_.
-> 1. Let _pjson_ be the result of **READ_PACKAGE_BOUNDARY**(_url_).
+> 1. Let _pjson_ be the result of **READ_PACKAGE_SCOPE**(_url_).
> 1. If _pjson_ is **null** and _isMain_ is **true**, then
> 1. If _url_ ends in _".mjs"_, then
> 1. Return _"module"_.
@@ -272,13 +642,13 @@ PACKAGE_MAIN_RESOLVE(_packageURL_, _pjson_)
> 1. Throw an _Unsupported File Extension_ error.
> 1. Return _"commonjs"_.
-READ_PACKAGE_BOUNDARY(_url_)
-> 1. Let _boundaryURL_ be _url_.
-> 1. While _boundaryURL_ is not the file system root,
-> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_boundaryURL_).
+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 _boundaryURL_ to the parent URL of _boundaryURL_.
+> 1. Set _scopeURL_ to the parent URL of _scopeURL_.
> 1. Return **null**.
READ_PACKAGE_JSON(_packageURL_)
@@ -289,128 +659,11 @@ READ_PACKAGE_JSON(_packageURL_)
> 1. Throw an _Invalid Package Configuration_ error.
> 1. Return the parsed JSON source of the file at _pjsonURL_.
-## Experimental Loader hooks
-
-**Note: This API is currently being redesigned and will still change.**.
-
-
-
-To customize the default module resolution, loader hooks can optionally be
-provided via a `--loader ./loader-name.mjs` argument to Node.js.
-
-When hooks are used they only apply to ES module loading and not to any
-CommonJS modules loaded.
-
-### Resolve hook
-
-The resolve hook returns the resolved file URL and module format for a
-given module specifier and parent file URL:
-
-```js
-const baseURL = new URL('file://');
-baseURL.pathname = `${process.cwd()}/`;
-
-export async function resolve(specifier,
- parentModuleURL = baseURL,
- defaultResolver) {
- return {
- url: new URL(specifier, parentModuleURL).href,
- format: 'esm'
- };
-}
-```
-
-The `parentModuleURL` is provided as `undefined` when performing main Node.js
-load itself.
-
-The default Node.js ES module resolution function is provided as a third
-argument to the resolver for easy compatibility workflows.
-
-In addition to returning the resolved file URL value, the resolve hook also
-returns a `format` property specifying the module format of the resolved
-module. This can be one of the following:
-
-| `format` | Description |
-| --- | --- |
-| `'module'` | Load a standard JavaScript module |
-| `'commonjs'` | Load a Node.js CommonJS module |
-| `'builtin'` | Load a Node.js builtin module |
-| `'dynamic'` | Use a [dynamic instantiate hook][] |
-
-For example, a dummy loader to load JavaScript restricted to browser resolution
-rules with only JS file extension and Node.js builtin modules support could
-be written:
-
-```js
-import path from 'path';
-import process from 'process';
-import Module from 'module';
-
-const builtins = Module.builtinModules;
-const JS_EXTENSIONS = new Set(['.js', '.mjs']);
-
-const baseURL = new URL('file://');
-baseURL.pathname = `${process.cwd()}/`;
-
-export function resolve(specifier, parentModuleURL = baseURL, defaultResolve) {
- if (builtins.includes(specifier)) {
- return {
- url: specifier,
- format: 'builtin'
- };
- }
- if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
- // For node_modules support:
- // return defaultResolve(specifier, parentModuleURL);
- throw new Error(
- `imports must begin with '/', './', or '../'; '${specifier}' does not`);
- }
- const resolved = new URL(specifier, parentModuleURL);
- const ext = path.extname(resolved.pathname);
- if (!JS_EXTENSIONS.has(ext)) {
- throw new Error(
- `Cannot load file with non-JavaScript file extension ${ext}.`);
- }
- return {
- url: resolved.href,
- format: 'esm'
- };
-}
-```
-
-With this loader, running:
-
-```console
-NODE_OPTIONS='--experimental-modules --loader ./custom-loader.mjs' node x.js
-```
-
-would load the module `x.js` as an ES module with relative resolution support
-(with `node_modules` loading skipped in this example).
-
-### Dynamic instantiate hook
-
-To create a custom dynamic module that doesn't correspond to one of the
-existing `format` interpretations, the `dynamicInstantiate` hook can be used.
-This hook is called only for modules that return `format: 'dynamic'` from
-the `resolve` hook.
-
-```js
-export async function dynamicInstantiate(url) {
- return {
- exports: ['customExportName'],
- execute: (exports) => {
- // Get and set functions provided for pre-allocated export names
- exports.customExportName.set('value');
- }
- };
-}
-```
-
-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.
+
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
[`module.createRequireFromPath()`]: modules.html#modules_module_createrequirefrompath_filename
-[ESM Minimal Kernel]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
+[`import.meta.url`]: esm.html#importmeta
+[`import()`]: esm.html#import-expressions
+[ecmascript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md