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

doc: update packages documentation for Node.js 12 EOL #43375

Closed
wants to merge 31 commits into from
Closed
Changes from 3 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4759735
doc: update packages documentation for Node.js 12 EOL
guybedford Jun 11, 2022
4fad6db
lint fixes
guybedford Jun 11, 2022
924c758
Apply suggestions from code review
guybedford Jun 11, 2022
894a104
main -> index, rewordings
guybedford Jun 12, 2022
5cb9eda
also update conditional example
guybedford Jun 12, 2022
47c501a
latest suggestions
guybedford Jun 13, 2022
2c9d7c3
remove extension in module export case
guybedford Jun 14, 2022
b65c5e5
update patterns history
guybedford Jun 14, 2022
e102fce
update imports trailers support
guybedford Jun 14, 2022
f4184d6
reorder changes
guybedford Jun 14, 2022
9db527f
use patterns extensions variant in interop example
guybedford Jun 14, 2022
4cbd77f
note major upgrade path
guybedford Jun 14, 2022
d339608
fixup unextensioned feature export
guybedford Jun 14, 2022
c83484f
Apply suggestions from code review
guybedford Jun 14, 2022
2dec621
dedicated section on import maps compat
guybedford Jun 14, 2022
074c1f5
clarification
guybedford Jun 14, 2022
a196e03
more clarifications
guybedford Jun 14, 2022
82ce47c
compatibility -> interopoerability
guybedford Jun 14, 2022
d8a9064
typo, rewording
guybedford Jun 14, 2022
b9ffef7
remove duplicated point
guybedford Jun 14, 2022
5096cc7
rewordings, fixups
guybedford Jun 14, 2022
21263f2
fixup reference ordering
guybedford Jun 14, 2022
469e5e8
subpath extensions guidance section over import maps interop section
guybedford Jun 15, 2022
f2b426a
typo
guybedford Jun 15, 2022
7804776
update import map link
guybedford Jun 15, 2022
ad0861f
lint fixes for Jacob
guybedford Jun 15, 2022
14a9f4e
further clarifications
guybedford Jun 15, 2022
eebd97d
final recommendataion cleanup
guybedford Jun 15, 2022
b0a0df3
correct patterns change
guybedford Jun 15, 2022
03e84ea
extensioned v extensionless -> extensions in subpaths
guybedford Jun 15, 2022
757bb60
casing
guybedford Jun 15, 2022
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
188 changes: 96 additions & 92 deletions doc/api/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,31 +225,32 @@ in your project's `package.json`.
## 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"`][] is 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.
package: [`"main"`][] and [`"exports"`][]. Both fields apply to both ES module
and CommonJS module entry points.

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"`][] provides a modern alternative to [`"main"`][] allowing
multiple entry points to be defined, conditional entry resolution support
between environments, and **preventing any other entry points besides those
defined in [`"exports"`][]**. This encapsulation allows module authors to
clearly define the public interface for their package.

For new packages supporting Node.js 12+ the [`"exports"`][] field is
recommended. For existing packages or packages supporting Node.js version 12.20
or below, the [`"main"`][] field is recommended. If both [`"exports"`][] and
[`"main"`][] are defined, the [`"exports"`][] field takes precedence over
[`"main"`][] in supported versions of Node.js.

[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
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
Existing packages introducing the [`"exports"`][] field will prevent consumers
of the 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.**

Expand All @@ -274,40 +275,31 @@ a project that previous exported `main`, `lib`,
}
```

Alternatively a project could choose to export entire folders:
Alternatively a project could choose to export entire folders using export
patterns:

```json
{
"name": "my-mod",
"exports": {
".": "./lib/index.js",
"./lib": "./lib/index.js",
"./lib/*": "./lib/*.js",
"./lib/*": "./lib/*",
guybedford marked this conversation as resolved.
Show resolved Hide resolved
"./feature": "./feature/index.js",
"./feature/*": "./feature/*.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 exposes 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
is encapsulation lost but module consumers are unable to
`import feature from 'my-mod/feature'` as they 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:
When writing a new package supporting Node.js 12.20 and above (i.e. including
all current Node.js LTS releases), it is recommended to use the [`"exports"`][]
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved
field:

```json
{
"main": "./main.js",
"exports": "./main.js"
}
```
Expand All @@ -323,6 +315,16 @@ 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`.

For packages supporting Node.js < 12.7.0 it is necessary to include the `"main"`
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved
field:

```json
{
"main": "./main.js",
"exports": "./main.js"
}
```

### Subpath exports

<!-- YAML
Expand All @@ -335,18 +337,17 @@ with the main entry point by treating the main entry point as the

```json
{
"main": "./main.js",
"exports": {
".": "./main.js",
"./submodule": "./src/submodule.js"
"./submodule.js": "./src/submodule.js"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"./submodule.js": "./src/submodule.js"
"./submodule": "./src/submodule.js"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i still think these examples shouldn't be changed to add the extension.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t really understand what’s motivating this change either, if anything that adds more bloat to the import maps – I mean it adds 3 bytes; jokes aside, it could give the reader the impression that using an extension is required when defining an export path, and/or confuse needlessly the package consumers on why the is no submodule.js file at the trot of the package – whereas extensionless makes a good job signaling we’re not dealing with actual paths imo.
(Same thing for the change main.js -> index.js, I don’t think it’s necessary)

That being said, I don’t feel strongly about this, so feel free to disregard.

}
}
```

Now only the defined subpath in [`"exports"`][] can be imported by a consumer:

```js
import submodule from 'es-module-package/submodule';
import submodule from 'es-module-package/submodule.js';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import submodule from 'es-module-package/submodule.js';
import submodule from 'es-module-package/submodule';

// Loads ./node_modules/es-module-package/src/submodule.js
```

Expand All @@ -357,6 +358,39 @@ import submodule from 'es-module-package/private-module.js';
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED
```

Even though subpaths provide an aribtrary string mapping to the package
guybedford marked this conversation as resolved.
Show resolved Hide resolved
interface, it is recommended (but not required) to use explicit file extensions
when defining package subpaths so that package consumers write
`import 'pkg/subpath.js'` instead of `import 'pkg/subpath'` as this simplifies
interop with other ecosystem tooling patterns such as when using import maps.
guybedford marked this conversation as resolved.
Show resolved Hide resolved
This also mirrors the requirement of using [the full specifier path][] in
relative and absolute import specifiers.

### Exports sugar

<!-- YAML
added: v12.11.0
-->

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

```json
{
"exports": {
".": "./main.js"
}
}
```

can be written:

```json
{
"exports": "./main.js"
}
```

### Subpath imports

<!-- YAML
Expand All @@ -370,7 +404,7 @@ package import maps that only apply to import specifiers from within the package
itself.

Entries in the imports field must always start with `#` to ensure they are
disambiguated from package specifiers.
disambiguated from external package specifiers.

For example, the imports field can be used to gain the benefits of conditional
exports for internal modules:
Expand Down Expand Up @@ -405,7 +439,6 @@ analogous to the exports field.
<!-- YAML
added:
- v14.13.0
- v12.20.0
guybedford marked this conversation as resolved.
Show resolved Hide resolved
-->

For packages with a small number of exports or imports, we recommend
Expand All @@ -419,10 +452,10 @@ For these use cases, subpath export patterns can be used instead:
// ./node_modules/es-module-package/package.json
{
"exports": {
"./features/*": "./src/features/*.js"
"./features/*.js": "./src/features/*.js"
},
"imports": {
"#internal/*": "./src/internal/*.js"
"#internal/*.js": "./src/internal/*.js"
}
}
```
Expand All @@ -434,16 +467,21 @@ All instances of `*` on the right hand side will then be replaced with this
value, including if it contains any `/` separators.

```js
import featureX from 'es-module-package/features/x';
import featureX from 'es-module-package/features/x.js';
// Loads ./node_modules/es-module-package/src/features/x.js

import featureY from 'es-module-package/features/y/y';
import featureY from 'es-module-package/features/y/y.js';
// Loads ./node_modules/es-module-package/src/features/y/y.js

import internalZ from '#internal/z';
import internalZ from '#internal/z.js';
// Loads ./node_modules/es-module-package/src/internal/z.js
```

Including the `"*.js"` on both sides of the mapping allows restricting which
file extensions can be resolved in the case of other files like source maps
existing, while also retaining file extensions for the consumed subpath as is
recommended.
guybedford marked this conversation as resolved.
Show resolved Hide resolved

This is a direct static replacement without any special handling for file
extensions. In the previous example, `pkg/features/x.json` would be resolved to
`./src/features/x.json.js` in the mapping.
Expand All @@ -460,48 +498,20 @@ To exclude private subfolders from patterns, `null` targets can be used:
// ./node_modules/es-module-package/package.json
{
"exports": {
"./features/*": "./src/features/*.js",
"./features/*.js": "./src/features/*.js",
"./features/private-internal/*": null
}
}
```

```js
import featureInternal from 'es-module-package/features/private-internal/m';
import featureInternal from 'es-module-package/features/private-internal/m.js';
// Throws: ERR_PACKAGE_PATH_NOT_EXPORTED

import featureX from 'es-module-package/features/x';
import featureX from 'es-module-package/features/x.js';
// Loads ./node_modules/es-module-package/src/features/x.js
```

### Exports sugar

<!-- YAML
added: v12.11.0
-->

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

<!-- YAML
Expand All @@ -525,7 +535,6 @@ For example, a package that wants to provide different ES module exports for
```json
// package.json
{
"main": "./main-require.cjs",
"exports": {
"import": "./main-module.js",
"require": "./main-require.cjs"
Expand Down Expand Up @@ -576,20 +585,19 @@ Conditional exports can also be extended to exports subpaths, for example:

```json
{
"main": "./main.js",
"exports": {
".": "./main.js",
"./feature": {
"./feature.js": {
"node": "./feature-node.js",
"default": "./feature.js"
}
}
}
```

Defines a package where `require('pkg/feature')` and `import 'pkg/feature'`
could provide different implementations between Node.js and other JS
environments.
Defines a package where `require('pkg/feature.js')` and
`import 'pkg/feature.js'` could provide different implementations between
Node.js and other JS environments.

When using environment branches, always include a `"default"` condition where
possible. Providing a `"default"` condition ensures that any unknown JS
Expand Down Expand Up @@ -710,7 +718,7 @@ For example, assuming the `package.json` is:
"name": "a-package",
"exports": {
".": "./main.mjs",
"./foo": "./foo.js"
"./foo.js": "./foo.js"
}
}
```
Expand Down Expand Up @@ -741,7 +749,7 @@ and in a CommonJS one. For example, this code will also work:

```cjs
// ./a-module.js
const { something } = require('a-package/foo'); // Loads from ./foo.js.
const { something } = require('a-package/foo.js'); // Loads from ./foo.js.
```

Finally, self-referencing also works with scoped packages. For example, this
Expand Down Expand Up @@ -854,7 +862,6 @@ CommonJS entry point for `require`.
// ./node_modules/pkg/package.json
{
"type": "module",
"main": "./index.cjs",
"exports": {
"import": "./wrapper.mjs",
"require": "./index.cjs"
Expand Down Expand Up @@ -911,7 +918,7 @@ This approach is appropriate for any of the following use cases:
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
be to add an export, e.g. `"./module.mjs"`, 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
Expand All @@ -922,10 +929,9 @@ stateless):
// ./node_modules/pkg/package.json
{
"type": "module",
"main": "./index.cjs",
"exports": {
".": "./index.cjs",
"./module": "./wrapper.mjs"
"./module.mjs": "./wrapper.mjs"
}
}
```
Expand All @@ -939,7 +945,6 @@ points directly:
// ./node_modules/pkg/package.json
{
"type": "module",
"main": "./index.cjs",
"exports": {
"import": "./index.mjs",
"require": "./index.cjs"
Expand Down Expand Up @@ -1019,16 +1024,15 @@ 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:
`"./module.mjs"`, 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"
"./module.mjs": "./index.mjs"
guybedford marked this conversation as resolved.
Show resolved Hide resolved
}
}
```
Expand Down