From ff8a62474e48da02385fc6b7022232db8cde2272 Mon Sep 17 00:00:00 2001 From: Antoine du HAMEL Date: Fri, 22 May 2020 13:50:46 +0200 Subject: [PATCH 01/11] doc: merge CJS and ESM docs Documents package.json supported fields. Fixes: /~https://github.com/nodejs/node/issues/33143 --- doc/api/esm.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/api/esm.md b/doc/api/esm.md index 8a3d5bdecbdb2e..f68a0c1ff09bc5 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -1,5 +1,7 @@ # ECMAScript Modules + + From ce340ee60b7f227edae2f931bf04359766ab82e2 Mon Sep 17 00:00:00 2001 From: Antoine du HAMEL Date: Fri, 22 May 2020 21:18:04 +0200 Subject: [PATCH 02/11] Refactor modules.md CJS sections --- doc/api/modules.md | 386 +++++++++++++++++++++++---------------------- 1 file changed, 197 insertions(+), 189 deletions(-) diff --git a/doc/api/modules.md b/doc/api/modules.md index afbc849180b414..18a6c9470137ee 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -2,6 +2,10 @@ +## Introduction + +### CommonJS modules + > Stability: 2 - Stable @@ -63,7 +67,9 @@ module.exports = class Square { The module system is implemented in the `require('module')` module. -## Accessing the main module +## CommonJS modules + +### Accessing the main module @@ -78,167 +84,14 @@ Because `module` provides a `filename` property (normally equivalent to `__filename`), the entry point of the current application can be obtained by checking `require.main.filename`. -## Addenda: Package Manager Tips - - - -The semantics of the Node.js `require()` function were designed to be general -enough to support reasonable directory structures. Package manager programs -such as `dpkg`, `rpm`, and `npm` will hopefully find it possible to build -native packages from Node.js modules without modification. - -Below we give a suggested directory structure that could work: - -Let's say that we wanted to have the folder at -`/usr/lib/node//` hold the contents of a -specific version of a package. - -Packages can depend on one another. In order to install package `foo`, it -may be necessary to install a specific version of package `bar`. The `bar` -package may itself have dependencies, and in some cases, these may even collide -or form cyclic dependencies. - -Since Node.js looks up the `realpath` of any modules it loads (that is, -resolves symlinks), and then looks for their dependencies in the `node_modules` -folders as described [here](#modules_loading_from_node_modules_folders), this -situation is very simple to resolve with the following architecture: - -* `/usr/lib/node/foo/1.2.3/`: Contents of the `foo` package, version 1.2.3. -* `/usr/lib/node/bar/4.3.2/`: Contents of the `bar` package that `foo` depends - on. -* `/usr/lib/node/foo/1.2.3/node_modules/bar`: Symbolic link to - `/usr/lib/node/bar/4.3.2/`. -* `/usr/lib/node/bar/4.3.2/node_modules/*`: Symbolic links to the packages that - `bar` depends on. - -Thus, even if a cycle is encountered, or if there are dependency -conflicts, every module will be able to get a version of its dependency -that it can use. - -When the code in the `foo` package does `require('bar')`, it will get the -version that is symlinked into `/usr/lib/node/foo/1.2.3/node_modules/bar`. -Then, when the code in the `bar` package calls `require('quux')`, it'll get -the version that is symlinked into -`/usr/lib/node/bar/4.3.2/node_modules/quux`. - -Furthermore, to make the module lookup process even more optimal, rather -than putting packages directly in `/usr/lib/node`, we could put them in -`/usr/lib/node_modules//`. Then Node.js will not bother -looking for missing dependencies in `/usr/node_modules` or `/node_modules`. - -In order to make modules available to the Node.js REPL, it might be useful to -also add the `/usr/lib/node_modules` folder to the `$NODE_PATH` environment -variable. Since the module lookups using `node_modules` folders are all -relative, and based on the real path of the files making the calls to -`require()`, the packages themselves can be anywhere. - -## Addenda: The `.mjs` extension +### Addenda: The `.mjs` extension It is not possible to `require()` files that have the `.mjs` extension. Attempting to do so will throw [an error][]. The `.mjs` extension is reserved for [ECMAScript Modules][] which cannot be loaded via `require()`. See [ECMAScript Modules][] for more details. -## All Together... - - - -To get the exact filename that will be loaded when `require()` is called, use -the `require.resolve()` function. - -Putting together all of the above, here is the high-level algorithm -in pseudocode of what `require()` does: - -```txt -require(X) from module at path Y -1. If X is a core module, - a. return the core module - b. STOP -2. If X begins with '/' - a. set Y to be the filesystem root -3. If X begins with './' or '/' or '../' - a. LOAD_AS_FILE(Y + X) - b. LOAD_AS_DIRECTORY(Y + X) - c. THROW "not found" -4. LOAD_SELF_REFERENCE(X, dirname(Y)) -5. LOAD_NODE_MODULES(X, dirname(Y)) -6. THROW "not found" - -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, load X.js as JavaScript text. STOP -3. If X.json is a file, parse X.json to a JavaScript Object. STOP -4. If X.node is a file, load X.node as binary addon. STOP - -LOAD_INDEX(X) -1. If X/index.js is a file, load X/index.js as JavaScript text. STOP -2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP -3. If X/index.node is a file, load X/index.node as binary addon. STOP - -LOAD_AS_DIRECTORY(X) -1. If X/package.json is a file, - a. Parse X/package.json, and look for "main" field. - b. If "main" is a falsy value, GOTO 2. - c. let M = X + (json main field) - d. LOAD_AS_FILE(M) - e. LOAD_INDEX(M) - f. LOAD_INDEX(X) DEPRECATED - g. THROW "not found" -2. LOAD_INDEX(X) - -LOAD_NODE_MODULES(X, START) -1. let DIRS = NODE_MODULES_PATHS(START) -2. for each DIR in DIRS: - a. LOAD_PACKAGE_EXPORTS(DIR, X) - b. LOAD_AS_FILE(DIR/X) - c. LOAD_AS_DIRECTORY(DIR/X) - -NODE_MODULES_PATHS(START) -1. let PARTS = path split(START) -2. let I = count of PARTS - 1 -3. let DIRS = [GLOBAL_FOLDERS] -4. while I >= 0, - a. if PARTS[I] = "node_modules" CONTINUE - b. DIR = path join(PARTS[0 .. I] + "node_modules") - c. DIRS = DIRS + DIR - d. let I = I - 1 -5. return DIRS - -LOAD_SELF_REFERENCE(X, START) -1. Find the closest package scope to START. -2. If no scope was found, return. -3. If the `package.json` has no "exports", return. -4. If the name in `package.json` isn't a prefix of X, throw "not found". -5. Otherwise, load the remainder of X relative to this package as if it - was loaded via `LOAD_NODE_MODULES` with a name in `package.json`. - -LOAD_PACKAGE_EXPORTS(DIR, X) -1. Try to interpret X as a combination of name and subpath where the name - may have a @scope/ prefix and the subpath begins with a slash (`/`). -2. If X does not match this pattern or DIR/name/package.json is not a file, - return. -3. Parse DIR/name/package.json, and look for "exports" field. -4. If "exports" is null or undefined, return. -5. If "exports" is an object with some keys starting with "." and some keys - not starting with ".", throw "invalid config". -6. If "exports" is a string, or object with no keys starting with ".", treat - it as having that value as its "." object property. -7. If subpath is "." and "exports" does not have a "." entry, return. -8. Find the longest key in "exports" that the subpath starts with. -9. If no such key can be found, throw "not found". -10. let RESOLVED = - fileURLToPath(PACKAGE_EXPORTS_TARGET_RESOLVE(pathToFileURL(DIR/name), - exports[key], subpath.slice(key.length), ["node", "require"])), as defined - in the ESM resolver. -11. If key ends with "/": - a. LOAD_AS_FILE(RESOLVED) - b. LOAD_AS_DIRECTORY(RESOLVED) -12. Otherwise - a. If RESOLVED is a file, load it as its file extension format. STOP -13. Throw "not found" -``` - -## Caching +### Caching @@ -254,7 +107,7 @@ allowing transitive dependencies to be loaded even when they would cause cycles. To have a module execute code multiple times, export a function, and call that function. -### Module Caching Caveats +#### Module Caching Caveats @@ -269,7 +122,7 @@ them as different modules and will reload the file multiple times. For example, `require('./foo')` and `require('./FOO')` return two different objects, irrespective of whether or not `./foo` and `./FOO` are the same file. -## Core Modules +### Core Modules @@ -283,7 +136,7 @@ Core modules are always preferentially loaded if their identifier is passed to `require()`. For instance, `require('http')` will always return the built in HTTP module, even if there is a file by that name. -## Cycles +### Cycles @@ -347,7 +200,7 @@ in main, a.done = true, b.done = true Careful planning is required to allow cyclic module dependencies to work correctly within an application. -## File Modules +### File Modules @@ -373,7 +226,7 @@ either be a core module or is loaded from a `node_modules` folder. If the given path does not exist, `require()` will throw an [`Error`][] with its `code` property set to `'MODULE_NOT_FOUND'`. -## Folders as Modules +### Folders as Modules @@ -413,7 +266,7 @@ with the default error: Error: Cannot find module 'some-library' ``` -## Loading from `node_modules` Folders +### Loading from `node_modules` Folders @@ -445,7 +298,7 @@ module by including a path suffix after the module name. For instance relative to where `example-module` is located. The suffixed path follows the same module resolution semantics. -## Loading from the global folders +### Loading from the global folders @@ -479,7 +332,7 @@ These are mostly for historic reasons. It is strongly encouraged to place dependencies in the local `node_modules` folder. These will be loaded faster, and more reliably. -## The module wrapper +### The module wrapper @@ -503,9 +356,9 @@ the module rather than the global object. * The convenience variables `__filename` and `__dirname`, containing the module's absolute filename and directory path. -## The module scope +### The module scope -### `__dirname` +#### `__dirname` @@ -526,7 +379,7 @@ console.log(path.dirname(__filename)); // Prints: /Users/mjr ``` -### `__filename` +#### `__filename` @@ -564,7 +417,7 @@ References to `__filename` within `b.js` will return `/Users/mjr/app/node_modules/b/b.js` while references to `__filename` within `a.js` will return `/Users/mjr/app/a.js`. -### `exports` +#### `exports` @@ -577,7 +430,7 @@ A reference to the `module.exports` that is shorter to type. See the section about the [exports shortcut][] for details on when to use `exports` and when to use `module.exports`. -### `module` +#### `module` @@ -590,7 +443,7 @@ A reference to the current module, see the section about the [`module` object][]. In particular, `module.exports` is used for defining what a module exports and makes available through `require()`. -### `require(id)` +#### `require(id)` @@ -620,7 +473,7 @@ const jsonData = require('./path/filename.json'); const crypto = require('crypto'); ``` -#### `require.cache` +##### `require.cache` @@ -637,7 +490,7 @@ native modules and if a name matching a native module is added to the cache, no require call is going to receive the native module anymore. Use with care! -#### `require.extensions` +##### `require.extensions` @@ -701,7 +554,7 @@ Module { '/node_modules' ] } ``` -#### `require.resolve(request[, options])` +##### `require.resolve(request[, options])` @@ -737,7 +590,7 @@ Returns an array containing the paths searched during resolution of `request` or `null` if the `request` string references a core module, for example `http` or `fs`. -## The `module` Object +### The `module` Object @@ -752,7 +605,7 @@ representing the current module. For convenience, `module.exports` is also accessible via the `exports` module-global. `module` is not actually a global but rather local to each module. -### `module.children` +#### `module.children` @@ -761,7 +614,7 @@ added: v0.1.16 The module objects required for the first time by this one. -### `module.exports` +#### `module.exports` @@ -815,7 +668,7 @@ const x = require('./x'); console.log(x.a); ``` -#### `exports` shortcut +##### `exports` shortcut @@ -862,7 +715,7 @@ function require(/* ... */) { } ``` -### `module.filename` +#### `module.filename` @@ -871,7 +724,7 @@ added: v0.1.16 The fully resolved filename of the module. -### `module.id` +#### `module.id` @@ -881,7 +734,7 @@ added: v0.1.16 The identifier for the module. Typically this is the fully resolved filename. -### `module.loaded` +#### `module.loaded` @@ -891,7 +744,7 @@ added: v0.1.16 Whether or not the module is done loading, or is in the process of loading. -### `module.parent` +#### `module.parent` @@ -900,7 +753,7 @@ added: v0.1.16 The module that first required this one. -### `module.path` +#### `module.path` @@ -910,7 +763,7 @@ added: v11.14.0 The directory name of the module. This is usually the same as the [`path.dirname()`][] of the [`module.id`][]. -### `module.paths` +#### `module.paths` @@ -919,7 +772,7 @@ added: v0.4.0 The search paths for the module. -### `module.require(id)` +#### `module.require(id)` @@ -935,7 +788,61 @@ Since `require()` returns the `module.exports`, and the `module` is typically *only* available within a specific module's code, it must be explicitly exported in order to be used. -## The `Module` Object +### Tips for Package Manager Authors + + + +The semantics of the Node.js `require()` function were designed to be general +enough to support reasonable directory structures. Package manager programs +such as `dpkg`, `rpm`, and `npm` will hopefully find it possible to build +native packages from Node.js modules without modification. + +Below we give a suggested directory structure that could work: + +Let's say that we wanted to have the folder at +`/usr/lib/node//` hold the contents of a +specific version of a package. + +Packages can depend on one another. In order to install package `foo`, it +may be necessary to install a specific version of package `bar`. The `bar` +package may itself have dependencies, and in some cases, these may even collide +or form cyclic dependencies. + +Since Node.js looks up the `realpath` of any modules it loads (that is, +resolves symlinks), and then looks for their dependencies in the `node_modules` +folders as described [here](#modules_loading_from_node_modules_folders), this +situation is very simple to resolve with the following architecture: + +* `/usr/lib/node/foo/1.2.3/`: Contents of the `foo` package, version 1.2.3. +* `/usr/lib/node/bar/4.3.2/`: Contents of the `bar` package that `foo` depends + on. +* `/usr/lib/node/foo/1.2.3/node_modules/bar`: Symbolic link to + `/usr/lib/node/bar/4.3.2/`. +* `/usr/lib/node/bar/4.3.2/node_modules/*`: Symbolic links to the packages that + `bar` depends on. + +Thus, even if a cycle is encountered, or if there are dependency +conflicts, every module will be able to get a version of its dependency +that it can use. + +When the code in the `foo` package does `require('bar')`, it will get the +version that is symlinked into `/usr/lib/node/foo/1.2.3/node_modules/bar`. +Then, when the code in the `bar` package calls `require('quux')`, it'll get +the version that is symlinked into +`/usr/lib/node/bar/4.3.2/node_modules/quux`. + +Furthermore, to make the module lookup process even more optimal, rather +than putting packages directly in `/usr/lib/node`, we could put them in +`/usr/lib/node_modules//`. Then Node.js will not bother +looking for missing dependencies in `/usr/node_modules` or `/node_modules`. + +In order to make modules available to the Node.js REPL, it might be useful to +also add the `/usr/lib/node_modules` folder to the `$NODE_PATH` environment +variable. Since the module lookups using `node_modules` folders are all +relative, and based on the real path of the files making the calls to +`require()`, the packages themselves can be anywhere. + +## Utility Methods + +To get the exact filename that will be loaded when `require()` is called, use +the `require.resolve()` function. + +Putting together all of the above, here is the high-level algorithm +in pseudocode of what `require()` does: + +```txt +require(X) from module at path Y +1. If X is a core module, + a. return the core module + b. STOP +2. If X begins with '/' + a. set Y to be the filesystem root +3. If X begins with './' or '/' or '../' + a. LOAD_AS_FILE(Y + X) + b. LOAD_AS_DIRECTORY(Y + X) + c. THROW "not found" +4. LOAD_SELF_REFERENCE(X, dirname(Y)) +5. LOAD_NODE_MODULES(X, dirname(Y)) +6. THROW "not found" + +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, load X.js as JavaScript text. STOP +3. If X.json is a file, parse X.json to a JavaScript Object. STOP +4. If X.node is a file, load X.node as binary addon. STOP + +LOAD_INDEX(X) +1. If X/index.js is a file, load X/index.js as JavaScript text. STOP +2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP +3. If X/index.node is a file, load X/index.node as binary addon. STOP + +LOAD_AS_DIRECTORY(X) +1. If X/package.json is a file, + a. Parse X/package.json, and look for "main" field. + b. If "main" is a falsy value, GOTO 2. + c. let M = X + (json main field) + d. LOAD_AS_FILE(M) + e. LOAD_INDEX(M) + f. LOAD_INDEX(X) DEPRECATED + g. THROW "not found" +2. LOAD_INDEX(X) + +LOAD_NODE_MODULES(X, START) +1. let DIRS = NODE_MODULES_PATHS(START) +2. for each DIR in DIRS: + a. LOAD_PACKAGE_EXPORTS(DIR, X) + b. LOAD_AS_FILE(DIR/X) + c. LOAD_AS_DIRECTORY(DIR/X) + +NODE_MODULES_PATHS(START) +1. let PARTS = path split(START) +2. let I = count of PARTS - 1 +3. let DIRS = [GLOBAL_FOLDERS] +4. while I >= 0, + a. if PARTS[I] = "node_modules" CONTINUE + b. DIR = path join(PARTS[0 .. I] + "node_modules") + c. DIRS = DIRS + DIR + d. let I = I - 1 +5. return DIRS + +LOAD_SELF_REFERENCE(X, START) +1. Find the closest package scope to START. +2. If no scope was found, return. +3. If the `package.json` has no "exports", return. +4. If the name in `package.json` isn't a prefix of X, throw "not found". +5. Otherwise, load the remainder of X relative to this package as if it + was loaded via `LOAD_NODE_MODULES` with a name in `package.json`. + +LOAD_PACKAGE_EXPORTS(DIR, X) +1. Try to interpret X as a combination of name and subpath where the name + may have a @scope/ prefix and the subpath begins with a slash (`/`). +2. If X does not match this pattern or DIR/name/package.json is not a file, + return. +3. Parse DIR/name/package.json, and look for "exports" field. +4. If "exports" is null or undefined, return. +5. If "exports" is an object with some keys starting with "." and some keys + not starting with ".", throw "invalid config". +6. If "exports" is a string, or object with no keys starting with ".", treat + it as having that value as its "." object property. +7. If subpath is "." and "exports" does not have a "." entry, return. +8. Find the longest key in "exports" that the subpath starts with. +9. If no such key can be found, throw "not found". +10. let RESOLVED = + fileURLToPath(PACKAGE_EXPORTS_TARGET_RESOLVE(pathToFileURL(DIR/name), + exports[key], subpath.slice(key.length), ["node", "require"])), as defined + in the ESM resolver. +11. If key ends with "/": + a. LOAD_AS_FILE(RESOLVED) + b. LOAD_AS_DIRECTORY(RESOLVED) +12. Otherwise + a. If RESOLVED is a file, load it as its file extension format. STOP +13. Throw "not found" +``` + [GLOBAL_FOLDERS]: #modules_loading_from_the_global_folders [`Error`]: errors.html#errors_class_error [`__dirname`]: #modules_dirname [`__filename`]: #modules_filename [`createRequire()`]: #modules_module_createrequire_filename -[`module` object]: #modules_the_module_object +[`module` object]: #modules_utility_methods [`module.id`]: #modules_module_id [`path.dirname()`]: path.html#path_path_dirname_path [ECMAScript Modules]: esm.html [an error]: errors.html#errors_err_require_esm [exports shortcut]: #modules_exports_shortcut -[module resolution]: #modules_all_together +[module resolution]: #modules_resolution_algorithms [module wrapper]: #modules_the_module_wrapper [native addons]: addons.html [source map include directives]: https://sourcemaps.info/spec.html#h.lmz475t4mvbx From 001af74c310444efe37bc7fbe13afb8dfe5068ea Mon Sep 17 00:00:00 2001 From: Antoine du HAMEL Date: Fri, 22 May 2020 21:21:33 +0200 Subject: [PATCH 03/11] esm intro --- doc/api/esm.md | 189 --------------------------------------------- doc/api/modules.md | 188 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 189 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index f68a0c1ff09bc5..ffd9fa3e9e3162 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -7,195 +7,6 @@ > Stability: 1 - Experimental -## Introduction - - - -ECMAScript modules are [the official standard format][] to package JavaScript -code for reuse. Modules are defined using a variety of [`import`][] and -[`export`][] statements. - -The following example of an ES module exports a function: - -```js -// addTwo.mjs -function addTwo(num) { - return num + 2; -} - -export { addTwo }; -``` - -The following example of an ES module imports the function from `addTwo.mjs`: - -```js -// app.mjs -import { addTwo } from './addTwo.mjs'; - -// Prints: 6 -console.log(addTwo(4)); -``` - -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][] and the [ECMAScript-modules implementation][]. - -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 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" -} -``` - -```sh -# In same folder as above 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 -down until the next folder containing another `package.json`, is considered a -_package scope_. The `"type"` field defines how `.js` 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 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 `-e`), or piped to `node` via -`STDIN`, will be treated as ES modules when the `--input-type=module` flag is -set. - -```sh -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. - ## Packages ### Package Entry Points diff --git a/doc/api/modules.md b/doc/api/modules.md index 18a6c9470137ee..3b5b376a4e30bc 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -67,6 +67,194 @@ module.exports = class Square { The module system is implemented in the `require('module')` module. +### ECMAScript modules + + + + +> Stability: 1 - Experimental + +ECMAScript modules are [the official standard format][] to package JavaScript +code for reuse. Modules are defined using a variety of [`import`][] and +[`export`][] statements. + +The following example of an ES module exports a function: + +```js +// addTwo.mjs +function addTwo(num) { + return num + 2; +} + +export { addTwo }; +``` + +The following example of an ES module imports the function from `addTwo.mjs`: + +```js +// app.mjs +import { addTwo } from './addTwo.mjs'; + +// Prints: 6 +console.log(addTwo(4)); +``` + +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][] and the [ECMAScript-modules implementation][]. + +Expect major changes in the implementation including interoperability support, +specifier resolution, and default behavior. + +## Usage + +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 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. + +```json +// package.json +{ + "type": "module" +} +``` + +```sh +# In same folder as above 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 +down until the next folder containing another `package.json`, is considered a +_package scope_. The `"type"` field defines how `.js` 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 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 `-e`), or piped to `node` via +`STDIN`, will be treated as ES modules when the `--input-type=module` flag is +set. + +```sh +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. + ## CommonJS modules ### Accessing the main module From 897b18a5014ceb127882d0b42c7183c03bdedb6b Mon Sep 17 00:00:00 2001 From: Antoine du HAMEL Date: Fri, 22 May 2020 21:25:07 +0200 Subject: [PATCH 04/11] Move ESM specific doc to modules.md --- doc/api/esm.md | 757 -------------------------------------------- doc/api/modules.md | 762 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 762 insertions(+), 757 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index ffd9fa3e9e3162..fa985b106c8992 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -606,763 +606,6 @@ conditional exports for consumers could be to add an export, e.g. } ``` -## `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:` and `data:` 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:///`. - -#### `data:` Imports - - - -[`data:` URLs][] are supported for importing with the following MIME types: - -* `text/javascript` for ES Modules -* `application/json` for JSON -* `application/wasm` for WASM. - -`data:` URLs only resolve [_Bare specifiers_][Terminology] for builtin modules -and [_Absolute specifiers_][Terminology]. Resolving -[_Relative specifiers_][Terminology] will not work because `data:` is not a -[special scheme][]. For example, attempting to load `./foo` -from `data:text/javascript,import "./foo";` will fail to resolve since there -is no concept of relative resolution for `data:` URLs. An example of a `data:` -URLs being used is: - -```js -import 'data:text/javascript,console.log("hello!");'; -import _ from 'data:application/json,"world!"'; -``` - -## `import.meta` - -* {Object} - -The `import.meta` metaproperty is an `Object` that contains the following -property: - -* `url` {string} The absolute `file:` URL of the module. - -## Differences Between ES Modules and CommonJS - -### 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. - -This behavior matches how `import` behaves in browser environments, assuming a -typically configured server. - -### No `NODE_PATH` - -`NODE_PATH` is not part of resolving `import` specifiers. Please use symlinks -if this behavior is desired. - -### 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.createRequire()`][]. - -Equivalents of `__filename` and `__dirname` can be created inside of each file -via [`import.meta.url`][]. - -```js -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -``` - -### No `require.resolve` - -Former use cases relying on `require.resolve` to determine the resolved path -of a module can be supported via `import.meta.resolve`, which is experimental -and supported via the `--experimental-import-meta-resolve` flag: - -```js -(async () => { - const dependencyAsset = await import.meta.resolve('component-lib/asset.css'); -})(); -``` - -`import.meta.resolve` also accepts a second argument which is the parent module -from which to resolve from: - -```js -(async () => { - // Equivalent to import.meta.resolve('./dep') - await import.meta.resolve('./dep', import.meta.url); -})(); -``` - -This function is asynchronous since the ES module resolver in Node.js is -asynchronous. With the introduction of [Top-Level Await][], these use cases -will be easier as they won't require an async function wrapper. - -### 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` - -`require.cache` is not used by `import`. It has a separate cache. - -### URL-based paths - -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.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. - -## 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.createRequire()`][]. - -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()`][]. - -`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; 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: - - -```js -import packageMain from 'commonjs-package'; // Works - -import { method } from 'commonjs-package'; // Errors -``` - -It is also possible to -[import an ES or CommonJS module for its side effects only][]. - -### `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, 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 -``` - -## Builtin modules - -Builtin 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 -[`module.syncBuiltinESMExports()`][]. - -```js -import EventEmitter from 'events'; -const e = new EventEmitter(); -``` - -```js -import { readFile } from 'fs'; -readFile('./foo.txt', (err, source) => { - if (err) { - console.error(err); - } else { - console.log(source); - } -}); -``` - -```js -import fs, { readFileSync } from 'fs'; -import { syncBuiltinESMExports } from 'module'; - -fs.readFileSync = () => Buffer.from('Hello, ESM'); -syncBuiltinESMExports(); - -fs.readFileSync === readFileSync; -``` - -## Experimental JSON Modules - -Currently importing JSON modules are only supported in the `commonjs` mode -and are loaded using the CJS loader. [WHATWG JSON modules specification][] are -still 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.mjs` with - - -```js -import packageConfig from './package.json'; -``` - -The `--experimental-json-modules` flag is needed for the module -to work. - -```bash -node index.mjs # fails -node --experimental-json-modules index.mjs # works -``` - -## Experimental Wasm Modules - -Importing Web Assembly modules is supported under the -`--experimental-wasm-modules` flag, allowing any `.wasm` files to be -imported as normal modules while also supporting their module imports. - -This integration is in line with the -[ES Module Integration Proposal for Web Assembly][]. - -For example, an `index.mjs` containing: - -```js -import * as M from './module.wasm'; -console.log(M); -``` - -executed under: - -```bash -node --experimental-wasm-modules index.mjs -``` - -would provide the exports interface for the instantiation of `module.wasm`. - -## Experimental Top-Level `await` - -When the `--experimental-top-level-await` flag is provided, `await` may be used -in the top level (outside of async functions) within modules. This implements -the [ECMAScript Top-Level `await` proposal][]. - -Assuming an `a.mjs` with - - -```js -export const five = await Promise.resolve(5); -``` - -And a `b.mjs` with - -```js -import { five } from './a.mjs'; - -console.log(five); // Logs `5` -``` - -```bash -node b.mjs # fails -node --experimental-top-level-await b.mjs # works -``` - -## Experimental Loaders - -**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 `--experimental-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. - -### Hooks - -#### resolve hook - -> Note: The loaders API is being redesigned. This hook may disappear or its -> signature may change. Do not rely on the API described below. - -The `resolve` hook returns the resolved file URL for a given module specifier -and parent URL. The module specifier is the string in an `import` statement or -`import()` expression, and the parent URL is the URL of the module that imported -this one, or `undefined` if this is the main entry point for the application. - -The `conditions` property on the `context` is an array of conditions for -[Conditional Exports][] that apply to this resolution request. They can be used -for looking up conditional mappings elsewhere or to modify the list when calling -the default resolution logic. - -The [current set of Node.js default conditions][Conditional Exports] will always -be in the `context.conditions` list passed to the hook. If the hook wants to -ensure Node.js-compatible resolution logic, all items from this default -condition list **must** be passed through to the `defaultResolve` function. - -```js -/** - * @param {string} specifier - * @param {object} context - * @param {string} context.parentURL - * @param {string[]} context.conditions - * @param {function} defaultResolve - * @returns {object} response - * @returns {string} response.url - */ -export async function resolve(specifier, context, defaultResolve) { - const { parentURL = null } = context; - if (someCondition) { - // For some or all specifiers, do some custom logic for resolving. - // Always return an object of the form {url: } - return { - url: (parentURL) ? - new URL(specifier, parentURL).href : new URL(specifier).href - }; - } - if (anotherCondition) { - // When calling the defaultResolve, the arguments can be modified. In this - // case it's adding another value for matching conditional exports. - return defaultResolve(specifier, { - ...context, - conditions: [...context.conditions, 'another-condition'], - }); - } - // Defer to Node.js for all other specifiers. - return defaultResolve(specifier, context, defaultResolve); -} -``` - -#### getFormat hook - -> Note: The loaders API is being redesigned. This hook may disappear or its -> signature may change. Do not rely on the API described below. - -The `getFormat` hook provides a way to define a custom method of determining how -a URL should be interpreted. The `format` returned also affects what the -acceptable forms of source values are for a module when parsing. This can be one -of the following: - -| `format` | Description | Acceptable Types For `source` Returned by `getSource` or `transformSource` | -| --- | --- | --- | -| `'builtin'` | Load a Node.js builtin module | Not applicable | -| `'commonjs'` | Load a Node.js CommonJS module | Not applicable | -| `'dynamic'` | Use a [dynamic instantiate hook][] | Not applicable | -| `'json'` | Load a JSON file | { [ArrayBuffer][], [string][], [TypedArray][] } | -| `'module'` | Load an ES module | { [ArrayBuffer][], [string][], [TypedArray][] } | -| `'wasm'` | Load a WebAssembly module | { [ArrayBuffer][], [string][], [TypedArray][] } | - -Note: These types all correspond to classes defined in ECMAScript. - -* The specific [ArrayBuffer][] object is a [SharedArrayBuffer][]. -* The specific [string][] object is not the class constructor, but an instance. -* The specific [TypedArray][] object is a [Uint8Array][]. - -Note: If the source value of a text-based format (i.e., `'json'`, `'module'`) is -not a string, it will be converted to a string using [`util.TextDecoder`][]. - -```js -/** - * @param {string} url - * @param {object} context (currently empty) - * @param {function} defaultGetFormat - * @returns {object} response - * @returns {string} response.format - */ -export async function getFormat(url, context, defaultGetFormat) { - if (someCondition) { - // For some or all URLs, do some custom logic for determining format. - // Always return an object of the form {format: }, where the - // format is one of the strings in the table above. - return { - format: 'module' - }; - } - // Defer to Node.js for all other URLs. - return defaultGetFormat(url, context, defaultGetFormat); -} -``` - -#### getSource hook - -> Note: The loaders API is being redesigned. This hook may disappear or its -> signature may change. Do not rely on the API described below. - -The `getSource` hook provides a way to define a custom method for retrieving -the source code of an ES module specifier. This would allow a loader to -potentially avoid reading files from disk. - -```js -/** - * @param {string} url - * @param {object} context - * @param {string} context.format - * @param {function} defaultGetSource - * @returns {object} response - * @returns {string|buffer} response.source - */ -export async function getSource(url, context, defaultGetSource) { - const { format } = context; - if (someCondition) { - // For some or all URLs, do some custom logic for retrieving the source. - // Always return an object of the form {source: }. - return { - source: '...' - }; - } - // Defer to Node.js for all other URLs. - return defaultGetSource(url, context, defaultGetSource); -} -``` - -#### transformSource hook - -> Note: The loaders API is being redesigned. This hook may disappear or its -> signature may change. Do not rely on the API described below. - -The `transformSource` hook provides a way to modify the source code of a loaded -ES module file after the source string has been loaded but before Node.js has -done anything with it. - -If this hook is used to convert unknown-to-Node.js file types into executable -JavaScript, a resolve hook is also necessary in order to register any -unknown-to-Node.js file extensions. See the [transpiler loader example][] below. - -```js -/** - * @param {string|buffer} source - * @param {object} context - * @param {string} context.url - * @param {string} context.format - * @param {function} defaultTransformSource - * @returns {object} response - * @returns {string|buffer} response.source - */ -export async function transformSource(source, - context, - defaultTransformSource) { - const { url, format } = context; - if (someCondition) { - // For some or all URLs, do some custom logic for modifying the source. - // Always return an object of the form {source: }. - return { - source: '...' - }; - } - // Defer to Node.js for all other sources. - return defaultTransformSource( - source, context, defaultTransformSource); -} -``` - -#### getGlobalPreloadCode hook - -> Note: The loaders API is being redesigned. This hook may disappear or its -> signature may change. Do not rely on the API described below. - -Sometimes it can be necessary to run some code inside of the same global scope -that the application will run in. This hook allows to return a string that will -be ran as sloppy-mode script on startup. - -Similar to how CommonJS wrappers work, the code runs in an implicit function -scope. The only argument is a `require`-like function that can be used to load -builtins like "fs": `getBuiltin(request: string)`. - -If the code needs more advanced `require` features, it will have to construct -its own `require` using `module.createRequire()`. - -```js -/** - * @returns {string} Code to run before application startup - */ -export function getGlobalPreloadCode() { - return `\ -globalThis.someInjectedProperty = 42; -console.log('I just set some globals!'); - -const { createRequire } = getBuiltin('module'); - -const require = createRequire(process.cwd() + '/'); -// [...] -`; -} -``` - -#### dynamicInstantiate hook - -> Note: The loaders API is being redesigned. This hook may disappear or its -> signature may change. Do not rely on the API described below. - -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 [`getFormat` hook][]. - -```js -/** - * @param {string} url - * @returns {object} response - * @returns {array} response.exports - * @returns {function} response.execute - */ -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. - -### Examples - -The various loader hooks can be used together to accomplish wide-ranging -customizations of Node.js’ code loading and evaluation behaviors. - -#### HTTPS loader - -In current Node.js, specifiers starting with `https://` are unsupported. The -loader below registers hooks to enable rudimentary support for such specifiers. -While this may seem like a significant improvement to Node.js core -functionality, there are substantial downsides to actually using this loader: -performance is much slower than loading files from disk, there is no caching, -and there is no security. - -```js -// https-loader.mjs -import { get } from 'https'; - -export function resolve(specifier, context, defaultResolve) { - const { parentURL = null } = context; - - // Normally Node.js would error on specifiers starting with 'https://', so - // this hook intercepts them and converts them into absolute URLs to be - // passed along to the later hooks below. - if (specifier.startsWith('https://')) { - return { - url: specifier - }; - } else if (parentURL && parentURL.startsWith('https://')) { - return { - url: new URL(specifier, parentURL).href - }; - } - - // Let Node.js handle all other specifiers. - return defaultResolve(specifier, context, defaultResolve); -} - -export function getFormat(url, context, defaultGetFormat) { - // This loader assumes all network-provided JavaScript is ES module code. - if (url.startsWith('https://')) { - return { - format: 'module' - }; - } - - // Let Node.js handle all other URLs. - return defaultGetFormat(url, context, defaultGetFormat); -} - -export function getSource(url, context, defaultGetSource) { - // For JavaScript to be loaded over the network, we need to fetch and - // return it. - if (url.startsWith('https://')) { - return new Promise((resolve, reject) => { - get(url, (res) => { - let data = ''; - res.on('data', (chunk) => data += chunk); - res.on('end', () => resolve({ source: data })); - }).on('error', (err) => reject(err)); - }); - } - - // Let Node.js handle all other URLs. - return defaultGetSource(url, context, defaultGetSource); -} -``` - -```js -// main.mjs -import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js'; - -console.log(VERSION); -``` - -With this loader, running: - -```console -node --experimental-loader ./https-loader.mjs ./main.js -``` - -Will print the current version of CoffeeScript per the module at the URL in -`main.mjs`. - -#### Transpiler loader - -Sources that are in formats Node.js doesn’t understand can be converted into -JavaScript using the [`transformSource` hook][]. Before that hook gets called, -however, other hooks need to tell Node.js not to throw an error on unknown file -types; and to tell Node.js how to load this new file type. - -This is less performant than transpiling source files before running -Node.js; a transpiler loader should only be used for development and testing -purposes. - -```js -// coffeescript-loader.mjs -import { URL, pathToFileURL } from 'url'; -import CoffeeScript from 'coffeescript'; - -const baseURL = pathToFileURL(`${process.cwd()}/`).href; - -// CoffeeScript files end in .coffee, .litcoffee or .coffee.md. -const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/; - -export function resolve(specifier, context, defaultResolve) { - const { parentURL = baseURL } = context; - - // Node.js normally errors on unknown file extensions, so return a URL for - // specifiers ending in the CoffeeScript file extensions. - if (extensionsRegex.test(specifier)) { - return { - url: new URL(specifier, parentURL).href - }; - } - - // Let Node.js handle all other specifiers. - return defaultResolve(specifier, context, defaultResolve); -} - -export function getFormat(url, context, defaultGetFormat) { - // Now that we patched resolve to let CoffeeScript URLs through, we need to - // tell Node.js what format such URLs should be interpreted as. For the - // purposes of this loader, all CoffeeScript URLs are ES modules. - if (extensionsRegex.test(url)) { - return { - format: 'module' - }; - } - - // Let Node.js handle all other URLs. - return defaultGetFormat(url, context, defaultGetFormat); -} - -export function transformSource(source, context, defaultTransformSource) { - const { url, format } = context; - - if (extensionsRegex.test(url)) { - return { - source: CoffeeScript.compile(source, { bare: true }) - }; - } - - // Let Node.js handle all other sources. - return defaultTransformSource(source, context, defaultTransformSource); -} -``` - -```coffee -# main.coffee -import { scream } from './scream.coffee' -console.log scream 'hello, world' - -import { version } from 'process' -console.log "Brought to you by Node.js version #{version}" -``` - -```coffee -# scream.coffee -export scream = (str) -> str.toUpperCase() -``` - -With this loader, running: - -```console -node --experimental-loader ./coffeescript-loader.mjs main.coffee -``` - -Will cause `main.coffee` to be turned into JavaScript after its source code is -loaded from disk but before Node.js executes it; and so on for any `.coffee`, -`.litcoffee` or `.coffee.md` files referenced via `import` statements of any -loaded file. - ## Resolution Algorithm ### Features diff --git a/doc/api/modules.md b/doc/api/modules.md index 3b5b376a4e30bc..a6458b4afb626c 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -1030,6 +1030,768 @@ variable. Since the module lookups using `node_modules` folders are all relative, and based on the real path of the files making the calls to `require()`, the packages themselves can be anywhere. +## ECMAScript modules + + + +> Stability: 1 - Experimental + +### `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:` and `data:` 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:///`. + +##### `data:` Imports + + + +[`data:` URLs][] are supported for importing with the following MIME types: + +* `text/javascript` for ES Modules +* `application/json` for JSON +* `application/wasm` for WASM. + +`data:` URLs only resolve [_Bare specifiers_][Terminology] for builtin modules +and [_Absolute specifiers_][Terminology]. Resolving +[_Relative specifiers_][Terminology] will not work because `data:` is not a +[special scheme][]. For example, attempting to load `./foo` +from `data:text/javascript,import "./foo";` will fail to resolve since there +is no concept of relative resolution for `data:` URLs. An example of a `data:` +URLs being used is: + +```js +import 'data:text/javascript,console.log("hello!");'; +import _ from 'data:application/json,"world!"'; +``` + +### `import.meta` + +* {Object} + +The `import.meta` metaproperty is an `Object` that contains the following +property: + +* `url` {string} The absolute `file:` URL of the module. + +### Differences Between ES Modules and CommonJS + +#### 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. + +This behavior matches how `import` behaves in browser environments, assuming a +typically configured server. + +#### No `NODE_PATH` + +`NODE_PATH` is not part of resolving `import` specifiers. Please use symlinks +if this behavior is desired. + +#### 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.createRequire()`][]. + +Equivalents of `__filename` and `__dirname` can be created inside of each file +via [`import.meta.url`][]. + +```js +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +``` + +#### No `require.resolve` + +Former use cases relying on `require.resolve` to determine the resolved path +of a module can be supported via `import.meta.resolve`, which is experimental +and supported via the `--experimental-import-meta-resolve` flag: + +```js +(async () => { + const dependencyAsset = await import.meta.resolve('component-lib/asset.css'); +})(); +``` + +`import.meta.resolve` also accepts a second argument which is the parent module +from which to resolve from: + +```js +(async () => { + // Equivalent to import.meta.resolve('./dep') + await import.meta.resolve('./dep', import.meta.url); +})(); +``` + +This function is asynchronous since the ES module resolver in Node.js is +asynchronous. With the introduction of [Top-Level Await][], these use cases +will be easier as they won't require an async function wrapper. + +#### 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` + +`require.cache` is not used by `import`. It has a separate cache. + +#### URL-based paths + +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.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. + +### 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.createRequire()`][]. + +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()`][]. + +`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; 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: + + +```js +import packageMain from 'commonjs-package'; // Works + +import { method } from 'commonjs-package'; // Errors +``` + +It is also possible to +[import an ES or CommonJS module for its side effects only][]. + +#### `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, 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 +``` + +### Builtin modules + +Builtin 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 +[`module.syncBuiltinESMExports()`][]. + +```js +import EventEmitter from 'events'; +const e = new EventEmitter(); +``` + +```js +import { readFile } from 'fs'; +readFile('./foo.txt', (err, source) => { + if (err) { + console.error(err); + } else { + console.log(source); + } +}); +``` + +```js +import fs, { readFileSync } from 'fs'; +import { syncBuiltinESMExports } from 'module'; + +fs.readFileSync = () => Buffer.from('Hello, ESM'); +syncBuiltinESMExports(); + +fs.readFileSync === readFileSync; +``` + +### Experimental JSON Modules + +Currently importing JSON modules are only supported in the `commonjs` mode +and are loaded using the CJS loader. [WHATWG JSON modules specification][] are +still 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.mjs` with + +```js +import packageConfig from './package.json'; +``` + +The `--experimental-json-modules` flag is needed for the module +to work. + +```bash +node index.mjs # fails +node --experimental-json-modules index.mjs # works +``` + +### Experimental Wasm Modules + +Importing Web Assembly modules is supported under the +`--experimental-wasm-modules` flag, allowing any `.wasm` files to be +imported as normal modules while also supporting their module imports. + +This integration is in line with the +[ES Module Integration Proposal for Web Assembly][]. + +For example, an `index.mjs` containing: + +```js +import * as M from './module.wasm'; +console.log(M); +``` + +executed under: + +```bash +node --experimental-wasm-modules index.mjs +``` + +would provide the exports interface for the instantiation of `module.wasm`. + +### Experimental Top-Level `await` + +When the `--experimental-top-level-await` flag is provided, `await` may be used +in the top level (outside of async functions) within modules. This implements +the [ECMAScript Top-Level `await` proposal][]. + +Assuming an `a.mjs` with + + +```js +export const five = await Promise.resolve(5); +``` + +And a `b.mjs` with + +```js +import { five } from './a.mjs'; + +console.log(five); // Logs `5` +``` + +```bash +node b.mjs # fails +node --experimental-top-level-await b.mjs # works +``` + +### Experimental Loaders + +**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 `--experimental-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. + +#### Hooks + +##### resolve hook + +> Note: The loaders API is being redesigned. This hook may disappear or its +> signature may change. Do not rely on the API described below. + +The `resolve` hook returns the resolved file URL for a given module specifier +and parent URL. The module specifier is the string in an `import` statement or +`import()` expression, and the parent URL is the URL of the module that imported +this one, or `undefined` if this is the main entry point for the application. + +The `conditions` property on the `context` is an array of conditions for +[Conditional Exports][] that apply to this resolution request. They can be used +for looking up conditional mappings elsewhere or to modify the list when calling +the default resolution logic. + +The [current set of Node.js default conditions][Conditional Exports] will always +be in the `context.conditions` list passed to the hook. If the hook wants to +ensure Node.js-compatible resolution logic, all items from this default +condition list **must** be passed through to the `defaultResolve` function. + +```js +/** + * @param {string} specifier + * @param {object} context + * @param {string} context.parentURL + * @param {string[]} context.conditions + * @param {function} defaultResolve + * @returns {object} response + * @returns {string} response.url + */ +export async function resolve(specifier, context, defaultResolve) { + const { parentURL = null } = context; + if (someCondition) { + // For some or all specifiers, do some custom logic for resolving. + // Always return an object of the form {url: } + return { + url: (parentURL) ? + new URL(specifier, parentURL).href : new URL(specifier).href + }; + } + if (anotherCondition) { + // When calling the defaultResolve, the arguments can be modified. In this + // case it's adding another value for matching conditional exports. + return defaultResolve(specifier, { + ...context, + conditions: [...context.conditions, 'another-condition'], + }); + } + // Defer to Node.js for all other specifiers. + return defaultResolve(specifier, context, defaultResolve); +} +``` + +##### getFormat hook + +> Note: The loaders API is being redesigned. This hook may disappear or its +> signature may change. Do not rely on the API described below. + +The `getFormat` hook provides a way to define a custom method of determining how +a URL should be interpreted. The `format` returned also affects what the +acceptable forms of source values are for a module when parsing. This can be one +of the following: + +| `format` | Description | Acceptable Types For `source` Returned by `getSource` or `transformSource` | +| --- | --- | --- | +| `'builtin'` | Load a Node.js builtin module | Not applicable | +| `'commonjs'` | Load a Node.js CommonJS module | Not applicable | +| `'dynamic'` | Use a [dynamic instantiate hook][] | Not applicable | +| `'json'` | Load a JSON file | { [ArrayBuffer][], [string][], [TypedArray][] } | +| `'module'` | Load an ES module | { [ArrayBuffer][], [string][], [TypedArray][] } | +| `'wasm'` | Load a WebAssembly module | { [ArrayBuffer][], [string][], [TypedArray][] } | + +Note: These types all correspond to classes defined in ECMAScript. + +* The specific [ArrayBuffer][] object is a [SharedArrayBuffer][]. +* The specific [string][] object is not the class constructor, but an instance. +* The specific [TypedArray][] object is a [Uint8Array][]. + +Note: If the source value of a text-based format (i.e., `'json'`, `'module'`) is +not a string, it will be converted to a string using [`util.TextDecoder`][]. + +```js +/** + * @param {string} url + * @param {object} context (currently empty) + * @param {function} defaultGetFormat + * @returns {object} response + * @returns {string} response.format + */ +export async function getFormat(url, context, defaultGetFormat) { + if (someCondition) { + // For some or all URLs, do some custom logic for determining format. + // Always return an object of the form {format: }, where the + // format is one of the strings in the table above. + return { + format: 'module' + }; + } + // Defer to Node.js for all other URLs. + return defaultGetFormat(url, context, defaultGetFormat); +} +``` + +##### getSource hook + +> Note: The loaders API is being redesigned. This hook may disappear or its +> signature may change. Do not rely on the API described below. + +The `getSource` hook provides a way to define a custom method for retrieving +the source code of an ES module specifier. This would allow a loader to +potentially avoid reading files from disk. + +```js +/** + * @param {string} url + * @param {object} context + * @param {string} context.format + * @param {function} defaultGetSource + * @returns {object} response + * @returns {string|buffer} response.source + */ +export async function getSource(url, context, defaultGetSource) { + const { format } = context; + if (someCondition) { + // For some or all URLs, do some custom logic for retrieving the source. + // Always return an object of the form {source: }. + return { + source: '...' + }; + } + // Defer to Node.js for all other URLs. + return defaultGetSource(url, context, defaultGetSource); +} +``` + +##### transformSource hook + +> Note: The loaders API is being redesigned. This hook may disappear or its +> signature may change. Do not rely on the API described below. + +The `transformSource` hook provides a way to modify the source code of a loaded +ES module file after the source string has been loaded but before Node.js has +done anything with it. + +If this hook is used to convert unknown-to-Node.js file types into executable +JavaScript, a resolve hook is also necessary in order to register any +unknown-to-Node.js file extensions. See the [transpiler loader example][] below. + +```js +/** + * @param {string|buffer} source + * @param {object} context + * @param {string} context.url + * @param {string} context.format + * @param {function} defaultTransformSource + * @returns {object} response + * @returns {string|buffer} response.source + */ +export async function transformSource(source, + context, + defaultTransformSource) { + const { url, format } = context; + if (someCondition) { + // For some or all URLs, do some custom logic for modifying the source. + // Always return an object of the form {source: }. + return { + source: '...' + }; + } + // Defer to Node.js for all other sources. + return defaultTransformSource( + source, context, defaultTransformSource); +} +``` + +##### getGlobalPreloadCode hook + +> Note: The loaders API is being redesigned. This hook may disappear or its +> signature may change. Do not rely on the API described below. + +Sometimes it can be necessary to run some code inside of the same global scope +that the application will run in. This hook allows to return a string that will +be ran as sloppy-mode script on startup. + +Similar to how CommonJS wrappers work, the code runs in an implicit function +scope. The only argument is a `require`-like function that can be used to load +builtins like "fs": `getBuiltin(request: string)`. + +If the code needs more advanced `require` features, it will have to construct +its own `require` using `module.createRequire()`. + +```js +/** + * @returns {string} Code to run before application startup + */ +export function getGlobalPreloadCode() { + return `\ +globalThis.someInjectedProperty = 42; +console.log('I just set some globals!'); + +const { createRequire } = getBuiltin('module'); + +const require = createRequire(process.cwd() + '/'); +// [...] +`; +} +``` + +##### dynamicInstantiate hook + +> Note: The loaders API is being redesigned. This hook may disappear or its +> signature may change. Do not rely on the API described below. + +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 [`getFormat` hook][]. + +```js +/** + * @param {string} url + * @returns {object} response + * @returns {array} response.exports + * @returns {function} response.execute + */ +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. + +#### Examples + +The various loader hooks can be used together to accomplish wide-ranging +customizations of Node.js’ code loading and evaluation behaviors. + +##### HTTPS loader + +In current Node.js, specifiers starting with `https://` are unsupported. The +loader below registers hooks to enable rudimentary support for such specifiers. +While this may seem like a significant improvement to Node.js core +functionality, there are substantial downsides to actually using this loader: +performance is much slower than loading files from disk, there is no caching, +and there is no security. + +```js +// https-loader.mjs +import { get } from 'https'; + +export function resolve(specifier, context, defaultResolve) { + const { parentURL = null } = context; + + // Normally Node.js would error on specifiers starting with 'https://', so + // this hook intercepts them and converts them into absolute URLs to be + // passed along to the later hooks below. + if (specifier.startsWith('https://')) { + return { + url: specifier + }; + } else if (parentURL && parentURL.startsWith('https://')) { + return { + url: new URL(specifier, parentURL).href + }; + } + + // Let Node.js handle all other specifiers. + return defaultResolve(specifier, context, defaultResolve); +} + +export function getFormat(url, context, defaultGetFormat) { + // This loader assumes all network-provided JavaScript is ES module code. + if (url.startsWith('https://')) { + return { + format: 'module' + }; + } + + // Let Node.js handle all other URLs. + return defaultGetFormat(url, context, defaultGetFormat); +} + +export function getSource(url, context, defaultGetSource) { + // For JavaScript to be loaded over the network, we need to fetch and + // return it. + if (url.startsWith('https://')) { + return new Promise((resolve, reject) => { + get(url, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => resolve({ source: data })); + }).on('error', (err) => reject(err)); + }); + } + + // Let Node.js handle all other URLs. + return defaultGetSource(url, context, defaultGetSource); +} +``` + +```js +// main.mjs +import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js'; + +console.log(VERSION); +``` + +With this loader, running: + +```console +node --experimental-loader ./https-loader.mjs ./main.js +``` + +Will print the current version of CoffeeScript per the module at the URL in +`main.mjs`. + +##### Transpiler loader + +Sources that are in formats Node.js doesn’t understand can be converted into +JavaScript using the [`transformSource` hook][]. Before that hook gets called, +however, other hooks need to tell Node.js not to throw an error on unknown file +types; and to tell Node.js how to load this new file type. + +This is less performant than transpiling source files before running +Node.js; a transpiler loader should only be used for development and testing +purposes. + +```js +// coffeescript-loader.mjs +import { URL, pathToFileURL } from 'url'; +import CoffeeScript from 'coffeescript'; + +const baseURL = pathToFileURL(`${process.cwd()}/`).href; + +// CoffeeScript files end in .coffee, .litcoffee or .coffee.md. +const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/; + +export function resolve(specifier, context, defaultResolve) { + const { parentURL = baseURL } = context; + + // Node.js normally errors on unknown file extensions, so return a URL for + // specifiers ending in the CoffeeScript file extensions. + if (extensionsRegex.test(specifier)) { + return { + url: new URL(specifier, parentURL).href + }; + } + + // Let Node.js handle all other specifiers. + return defaultResolve(specifier, context, defaultResolve); +} + +export function getFormat(url, context, defaultGetFormat) { + // Now that we patched resolve to let CoffeeScript URLs through, we need to + // tell Node.js what format such URLs should be interpreted as. For the + // purposes of this loader, all CoffeeScript URLs are ES modules. + if (extensionsRegex.test(url)) { + return { + format: 'module' + }; + } + + // Let Node.js handle all other URLs. + return defaultGetFormat(url, context, defaultGetFormat); +} + +export function transformSource(source, context, defaultTransformSource) { + const { url, format } = context; + + if (extensionsRegex.test(url)) { + return { + source: CoffeeScript.compile(source, { bare: true }) + }; + } + + // Let Node.js handle all other sources. + return defaultTransformSource(source, context, defaultTransformSource); +} +``` + +```coffee +# main.coffee +import { scream } from './scream.coffee' +console.log scream 'hello, world' + +import { version } from 'process' +console.log "Brought to you by Node.js version #{version}" +``` + +```coffee +# scream.coffee +export scream = (str) -> str.toUpperCase() +``` + +With this loader, running: + +```console +node --experimental-loader ./coffeescript-loader.mjs main.coffee +``` + +Will cause `main.coffee` to be turned into JavaScript after its source code is +loaded from disk but before Node.js executes it; and so on for any `.coffee`, +`.litcoffee` or `.coffee.md` files referenced via `import` statements of any +loaded file. + ## Utility Methods -```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: - -* `"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. -* `"require"` - matched when the package is loaded via `require()`. - As `require()` only supports CommonJS, the referenced file must be CommonJS. -* `"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._ - -Condition matching is applied in object order from first to last within the -`"exports"` object. _The general rule is that conditions should be used -from most specific to least specific in object order._ - -Other conditions such as `"browser"`, `"electron"`, `"deno"`, `"react-native"`, -etc. are ignored by Node.js but may be used by other runtimes or tools. -Further restrictions, definitions or guidance on condition names may be -provided in the future. - -Using the `"import"` and `"require"` conditions can lead to some hazards, -which are explained further 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": { - "browser": "./feature-browser.js", - "default": "./feature.js" - } - } -} -``` - -Defines a package where `require('pkg/feature')` and `import 'pkg/feature'` -could provide different implementations between the browser and Node.js, -given third-party tool support for a `"browser"` condition. - -#### 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": { - "browser": "./feature-browser.mjs", - "node": { - "import": "./feature-node.mjs", - "require": "./feature-node.cjs" - } - } -} -``` - -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. - -#### 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 package above, 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 diff --git a/doc/api/modules.md b/doc/api/modules.md index a6458b4afb626c..e48042767ffb37 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -1792,6 +1792,339 @@ loaded from disk but before Node.js executes it; and so on for any `.coffee`, `.litcoffee` or `.coffee.md` files referenced via `import` statements of any loaded file. +## 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: + +```json +{ + "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: + +```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 +``` + +Entire folders can also be mapped with package exports: + +```json +// ./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: + +```json +{ + "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. + +```json +{ + "exports": { + ".": "./main.js" + } +} +``` + +can be written: + +```json +{ + "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: + +```json +// package.json +{ + "main": "./main-require.cjs", + "exports": { + "import": "./main-module.js", + "require": "./main-require.cjs" + }, + "type": "module" +} +``` + +Node.js supports the following conditions: + +* `"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. +* `"require"` - matched when the package is loaded via `require()`. + As `require()` only supports CommonJS, the referenced file must be CommonJS. +* `"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._ + +Condition matching is applied in object order from first to last within the +`"exports"` object. _The general rule is that conditions should be used +from most specific to least specific in object order._ + +Other conditions such as `"browser"`, `"electron"`, `"deno"`, `"react-native"`, +etc. are ignored by Node.js but may be used by other runtimes or tools. +Further restrictions, definitions or guidance on condition names may be +provided in the future. + +Using the `"import"` and `"require"` conditions can lead to some hazards, +which are explained further 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": { + "browser": "./feature-browser.js", + "default": "./feature.js" + } + } +} +``` + +Defines a package where `require('pkg/feature')` and `import 'pkg/feature'` +could provide different implementations between the browser and Node.js, +given third-party tool support for a `"browser"` condition. + +#### 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: + +```json +{ + "main": "./main.js", + "exports": { + "browser": "./feature-browser.mjs", + "node": { + "import": "./feature-node.mjs", + "require": "./feature-node.cjs" + } + } +} +``` + +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. + +#### 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 package above, 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. +``` + ## Utility Methods -```js -// ./node_modules/pkg/package.json -{ - "type": "module", - "main": "./index.cjs", - "exports": { - "import": "./wrapper.mjs", - "require": "./index.cjs" - } -} -``` - -```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" - } -} -``` - ## Resolution Algorithm ### Features diff --git a/doc/api/modules.md b/doc/api/modules.md index e48042767ffb37..70cbf875e1ae37 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -2125,6 +2125,259 @@ and in a CommonJS one. For example, this code will also work: 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 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`. + +```json +// ./node_modules/pkg/package.json +{ + "type": "module", + "main": "./index.cjs", + "exports": { + "import": "./wrapper.mjs", + "require": "./index.cjs" + } +} +``` + +```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 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: + +```json +// ./node_modules/pkg/package.json +{ + "type": "module", + "main": "./index.cjs", + "exports": { + ".": "./index.cjs", + "./module": "./index.mjs" + } +} +``` + ## Utility Methods + +```json +{ + "name": "package-name" +} +``` + +The `"name"` field defines your package’s name. Node.js doesn't apply any +restriction on the name field, although the field is ignored if it is not a +string or an empty string. + +The `"name"` field can be used in addition to the [`"exports"`][] field to +[self-reference a package using its name][]. + +#### `"exports"` + + +```json +{ + "exports": { + ".": "./index.js" + } +} +``` + +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"`. If package entry +points are defined in both [`"main"`][] and `"exports"`, the latter takes +precedence in versions of Node.js that support `"exports"` when referencing the +package by its name. + +All paths defined in the `"exports"` field must be relative file URLs starting +with `./`. + +Refer to the [Package Entry Points][] section for more information. + +#### `"main"` + + +```json +{ + "main": "./main.js" +} +``` + +The `"main"` field defines the script that is used when the +current directory is required by another script. Its value is interpreted as a +path. + +```js +require('./path/to/directory'); // This resolves to ./path/to/directory/main.js. +``` + +This field is ignored when referencing the package by its name and the +[`"exports"`][] field is provided. 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 directory. + +#### `"type"` + + +The `"type"` field defines how `.js` files should be treated +within a particular `package.json` file’s package scope. + +Refer to the [`package.json` `"type"` field][] section for more information. + ### Package Entry Points In a package’s `package.json` file, two fields can define entry points for a From a0d497b8b6de8cefe29cc8313501c6033fb376ee Mon Sep 17 00:00:00 2001 From: Antoine du HAMEL Date: Fri, 22 May 2020 21:33:56 +0200 Subject: [PATCH 09/11] Fix links --- doc/api/cli.md | 4 ++-- doc/api/errors.md | 31 +++++++++++++++-------------- doc/api/esm.md | 35 --------------------------------- doc/api/index.md | 1 - doc/api/modules.md | 42 +++++++++++++++++++++++++++++++++++++++- doc/api/vm.md | 2 +- tools/doc/type-parser.js | 2 +- 7 files changed, 62 insertions(+), 55 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index 2b42e9a5dbd689..8759e866e57331 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1517,11 +1517,11 @@ $ node --max-old-space-size=1536 index.js [Subresource Integrity]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity [V8 JavaScript code coverage]: https://v8project.blogspot.com/2017/12/javascript-code-coverage.html [context-aware]: addons.html#addons_context_aware_addons -[customizing ESM specifier resolution]: esm.html#esm_customizing_esm_specifier_resolution_algorithm +[customizing ESM specifier resolution]: modules.html#modules_customizing_esm_specifier_resolution_algorithm [debugger]: debugger.html [debugging security implications]: https://nodejs.org/en/docs/guides/debugging-getting-started/#security-implications [emit_warning]: process.html#process_process_emitwarning_warning_type_code_ctor -[experimental ECMAScript Module loader]: esm.html#esm_experimental_loaders +[experimental ECMAScript Module loader]: modules.html#modules_experimental_loaders [jitless]: https://v8.dev/blog/jitless [libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html [remote code execution]: https://www.owasp.org/index.php/Code_Injection diff --git a/doc/api/errors.md b/doc/api/errors.md index 1cf926c84be4a2..4afa963bf2665d 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1367,13 +1367,13 @@ An invalid or unknown file encoding was passed. ### `ERR_INVALID_PACKAGE_CONFIG` -An invalid `package.json` file was found which failed parsing. +An invalid [`package.json`][] file was found which failed parsing. ### `ERR_INVALID_PACKAGE_TARGET` -The `package.json` [exports][] field contains an invalid target mapping value -for the attempted module resolution. +The [`package.json`][] [`"exports"`][] field contains an invalid target mapping +value for the attempted module resolution. ### `ERR_INVALID_PERFORMANCE_MARK` @@ -1684,9 +1684,10 @@ A given value is out of the accepted range. ### `ERR_PACKAGE_PATH_NOT_EXPORTED` -The `package.json` [exports][] field does not export the requested subpath. -Because exports are encapsulated, private internal modules that are not exported -cannot be imported through the package resolution, unless using an absolute URL. +The [`package.json`][] [`"exports"`][] field does not export the requested +subpath. Because exports are encapsulated, private internal modules that are not +exported cannot be imported through the package resolution, unless using an +absolute URL. ### `ERR_PROTO_ACCESS` @@ -2050,7 +2051,7 @@ signal (such as [`subprocess.kill()`][]). `import` a directory URL is unsupported. Instead, you can [self-reference a package using its name][] and [define a custom subpath][] in -the `"exports"` field of the `package.json` file. +the [`"exports"`][] field of the [`package.json`][] file. ```js @@ -2471,11 +2472,11 @@ releases. > Stability: 1 - Experimental The `--entry-type=commonjs` flag was used to attempt to execute an `.mjs` file -or a `.js` file where the nearest parent `package.json` contains +or a `.js` file where the nearest parent [`package.json`][] contains `"type": "module"`; or the `--entry-type=module` flag was used to attempt to execute a `.cjs` file or -a `.js` file where the nearest parent `package.json` either lacks a `"type"` -field or contains `"type": "commonjs"`. +a `.js` file where the nearest parent [`package.json`][] either lacks a +[`"type"`][] field or contains `"type": "commonjs"`. #### `ERR_FS_WATCHER_ALREADY_STARTED` @@ -2601,7 +2602,7 @@ such as `process.stdout.on('data')`. [`subprocess.send()`]: child_process.html#child_process_subprocess_send_message_sendhandle_options_callback [`util.getSystemErrorName(error.errno)`]: util.html#util_util_getsystemerrorname_err [`zlib`]: zlib.html -[ES Module]: esm.html +[ES Module]: modules.html [ICU]: intl.html#intl_internationalization_support [Node.js Error Codes]: #nodejs-error-codes [V8's stack trace API]: /~https://github.com/v8/v8/wiki/Stack-Trace-API @@ -2610,7 +2611,9 @@ such as `process.stdout.on('data')`. [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 +[`"exports"`]: modules.html#modules_exports_1 +[`"type"`]: modules.html#modules_type +[`package.json`]: modules.html#modules_package_json_file [file descriptors]: https://en.wikipedia.org/wiki/File_descriptor [policy]: policy.html [stream-based]: stream.html @@ -2618,5 +2621,5 @@ such as `process.stdout.on('data')`. [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 +[self-reference a package using its name]: modules.html#modules_self_referencing_a_package_using_its_name +[define a custom subpath]: modules.html#modules_subpath_exports diff --git a/doc/api/esm.md b/doc/api/esm.md index 8778c2c9c3bfaf..0547ad9b7acf92 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -7,38 +7,3 @@ > Stability: 1 - Experimental -[Babel]: https://babeljs.io/ -[CommonJS]: modules.html -[Conditional Exports]: #esm_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 -[ECMAScript Top-Level `await` proposal]: /~https://github.com/tc39/proposal-top-level-await/ -[ES Module Integration Proposal for Web Assembly]: /~https://github.com/webassembly/esm-integration -[Node.js EP for ES Modules]: /~https://github.com/nodejs/node-eps/blob/master/002-es-modules.md -[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 -[`import.meta.url`]: #esm_import_meta -[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import -[`module.createRequire()`]: modules.html#modules_module_createrequire_filename -[`module.syncBuiltinESMExports()`]: modules.html#modules_module_syncbuiltinesmexports -[`transformSource` hook]: #esm_code_transformsource_code_hook -[ArrayBuffer]: http://www.ecma-international.org/ecma-262/6.0/#sec-arraybuffer-constructor -[SharedArrayBuffer]: https://tc39.es/ecma262/#sec-sharedarraybuffer-constructor -[string]: http://www.ecma-international.org/ecma-262/6.0/#sec-string-constructor -[TypedArray]: http://www.ecma-international.org/ecma-262/6.0/#sec-typedarray-objects -[Uint8Array]: http://www.ecma-international.org/ecma-262/6.0/#sec-uint8array -[`util.TextDecoder`]: util.html#util_class_util_textdecoder -[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 diff --git a/doc/api/index.md b/doc/api/index.md index ec760f5342dc3f..358713ccf70a88 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) diff --git a/doc/api/modules.md b/doc/api/modules.md index 99717ef4b65286..74123d8f5d1c17 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -3058,7 +3058,7 @@ success! [`module` object]: #modules_utility_methods [`module.id`]: #modules_module_id [`path.dirname()`]: path.html#path_path_dirname_path -[ECMAScript Modules]: esm.html +[ECMAScript Modules]: #modules_ecmascript_modules_1 [an error]: errors.html#errors_err_require_esm [exports shortcut]: #modules_exports_shortcut [module resolution]: #modules_resolution_algorithms @@ -3070,3 +3070,43 @@ success! [`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 +[Babel]: https://babeljs.io/ +[CommonJS]: #modules_commonjs_modules_1 +[Conditional Exports]: #modules_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 +[ECMAScript Top-Level `await` proposal]: /~https://github.com/tc39/proposal-top-level-await/ +[ES Module Integration Proposal for Web Assembly]: /~https://github.com/webassembly/esm-integration +[Node.js EP for ES Modules]: /~https://github.com/nodejs/node-eps/blob/master/002-es-modules.md +[Terminology]: #modules_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]: #modules_code_getformat_code_hook +[`import()`]: #modules_import_expressions +[`import.meta.url`]: #modules_import_meta +[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import +[`module.createRequire()`]: modules.html#modules_module_createrequire_filename +[`module.syncBuiltinESMExports()`]: modules.html#modules_module_syncbuiltinesmexports +[`transformSource` hook]: #modules_code_transformsource_code_hook +[ArrayBuffer]: http://www.ecma-international.org/ecma-262/6.0/#sec-arraybuffer-constructor +[SharedArrayBuffer]: https://tc39.es/ecma262/#sec-sharedarraybuffer-constructor +[string]: http://www.ecma-international.org/ecma-262/6.0/#sec-string-constructor +[TypedArray]: http://www.ecma-international.org/ecma-262/6.0/#sec-typedarray-objects +[Uint8Array]: http://www.ecma-international.org/ecma-262/6.0/#sec-uint8array +[`util.TextDecoder`]: util.html#util_class_util_textdecoder +[dynamic instantiate hook]: #modules_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]: #modules_mandatory_file_extensions +[the official standard format]: https://tc39.github.io/ecma262/#sec-modules +[transpiler loader example]: #modules_transpiler_loader +[6.1.7 Array Index]: https://tc39.es/ecma262/#integer-index +[Top-Level Await]: /~https://github.com/tc39/proposal-top-level-await +[`"exports"`]: #modules_exports_1 +[`"main"`]: #modules_main +[self-reference a package using its name]: #modules_self_referencing_a_package_using_its_name +[the dual CommonJS/ES module packages section]: #modules_dual_commonjs_es_module_packages +[Package Entry Points]: #modules_package_entry_points +[`package.json` `"type"` field]: #modules_package_json_type_field diff --git a/doc/api/vm.md b/doc/api/vm.md index 25900c2bb7c49f..c7fb62c0887192 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -1264,7 +1264,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]: modules.html#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/tools/doc/type-parser.js b/tools/doc/type-parser.js index f35abb52f65e47..d9f7e72661391c 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -74,7 +74,7 @@ const customTypesMap = { 'errors.Error': 'errors.html#errors_class_error', - 'import.meta': 'esm.html#esm_import_meta', + 'import.meta': 'modules.html#modules_import_meta', 'EventEmitter': 'events.html#events_class_eventemitter', From eadbb2b44d561c8336366b6386fcd27bdd377aba Mon Sep 17 00:00:00 2001 From: Antoine du HAMEL Date: Fri, 22 May 2020 21:35:23 +0200 Subject: [PATCH 10/11] Add redirect link from esm.md to modules.md --- doc/api/esm.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 0547ad9b7acf92..3431642d1aa9f0 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -1,9 +1,5 @@ # ECMAScript Modules - - - - - -> Stability: 1 - Experimental +This documentation was moved to the [Modules][] page. +[Modules]: modules.html From a3029c731c6dc77e8f360aa78ab4ef621c00affb Mon Sep 17 00:00:00 2001 From: Antoine du HAMEL Date: Fri, 22 May 2020 21:47:27 +0200 Subject: [PATCH 11/11] Remove esm.md to make the tests pass --- doc/api/esm.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 doc/api/esm.md diff --git a/doc/api/esm.md b/doc/api/esm.md deleted file mode 100644 index 3431642d1aa9f0..00000000000000 --- a/doc/api/esm.md +++ /dev/null @@ -1,5 +0,0 @@ -# ECMAScript Modules - -This documentation was moved to the [Modules][] page. - -[Modules]: modules.html