Skip to content

fix: make the Node.js ESM entry work on Node.js 14.0 - #191

Merged
unadlib merged 4 commits into
mainfrom
fix/node-14-esm-entry
Oct 7, 2026
Merged

unadlib merged 4 commits into
mainfrom
fix/node-14-esm-entry

Conversation

@unadlib

@unadlib unadlib commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Part of #168.

Summary

The v2 pre-release review (R3) found that import 'mutative' fails on Node.js 14.0 to 14.12, although engines declares >=14.0. #182 made the node import condition resolve dist/index.mjs, which re-exports the CJS entry by name, export { apply, create, … } from './index.js'. Node.js detects the names that a CommonJS module exports only since 14.13.0, so earlier versions reject the entry:

SyntaxError: The requested module './index.js' does not provide an export named 'apply'

require('mutative') works on every version, and npm 1.3.0, whose import resolved the ESM bundle, works on 14.0, with two instances. This PR keeps engines at >=14.0 and makes the entry work there, without changing its exports or the one instance that import and require share.

Changes, one commit per item

  1. fix: make the Node.js ESM entry work on Node.js 14.0: the entry takes the exports object of the CJS entry as its default import and exports its functions by name. A default import of a CommonJS module works on every Node.js version with ES modules, and it is the same CJS instance that require returns. scripts/write-package-entries.mjs fails if the CJS bundle sets __esModule, because bundlers that resolve this entry and follow that convention would then import exports.default; the bundle sets it neither now nor before.
    import mutative from './index.js';
    
    export const { apply, castDraft, …, unsafe } = mutative;
  2. test: check the packed package on the oldest Node.js version that engines allows: scripts/check-minimum-node.mjs installs the packed tarball with that version's npm and, in development and production, loads the CJS entry with require and the Node.js ESM entry with static and dynamic import, checks that both expose the same functions of one instance, and runs a runtime test of objects, native array methods, Map and Set drafts, auto-freeze, patches, strict mode, marks, manual finalization and async recipes. It refuses to run on any version other than the floor of engines, so the CI job has to follow a change of engines. A new minimum-node job in the Node CI workflow builds and packs with Node.js 24 and runs the script with Node.js 14.0.0. The script only uses what Node.js 14.0 provides: no node: specifiers, assert/strict or top-level await.
  3. test: bundle the Node.js ESM entry for Node.js in the package checks: pnpm test:package also bundles the package with esbuild for Node.js, which resolves the node condition and so this entry, and runs the bundle with all exports, in development and production.
  4. docs: describe the Node.js ESM entry and the minimum Node.js check: BUILDING.md.

Verification

The packed tarballs of main and of this PR, installed in a consumer and run on Node.js binaries from nodejs.org (x64 under Rosetta for 14.x), in development and production, with named, namespace and dynamic import:

Node.js main: import This PR: import require import and require share one instance
14.0.0 SyntaxError works works yes
14.13.0, 14.13.1 works works works yes
24.16 works works works yes
  • scripts/check-minimum-node.mjs passes on Node.js 14.0.0 with this PR, fails with the SyntaxError above on the tarball of main, and refuses to run on 14.13.0 and 24.16.
  • The rest of the package runs on Node.js 14.0 as well: the CJS bundles contain no syntax or API newer than 14.0, and a differential fuzz of 20,000 random sequences of array operations on each CJS bundle, comparing the native methods with the proxy path and replaying patches in both directions, gives the same results on 14.0.0 as on 24.
  • esbuild bundles for Node.js, with ESM and CJS output, minified and not, run with all exports, as with the previous entry.
  • The Node CI sequence passes locally, and each commit builds and passes test:package, lint and the format check. Of the 76 files in the tarball, only dist/index.mjs changes, so pnpm size and the size baseline are unaffected.

Not covered

Deep imports such as mutative/dist/mutative.cjs.production.min.js resolve through the "./*" subpath pattern in exports, which Node.js supports since 14.13.0. On 14.0 to 14.12 they fail with ERR_PACKAGE_PATH_NOT_EXPORTED, as they did with 1.3.0; the docs do not describe such imports.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Coverage after merging fix/node-14-esm-entry into main will be

100.00%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   apply.ts100%100%100%100%
   array.ts100%100%100%100%
   constant.ts100%100%100%100%
   create.ts100%100%100%100%
   current.ts100%100%100%100%
   draft.ts100%100%100%100%
   draftify.ts100%100%100%100%
   error.ts100%100%100%100%
   index.ts100%100%100%100%
   interface.ts100%100%100%100%
   internal.ts100%100%100%100%
   makeCreator.ts100%100%100%100%
   map.ts100%100%100%100%
   original.ts100%100%100%100%
   patch.ts100%100%100%100%
   rawReturn.ts100%100%100%100%
   set.ts100%100%100%100%
   unsafe.ts100%100%100%100%
src/utils
   cast.ts100%100%100%100%
   copy.ts100%100%100%100%
   deepFreeze.ts100%100%100%100%
   draft.ts100%100%100%100%
   finalize.ts100%100%100%100%
   forEach.ts100%100%100%100%
   index.ts100%100%100%100%
   mark.ts100%100%100%100%
   marker.ts100%100%100%100%
   proto.ts100%100%100%100%

@unadlib
unadlib merged commit 3c4a667 into main Oct 7, 2026
5 checks passed
@unadlib
unadlib deleted the fix/node-14-esm-entry branch October 7, 2026 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant