From f9cc655841b5153869a9503d27e6054bfbe75ccc Mon Sep 17 00:00:00 2001 From: Akash Gupta Date: Tue, 7 Apr 2026 14:20:14 +0530 Subject: [PATCH 1/2] 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: https://github.com/nodejs/node/issues/52174 --- doc/api/packages.md | 57 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/doc/api/packages.md b/doc/api/packages.md index 429b1b19b90801..4d645088fd4f47 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -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 + +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 From e392a48b2a20327b2e94b83915ed9755c0870c65 Mon Sep 17 00:00:00 2001 From: Akash Gupta Date: Wed, 8 Apr 2026 15:37:26 +0530 Subject: [PATCH 2/2] doc: add approach for using a single format with default Address review feedback by adding Approach 1 that recommends using just the "default" condition with a single format as the simplest solution to the dual package hazard. --- doc/api/packages.md | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/doc/api/packages.md b/doc/api/packages.md index 4d645088fd4f47..1b80112cb3a41c 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -967,11 +967,41 @@ 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 +### Approach 1: Use a single format with `default` -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"`: +The simplest way to avoid the dual package hazard is to pick one format and +export it using the `"default"` condition: + +```json +{ + "name": "my-package", + "exports": { + "default": "./index.cjs" + } +} +``` + +If the package uses CommonJS, non-Node.js environments will need to bundle it. +Alternatively, an ES module entry point can be used: + +```json +{ + "name": "my-package", + "exports": { + "default": "./index.mjs" + } +} +``` + +In this case, CommonJS consumers can only use the package in Node.js versions +that support [`require()` of ES modules][]. ESM consumers can always `import` +it regardless of the format. + +### Approach 2: Use `node` and `default` conditions + +If the package needs to provide different entry points for Node.js and other +environments, use the `"node"` and `"default"` conditions instead of +`"require"` and `"import"`: ```json { @@ -995,7 +1025,7 @@ 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 +### Approach 3: 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 @@ -1241,6 +1271,7 @@ This field defines [subpath imports][] for the current package. [folders as modules]: modules.md#folders-as-modules [import maps]: https://github.com/WICG/import-maps [load ECMAScript modules from CommonJS modules]: modules.md#loading-ecmascript-modules-using-require +[`require()` of ES modules]: modules.md#loading-ecmascript-modules-using-require [merve]: https://github.com/anonrig/merve [packages folder mapping]: https://github.com/WICG/import-maps#packages-via-trailing-slashes [self-reference]: #self-referencing-a-package-using-its-name