Skip to content

Commit f9cc655

Browse files
committed
doc: recommend node/default conditions for dual packages
Expand the Dual CommonJS/ES module packages section to explain the dual package hazard and recommend using the "node" and "default" export conditions as the primary approach to avoid it. Fixes: #52174
1 parent 4f08c64 commit f9cc655

File tree

1 file changed

+56
-1
lines changed

1 file changed

+56
-1
lines changed

doc/api/packages.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,62 @@ $ node other.js
945945

946946
## Dual CommonJS/ES module packages
947947

948-
See [the package examples repository][] for details.
948+
Prior to the introduction of [`"exports"`][], authors of packages that support
949+
both CommonJS and ES modules typically used the `"import"` and `"require"`
950+
conditions. However, using these conditions can lead to the _dual package
951+
hazard_, where the same package may be loaded twice (once as CommonJS and once
952+
as an ES module), causing issues with package state and object identity.
953+
954+
For example, given a package with the following `package.json`:
955+
956+
```json
957+
{
958+
"name": "my-package",
959+
"exports": {
960+
"import": "./index.mjs",
961+
"require": "./index.cjs"
962+
}
963+
}
964+
```
965+
966+
If one dependency `require()`s `my-package` while another `import`s it, two
967+
separate copies of the package are loaded. Any state or objects exported by the
968+
package will not be shared between the two copies.
969+
970+
### Approach 1: Use `node` and `default` conditions
971+
972+
The recommended approach to avoid the dual package hazard while still providing
973+
both CommonJS and ES module entry points is to use the `"node"` and `"default"`
974+
conditions instead of `"require"` and `"import"`:
975+
976+
```json
977+
{
978+
"name": "my-package",
979+
"exports": {
980+
"node": "./index.cjs",
981+
"default": "./index.mjs"
982+
}
983+
}
984+
```
985+
986+
With this configuration:
987+
988+
* Node.js always loads the CommonJS version, regardless of whether the package
989+
is `require()`d or `import`ed, avoiding the dual package hazard.
990+
* Other environments (such as browsers or bundlers configured for non-Node.js
991+
targets) use the ES module version via the `"default"` condition.
992+
* Bundlers configured to target Node.js use the `"node"` condition.
993+
994+
This approach ensures there is only one copy of the package loaded per
995+
environment, while still allowing non-Node.js environments to benefit from
996+
ES modules.
997+
998+
### Approach 2: Isolate state in a CommonJS wrapper
999+
1000+
If the package must provide both true ESM and CJS entry points in Node.js (for
1001+
example, to allow ESM consumers to use top-level `await`), the stateful parts
1002+
can be isolated in a CommonJS module that is shared by both entry points. See
1003+
[the package examples repository][] for details.
9491004

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

0 commit comments

Comments
 (0)