Repository navigation
fix(exports): provide ESM resolution for ./compat/* subpaths - #1757
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
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.jsonconditional exports for./compat/*to splitimportvsrequirewith differenttypes/defaulttargets. - Extend
.scripts/postbuild.shto generatecompat/*.mjsandcompat/*.d.mtswrappers alongside existing CJS and.d.tswrappers.
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.
|
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! |
The
./compat/*wildcard export only declared a CJSdefaultcondition, soimport get from "es-toolkit/compat/get"always resolved to a CommonJS stub even in fully-ESM graphs. Every other subpath already splitsimport/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.mtstypes alongside the existing.js/.d.tsin postbuild, and split./compat/*intoimport/requireconditions like every other subpath. ESM importers now resolve to pure-ESM.mjs(zerorequire(in the chain);requirestill gets CJS. attwnode16 (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):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:
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 definedand proposedthis 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