ADR-0006: tsup for ESM+CJS Dual Output
- Status: Accepted
- Date: 2026-03-07
- Phase: 0
- Deciders: Nick Hart
Context
The TypeScript ecosystem is in an ESM transition. Some consumers use ESM (import), others still require CJS (require). We need to ship both formats from every package with correct type declarations.
Decision
We will use tsup (esbuild-based) to build ESM + CJS dual output with .d.ts generation for every package.
typescript
// tsup.config.ts (identical across packages)
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
sourcemap: true,
clean: true,
});Rationale
tsup is fast (esbuild-powered), zero-config for common cases, and handles the dual-format complexity (conditional exports, .js/.cjs extensions) correctly. It's the most popular build tool for library authoring in the TypeScript ecosystem.
Alternatives Considered
tsc Only
- Pros: No extra dependency, TypeScript-native
- Cons: Cannot produce CJS + ESM from single source, no bundling, slow
Rollup + rollup-plugin-typescript2
- Pros: Maximum control, tree-shaking
- Cons: Complex config, slower than esbuild, more dependencies
unbuild
- Pros: Similar to tsup, passive bundling
- Cons: Less mature, smaller community
Consequences
- Positive: Fast builds, correct dual output, minimal config
- Negative: esbuild doesn't type-check (we use
tsc --noEmitseparately) - Neutral: All packages share identical
tsup.config.ts
Compliance
- Every package must have a
tsup.config.tswith the standard config package.jsonmust have conditionalexportswithimport,require, andtypes- CI runs
tsc --noEmit(typecheck) separately from build