Skip to content
Open
Changes from 1 commit
Commits
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
57 changes: 56 additions & 1 deletion doc/api/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,62 @@ $ node other.js

## Dual CommonJS/ES module packages

See [the package examples repository][] for details.
Prior to the introduction of [`"exports"`][], authors of packages that support
both CommonJS and ES modules typically used the `"import"` and `"require"`
conditions. However, using these conditions can lead to the _dual package
hazard_, where the same package may be loaded twice (once as CommonJS and once
as an ES module), causing issues with package state and object identity.

For example, given a package with the following `package.json`:

```json
{
"name": "my-package",
"exports": {
"import": "./index.mjs",
"require": "./index.cjs"
}
}
```

If one dependency `require()`s `my-package` while another `import`s it, two
separate copies of the package are loaded. Any state or objects exported by the
package will not be shared between the two copies.

### Approach 1: Use `node` and `default` conditions
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i'd say approach 0 should be "just use default" - if you use CJS, non-node environments have to bundle. If you use ESM, then CJS consumers can only use it in a node version with require(ESM) support (ESM consumers can always use it, ofc).


The recommended approach to avoid the dual package hazard while still providing
both CommonJS and ES module entry points is to use the `"node"` and `"default"`
conditions instead of `"require"` and `"import"`:

```json
{
"name": "my-package",
"exports": {
"node": "./index.cjs",
"default": "./index.mjs"
}
}
```

With this configuration:

* Node.js always loads the CommonJS version, regardless of whether the package
is `require()`d or `import`ed, avoiding the dual package hazard.
* Other environments (such as browsers or bundlers configured for non-Node.js
targets) use the ES module version via the `"default"` condition.
* Bundlers configured to target Node.js use the `"node"` condition.

This approach ensures there is only one copy of the package loaded per
environment, while still allowing non-Node.js environments to benefit from
ES modules.

### Approach 2: Isolate state in a CommonJS wrapper

If the package must provide both true ESM and CJS entry points in Node.js (for
example, to allow ESM consumers to use top-level `await`), the stateful parts
can be isolated in a CommonJS module that is shared by both entry points. See
[the package examples repository][] for details.

## Node.js `package.json` field definitions

Expand Down
Loading