Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: refine conditional exports documentation #32098

Closed
wants to merge 39 commits into from
Closed
Changes from 14 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a3859aa
docs: update conditional exports recommendations
guybedford Mar 4, 2020
509e644
revert default / require guidance
guybedford Mar 4, 2020
177d3dc
Update doc/api/esm.md
MylesBorins Mar 4, 2020
86f417b
Update doc/api/esm.md
guybedford Mar 6, 2020
b41d5b6
Update doc/api/esm.md
guybedford Mar 6, 2020
d030297
Update doc/api/esm.md
guybedford Mar 6, 2020
f2e8b26
Update doc/api/esm.md
guybedford Mar 6, 2020
05ee0db
Update doc/api/esm.md
guybedford Mar 6, 2020
e9ed602
Update doc/api/esm.md
guybedford Mar 6, 2020
c251c50
Update doc/api/esm.md
guybedford Mar 6, 2020
74206e7
reorder conditions, note nesting, update encapsulation note, exports …
guybedford Mar 6, 2020
28e3157
fixup numbering in spec
guybedford Mar 6, 2020
88ed6e2
update package exports section
guybedford Mar 6, 2020
37d439a
remove unused definition
guybedford Mar 6, 2020
96f226c
note on esm main
guybedford Mar 6, 2020
c454cc3
Update doc/api/esm.md
guybedford Mar 6, 2020
cf615ca
rename packages heading
guybedford Mar 6, 2020
4f34a41
fixup line lengths
guybedford Mar 6, 2020
493d2b1
update exports overview
guybedford Mar 6, 2020
b7e03fc
fallbacks as its own section, refine directory mapping notes
guybedford Mar 8, 2020
2f64d57
package entry points -> packages
guybedford Mar 8, 2020
09d76a7
Update doc/api/esm.md
guybedford Mar 11, 2020
b2125e6
Update doc/api/esm.md
guybedford Mar 11, 2020
ca38c5a
Update doc/api/esm.md
guybedford Mar 11, 2020
fc7f258
Update doc/api/esm.md
guybedford Mar 11, 2020
606eec3
Update doc/api/esm.md
guybedford Mar 11, 2020
6a28834
Update doc/api/esm.md
guybedford Mar 11, 2020
598da72
Update doc/api/esm.md
guybedford Mar 11, 2020
59729d4
Update doc/api/esm.md
guybedford Mar 11, 2020
15bdd25
Update doc/api/esm.md
guybedford Mar 11, 2020
a74474c
fixup line length
guybedford Mar 11, 2020
20ebd1a
Update doc/api/esm.md
guybedford Mar 11, 2020
33d7d9c
Update doc/api/esm.md
guybedford Mar 11, 2020
13e1c71
Update doc/api/esm.md
guybedford Mar 11, 2020
766940e
remove exports link
guybedford Mar 11, 2020
814f672
Update doc/api/esm.md
guybedford Mar 11, 2020
756c801
Update errors.md
guybedford Mar 11, 2020
b6e8394
update link
guybedford Mar 11, 2020
748cbad
linting fixes
guybedford Mar 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 102 additions & 134 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,88 +175,75 @@ unspecified.

## Packages

### Package Entry Points
### Package Exports
guybedford marked this conversation as resolved.
Show resolved Hide resolved

There are two fields that can define entry points for a package: `"main"` and
guybedford marked this conversation as resolved.
Show resolved Hide resolved
`"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, part of [Package Exports][], 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"`. [Conditional Exports][] can also be used within `"exports"` to
define different package entry points per environment.

#### `package.json` `"main"`
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"`. [Conditional Exports][] can
also be used within `"exports"` to define different package entry points per
environment.
guybedford marked this conversation as resolved.
Show resolved Hide resolved

The `package.json` `"main"` field defines the entry point for a package,
whether the package is included into CommonJS via `require` or into an ES
module via `import`.
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved
#### Package Exports Main
guybedford marked this conversation as resolved.
Show resolved Hide resolved

To set the main entry point for a package, it is advisable to use both
`"exports"` and the `"main"`:
guybedford marked this conversation as resolved.
Show resolved Hide resolved

<!-- eslint-skip -->
```js
// ./node_modules/es-module-package/package.json
{
"type": "module",
"main": "./src/index.js"
"main": "./main.js",
"exports": "./main.js"
guybedford marked this conversation as resolved.
Show resolved Hide resolved
}
```

```js
// ./my-app.mjs

import { something } from 'es-module-package';
// Loads from ./node_modules/es-module-package/src/index.js
```

An attempt to `require` the above `es-module-package` would attempt to load
`./node_modules/es-module-package/src/index.js` as CommonJS, which would throw
an error as Node.js would not be able to parse the `export` statement in
CommonJS.

As with `import` statements, for ES module usage the value of `"main"` must be
a full path including extension: `"./index.mjs"`, not `"./index"`.

If the `package.json` `"type"` field is omitted, a `.js` file in `"main"` will
be interpreted as CommonJS.
guybedford marked this conversation as resolved.
Show resolved Hide resolved
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`.

The `"main"` field can point to exactly one file, regardless of whether the
package is referenced via `require` (in a CommonJS context) or `import` (in an
ES module context).
This new encapsulation of exports provides new more reliable guarantees
guybedford marked this conversation as resolved.
Show resolved Hide resolved
about package interfaces for tools and when handling semver upgrades for a
package. It is not a strong encapsulation since
`require('/path/to/pkg/subpath.js')` will still work correctly.
guybedford marked this conversation as resolved.
Show resolved Hide resolved

#### Package Exports
#### Package Exports Subpaths
guybedford marked this conversation as resolved.
Show resolved Hide resolved

By default, all subpaths from a package can be imported (`import 'pkg/x.js'`).
Custom subpath aliasing and encapsulation can be provided through the
`"exports"` field.
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:

<!-- eslint-skip -->
```js
// ./node_modules/es-module-package/package.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
```

In addition to defining an alias, subpaths not defined by `"exports"` will
throw when an attempt is made to import them:
While other subpaths will still error:
guybedford marked this conversation as resolved.
Show resolved Hide resolved

```js
import submodule from 'es-module-package/private-module.js';
// Throws ERR_MODULE_NOT_FOUND
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED
```

> Note: this is not a strong encapsulation as any private modules can still be
> loaded by absolute paths.

Folders can also be mapped with package exports:

<!-- eslint-skip -->
Expand All @@ -274,10 +261,6 @@ import feature from 'es-module-package/features/x.js';
// Loads ./node_modules/es-module-package/src/features/x.js
```

If a package has no exports, setting `"exports": false` can be used instead of
`"exports": {}` to indicate the package does not intend for submodules to be
exposed.

Any invalid exports entries will be ignored. This includes exports not
starting with `"./"` or a missing trailing `"/"` for directory exports.

Expand All @@ -296,140 +279,125 @@ in order to be forwards-compatible with possible fallback workflows in future:
Since `"not:valid"` is not a supported target, `"./submodule.js"` is used
instead as the fallback, as if it were the only target.

Defining a `"."` export will define the main entry point for the package,
and will always take precedence over the `"main"` field in the `package.json`.
#### Exports Sugar

If the `"."` export is the only export, the `"exports"` field provides sugar
for this case being the direct `"exports"` field value.

This allows defining a different entry point for Node.js versions that support
ECMAScript modules and versions that don't, for example:
If the `"."` export has a fallback array or string value, then the `"exports"`
field can be set to this value directly.

<!-- eslint-skip -->
```js
{
"main": "./main-legacy.cjs",
"exports": {
".": "./main-modern.cjs"
".": "./main.js"
}
}
```

can be written:

<!-- eslint-skip -->
```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
Node.js and the browser can be written:
`require()` and `import` can be written:

<!-- eslint-skip -->
```js
// ./node_modules/pkg/package.json
// package.json
{
"type": "module",
"main": "./index.js",
"main": "./main-require.cjs",
"exports": {
"./feature": {
"import": "./feature-default.js",
"browser": "./feature-browser.js"
}
}
"import": "./main-module.js",
"require": "./main-require.cjs"
},
"type": "module"
}
```

When resolving the `"."` export, if no matching target is found, the `"main"`
will be used as the final fallback.

The conditions supported in Node.js condition matching:
Node.js supports the following conditions:

* `"default"` - the generic fallback that will always match. Can be a CommonJS
or ES module file.
* `"import"` - matched when the package is loaded via `import` or
`import()`. Can be any module format, this field does not set the type
interpretation.
* `"node"` - matched for any Node.js environment. Can be a CommonJS or ES
module file.
`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()`.
guybedford marked this conversation as resolved.
Show resolved Hide resolved
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.

Using the `"require"` condition it is possible to define a package that will
have a different exported value for CommonJS and ES modules, which can be a
hazard in that it can result in having two separate instances of the same
package in use in an application, which can cause a number of bugs.
`"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. could be defined in other runtimes or tools. Condition names must not start
with `"."` or be numbers. Further restrictions, definitions or guidance on
condition names may be provided in future.

#### Exports Sugar
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.

If the `"."` export is the only export, the `"exports"` field provides sugar
for this case being the direct `"exports"` field value.
Using the `"import"` and `"require"` conditions can lead to some hazards,
which are explained further in
[the dual CommonJS/ES module packages section][].

If the `"."` export has a fallback array or string value, then the `"exports"`
field can be set to this value directly.
Conditional exports can also be extended to exports subpaths, for example:

<!-- eslint-skip -->
```js
{
"main": "./main.js",
"exports": {
".": "./main.js"
".": "./main.js",
"./feature": {
"browser": "./feature-browser.js",
"default": "./feature.js"
}
}
}
```

can be written:
Defines a package where `require('pkg/feature')` and `import 'pkg/feature'`
could provide different implementations between the browser and Node.js,
given third-part tool support for a `"browser"` condition.
guybedford marked this conversation as resolved.
Show resolved Hide resolved

<!-- eslint-skip -->
```js
{
"exports": "./main.js"
}
```
#### Nested conditions

When using [Conditional Exports][], the rule is that all keys in the object
mapping must not start with a `"."` otherwise they would be indistinguishable
from exports subpaths.
In addition to direct mappings, conditional exports also supports nested
conditions with nested condition objects.
guybedford marked this conversation as resolved.
Show resolved Hide resolved

<!-- eslint-skip -->
```js
{
"exports": {
".": {
"import": "./main.js",
"require": "./main.cjs"
}
}
}
```

can be written:
For example, to define a package that only has dual mode entry points for
use in Node.js but not the browser:

<!-- eslint-skip -->
```js
{
"main": "./main.js",
"exports": {
"import": "./main.js",
"require": "./main.cjs"
"browser": "./feature-browser.mjs",
"node": {
"import": "./feature-node.mjs",
"require": "./feature-node.cjs"
}
}
}
```

If writing any exports value that mixes up these two forms, an error will be
thrown:

<!-- eslint-skip -->
```js
{
// Throws on resolution!
"exports": {
"./feature": "./lib/feature.js",
"import": "./main.js",
"require": "./main.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 fully analogously to nested `if` statements in JS.
guybedford marked this conversation as resolved.
Show resolved Hide resolved

### Dual CommonJS/ES Module Packages

Expand Down Expand Up @@ -516,8 +484,8 @@ CommonJS entry point for `require`.
"type": "module",
"main": "./index.cjs",
"exports": {
"require": "./index.cjs",
"import": "./wrapper.mjs"
"import": "./wrapper.mjs",
"require": "./index.cjs"
}
}
```
Expand Down Expand Up @@ -1552,7 +1520,7 @@ The resolver can throw the following errors:

**PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_, _env_)

> 1.If _target_ is a String, then
> 1. If _target_ is a String, then
> 1. If _target_ does not start with _"./"_ or contains any _"node_modules"_
> segments including _"node_modules"_ percent-encoding, throw an
> _Invalid Package Target_ error.
Expand Down Expand Up @@ -1652,7 +1620,6 @@ success!
[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
[Package Exports]: #esm_package_exports
[Terminology]: #esm_terminology
[WHATWG JSON modules specification]: https://html.spec.whatwg.org/#creating-a-json-module-script
[`"exports"` field]: #esm_package_exports
Expand All @@ -1669,6 +1636,7 @@ success!
[dynamic instantiate hook]: #esm_code_dynamicinstantiate_code_hook
[special scheme]: https://url.spec.whatwg.org/#special-scheme
[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