Skip to content

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.ts and .d.cts files

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.

Proprietary software. All rights reserved.