Skip to content

fix: resolve ESM-only exports conditions when packaging (#281)#283

Open
robertsLando wants to merge 1 commit into
mainfrom
fix/issue-281-esm-exports
Open

fix: resolve ESM-only exports conditions when packaging (#281)#283
robertsLando wants to merge 1 commit into
mainfrom
fix/issue-281-esm-exports

Conversation

@robertsLando

Copy link
Copy Markdown
Member

Closes #281

Problem

A packaged ESM app crashes at startup when it imports a dependency whose exports map is valid for ESM import but not resolvable through CommonJS require(). Plain Node runs fine; the packaged binary builds but crashes at runtime.

Two cases (from the repro):

  • Import-only { "exports": { ".": { "import": "./index.mjs" } } }ERR_PACKAGE_PATH_NOT_EXPORTED
  • .mjs targetsMODULE_NOT_FOUND .../index.mjs

Root cause

pkg transforms ESM→CJS at build time and renames each dependency's .mjs files to .js in the snapshot, but left the dependency's package.json exports field untouched. Since Node's CJS resolver always prefers exports over main, at runtime require('<dep>') resolves against the original, unconverted exports map and fails. The pre-existing "synthetic main" workaround could never help, because Node ignores main whenever exports is present.

Fix

  • New exported helper normalizeExportsForCJS() in lib/esm-transformer.ts (recursive): rewrites every .mjs string target to .js, and for any conditions object lacking a CJS-resolvable condition (require/node/node-addons/default) injects a synthetic require mirroring the import/default target.
  • lib/walker.ts: rewrites the snapshot's exports field via normalizeExportsForCJS in the existing package.json patch block (alongside synthetic-main and type rewrites). Gated on !seaMode, matching the surrounding ESM-transform gating.

Tests

  • test/unit/esm-transformer.test.ts — 7 unit cases for normalizeExportsForCJS (string, import-only, .mjs-target, default fallback, no-op when CJS condition present, nested/subpath recursion, arrays).
  • test/test-54-esm-exports-conditions/ — e2e regression mirroring the repro (esm-only import-only package + req-mjs .mjs-target package). Fails without the fix (exit 1), passes with it (exit 0).

Verification

  • Reproduced both crashes against the repro, confirmed correct output after the fix.
  • tsc --noEmit, prettier, eslint all clean; existing ESM e2e tests (test-50/51/52/53) still pass.
  • SEA mode intentionally excluded (no ESM→CJS transform there), consistent with adjacent logic.

🤖 Generated with Claude Code

Packaged ESM apps crashed at startup when importing a dependency whose
package.json "exports" map is valid for `import` but not resolvable
through CommonJS `require()`.

pkg transforms ESM to CJS and renames .mjs files to .js in the snapshot,
but left the "exports" field untouched. Node's CJS resolver always
prefers "exports" over "main", so at runtime it resolved the original,
unconverted conditions and failed with:
  - ERR_PACKAGE_PATH_NOT_EXPORTED for import-only packages, and
  - MODULE_NOT_FOUND for targets pointing at the renamed .mjs files.

The previous synthetic-"main" workaround could never take effect because
Node ignores "main" whenever "exports" is present.

Rewrite the "exports" field in the snapshotted package.json to mirror the
ESM->CJS transformation pkg already applies: rewrite every .mjs target to
.js, and add a synthetic `require` condition (mirroring `import`/`default`)
to any conditions object that lacks a CJS-resolvable condition.

Add unit tests for normalizeExportsForCJS and an e2e regression test
(test-54-esm-exports-conditions) covering both failure cases.
@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.77419% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.12%. Comparing base (546bbf0) to head (329d771).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/esm-transformer.ts 95.71% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #283      +/-   ##
==========================================
+ Coverage   86.36%   87.12%   +0.76%     
==========================================
  Files          22       22              
  Lines        7297     7389      +92     
  Branches     1047     1076      +29     
==========================================
+ Hits         6302     6438     +136     
+ Misses        988      943      -45     
- Partials        7        8       +1     
Files with missing lines Coverage Δ
lib/walker.ts 87.77% <100.00%> (+0.50%) ⬆️
lib/esm-transformer.ts 89.08% <95.71%> (+1.06%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

Packaged ESM app crashes at startup when dependency exports is not CJS-resolvable

1 participant