ADR-002: Dual ESM/CJS Publishing
Status: Accepted Date: 2026-01-25
Context
The JavaScript ecosystem is in transition from CommonJS (CJS) to ECMAScript Modules (ESM). Different tools and environments have varying support:
- Modern bundlers (Vite, esbuild): Prefer ESM
- Node.js: Supports both (ESM preferred in v20+)
- Legacy tools: Still require CJS
We need to publish packages that work everywhere.
Decision
Publish all packages as dual ESM + CJS using tsup with conditional exports.
json
{
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
}
}
}Consequences
Positive
- Universal compatibility -- Works with any bundler or runtime
- Tree-shaking -- ESM enables dead code elimination
- Future-proof -- ESM is the standard going forward
- Modern defaults --
"type": "module"signals modern practices
Negative
- Larger package size -- Two builds per package
- Build complexity -- tsup configuration required for all packages
- Type definition duplication -- Separate
.d.tsand.d.ctsfiles
Alternatives Considered
- ESM-only -- Simpler, but breaks compatibility with CJS-only tools. Rejected because we want broad adoption.
- CJS-only -- Maximum compatibility but no tree-shaking benefits. Rejected because modern bundlers are the primary target.