Skip to content

fix(exports): provide ESM resolution for ./compat/* subpaths - #1757

Merged
raon0211 merged 1 commit into
toss:mainfrom
Amund211:fix/compat-subpath-esm-exports
Jun 7, 2026
Merged

raon0211 merged 1 commit into
toss:mainfrom
Amund211:fix/compat-subpath-esm-exports

Conversation

@Amund211

@Amund211 Amund211 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

The ./compat/* wildcard export only declared a CJS default condition, so import get from "es-toolkit/compat/get" always resolved to a CommonJS stub even in fully-ESM graphs. Every other subpath already splits import/require; compat alone shipped CJS to ESM importers, costing interop wrappers and worse tree-shaking.

This was uncovered by a rolldown (vite 8) chunk-rendering bug where the dist require_* helper naming crashed production builds (rolldown#9653) — but that bug is now fixed upstream, so it is not the motivation here. The motivation is the independent, measurable cost of shipping CJS to ESM consumers, which stands regardless of any bundler.

Generate .mjs (ESM) stubs and .d.mts types alongside the existing .js/.d.ts in postbuild, and split ./compat/* into import / require conditions like every other subpath. ESM importers now resolve to pure-ESM .mjs (zero require( in the chain); require still gets CJS. attw node16 (from ESM) goes from "Incorrect default export" to "OK (ESM)".

Package size impact (measured via npm pack; dist/ is byte-identical between old and new — the ESM implementations already ship either way, this only adds tiny one-line re-export stubs):

    metric            OLD (CJS)   NEW (+ESM)   delta
    tarball (gzip)    645.5 KB    670.0 KB     +24.5 KB (+3.8%)
    unpacked          3.33 MB     3.37 MB      +0.04 MB (+1.2%)
    file count        3018        3612         +594 (297 fns x 2)

Smaller consumer bundles. Bundled small ESM apps that deep-import compat functions with esbuild (bundle + tree-shake + minify) against the old and new packages and compared output bytes:

    import set                       OLD (CJS)   NEW (ESM)   reduction
    get + maxBy                      11,602 B    8,122 B     -30.0%
    recharts 3's 11 deep-imports     21,526 B    15,193 B    -29.4%

Timeline:
2025-06-07 #1199 (f6e1ce3) added this exact change (.mjs/.d.mts
stubs + import/require split) to support ESM consumers.
2025-06-15 e6f6762 reverted it ("Provide compat/* in CommonJS
format by default") — direct push to main, no PR, no
rationale in the message.
2025-10-18 #1479 reported exports is not defined and proposed
this identical fix. Closed; maintainer rationale was
package size + "importing CJS from ESM is fine."
2026-06-05 #1756 filed. The triggering rolldown bug is fixed
upstream, but it surfaced that compat/* ships CJS to ESM
importers — independently worth fixing (size, resolution,
tree-shaking) per the measurements above.

Closes #1756

The `./compat/*` wildcard export only declared a CJS `default`
condition, so `import get from "es-toolkit/compat/get"` always
resolved to a CommonJS stub even in fully-ESM graphs. Every other
subpath already splits `import`/`require`; compat alone shipped CJS to
ESM importers, costing interop wrappers and worse tree-shaking.

This was uncovered by a rolldown (vite 8) chunk-rendering bug where
the dist `require_*` helper naming crashed production builds
(rolldown#9653) — but that bug is now fixed upstream, so it is not the
motivation here. The motivation is the independent, measurable cost of
shipping CJS to ESM consumers, which stands regardless of any bundler.

Generate `.mjs` (ESM) stubs and `.d.mts` types alongside the existing
`.js`/`.d.ts` in postbuild, and split `./compat/*` into `import` /
`require` conditions like every other subpath. ESM importers now
resolve to pure-ESM `.mjs` (zero `require(` in the chain); `require`
still gets CJS. attw `node16 (from ESM)` goes from "Incorrect default
export" to "OK (ESM)".

Package size impact (measured via `npm pack`; dist/ is byte-identical
between old and new — the ESM implementations already ship either way,
this only adds tiny one-line re-export stubs):

    metric            OLD (CJS)   NEW (+ESM)   delta
    tarball (gzip)    645.5 KB    670.0 KB     +24.5 KB (+3.8%)
    unpacked          3.33 MB     3.37 MB      +0.04 MB (+1.2%)
    file count        3018        3612         +594 (297 fns x 2)

Smaller consumer bundles. Bundled small ESM apps that deep-import
compat functions with esbuild (bundle + tree-shake + minify) against
the old and new packages and compared output bytes:

    import set                       OLD (CJS)   NEW (ESM)   reduction
    get + maxBy                      11,602 B    8,122 B     -30.0%
    recharts 3's 11 deep-imports     21,526 B    15,193 B    -29.4%

Timeline:
  2025-06-07  toss#1199 (f6e1ce3) added this exact change (.mjs/.d.mts
              stubs + import/require split) to support ESM consumers.
  2025-06-15  e6f6762 reverted it ("Provide compat/* in CommonJS
              format by default") — direct push to main, no PR, no
              rationale in the message.
  2025-10-18  toss#1479 reported `exports is not defined` and proposed
              this identical fix. Closed; maintainer rationale was
              package size + "importing CJS from ESM is fine."
  2026-06-05  toss#1756 filed. The triggering rolldown bug is fixed
              upstream, but it surfaced that compat/* ships CJS to ESM
              importers — independently worth fixing (size, resolution,
              tree-shaking) per the measurements above.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 6, 2026 09:45
@vercel

vercel Bot commented Jun 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
es-toolkit Ready Ready Preview, Comment Jun 6, 2026 9:50am

Request Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates the package’s ./compat/* exports to support separate ESM/CJS entrypoints (and corresponding TypeScript declaration outputs) and extends the post-build script to generate the additional compat wrapper files.

Changes:

  • Adjust package.json conditional exports for ./compat/* to split import vs require with different types/default targets.
  • Extend .scripts/postbuild.sh to generate compat/*.mjs and compat/*.d.mts wrappers alongside existing CJS and .d.ts wrappers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
package.json Adds conditional import/require mapping (including types) for ./compat/* exports.
.scripts/postbuild.sh Generates additional ESM (.mjs) and ESM declaration (.d.mts) compat wrapper files during postbuild.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread package.json Resolved
Comment thread .scripts/postbuild.sh Resolved
Comment thread .scripts/postbuild.sh Resolved
Comment thread .scripts/postbuild.sh Resolved
@raon0211

raon0211 commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

We originally held off on adding ESM exports for individual functions because (1) we already do some manual tree-shaking, and (2) it bloats our install size. But it looks like it still helps with bundle size, so let's go ahead and merge this. Thanks for the PR!

This branch was successfully deployed

1 active deployment
Preview — 399ad798 Deployed Jun 6, 2026 by vercel[bot]
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.

esm exports: ./compat/* subpath exports resolve to CJS for ESM importers (no import condition)

3 participants