Best TypeScript-First Build Tools 2026
TL;DR
tsup is the default for TypeScript library bundling in 2026 — zero config, esbuild-powered, 3M weekly downloads. unbuild (from the Nuxt/UnJS team) is the best choice for monorepos and cross-platform packages. pkgroll is the newest, fastest option with Rollup under the hood. If you're publishing an npm package today: tsup gets you to done fastest. If you need advanced export map control or cross-platform compatibility: unbuild.
Key Takeaways
- tsup: ~3M downloads/week, zero config, esbuild-powered, CommonJS + ESM output
- unbuild: ~500K downloads/week, UnJS ecosystem, advanced export maps, stub mode
- pkgroll: ~80K downloads/week, Rollup-based, fastest builds, newer
- esbuild (direct): 25M downloads/week — the underlying engine, use it via tsup usually
- For most libraries: tsup (simplest DX, proven, extensive docs)
- For monorepo packages: unbuild (stub mode = instant dev reloads)
Downloads
| Package | Weekly Downloads | Trend |
|---|---|---|
esbuild | ~25M | ↑ Growing |
tsup | ~3M | ↑ Growing |
unbuild | ~500K | ↑ Growing |
pkgroll | ~80K | ↑ Growing |
tsup: The Standard
npm install --save-dev tsup typescript
// tsup.config.ts — zero config or full config:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'], // Or multiple entries
format: ['cjs', 'esm'], // Output both formats
dts: true, // Generate .d.ts files
splitting: false, // No code splitting for libraries
sourcemap: true,
clean: true, // Clean dist before build
// Tree-shaking friendly:
treeshake: true,
// For bundling vs externalizing:
external: ['react', 'react-dom'], // Don't bundle peer deps
noExternal: ['my-util'], // Force bundle this dep
// Multiple entry points with custom output names:
// entry: {
// index: 'src/index.ts',
// cli: 'src/cli.ts',
// },
});
// package.json for the published library:
{
"name": "my-library",
"version": "1.0.0",
"main": "./dist/index.js", // CJS
"module": "./dist/index.mjs", // ESM
"types": "./dist/index.d.ts", // Types
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch"
}
}
tsup Watch Mode
# Rebuild on file changes (good for library development):
npx tsup --watch
# Or with type checking:
npx tsup --watch & npx tsc --watch --noEmit
unbuild: Monorepo Powerhouse
unbuild's killer feature: stub mode — instead of building, it creates a thin shim that points directly to your TypeScript source. Zero rebuild time during development.
npm install --save-dev unbuild
// build.config.ts:
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
entries: ['src/index'],
declaration: true, // Generate .d.ts
rollup: {
emitCJS: true, // Output CJS
esbuild: {
minify: false, // Keep readable in dev
target: 'es2020',
},
},
// External packages (don't bundle):
externals: ['react', 'vue', 'rollup'],
// For monorepo: auto-detect and external workspace packages
// failOnWarn: true, // Fail CI on warnings
});
Stub Mode (unbuild's Secret Weapon)
# Instead of building, creates a stub that points to source:
npx unbuild --stub
# This creates dist/index.mjs that just re-exports from src/:
# export * from '../src/index.ts'
# (TypeScript is executed directly via tsx)
// Monorepo package.json workflow:
{
"scripts": {
"dev": "unbuild --stub", // Instant — no rebuild on changes
"build": "unbuild", // Real build for publishing
"prepack": "unbuild" // Build before npm publish
}
}
In a Turborepo: pnpm dev runs unbuild --stub in all packages simultaneously, then app packages can import them directly without waiting for builds.
pkgroll: Rollup-Powered Speed
npm install --save-dev pkgroll
// package.json — pkgroll reads exports map and builds accordingly:
{
"scripts": {
"build": "pkgroll",
"watch": "pkgroll --watch"
},
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
}
}
pkgroll reads your package.json exports map and builds whatever outputs you declare. Zero separate config file needed.
esbuild Direct (For Maximum Control)
// build.ts — direct esbuild API:
import esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
format: 'esm',
outfile: 'dist/index.mjs',
external: ['react', 'react-dom'],
target: 'es2020',
sourcemap: true,
treeShaking: true,
minify: process.env.NODE_ENV === 'production',
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'development'),
},
});
// Run for CJS:
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
format: 'cjs',
outfile: 'dist/index.js',
external: ['react', 'react-dom'],
});
// Generate declarations separately (esbuild doesn't emit .d.ts):
import { execSync } from 'child_process';
execSync('tsc --emitDeclarationOnly --outDir dist');
esbuild doesn't generate TypeScript declarations — you need a separate tsc --emitDeclarationOnly step or use tsup which handles this.
Comparison Table
| tsup | unbuild | pkgroll | esbuild | |
|---|---|---|---|---|
| Config needed | Minimal | Minimal | Zero (reads package.json) | JavaScript API |
| Declaration (.d.ts) | ✅ via dts | ✅ | ✅ | ❌ (manual tsc) |
| Stub mode | ❌ | ✅ | ❌ | ❌ |
| Underlying engine | esbuild | Rollup+esbuild | Rollup | esbuild |
| Speed | Fast | Medium | Fast | Fastest |
| Ecosystem | Large | UnJS | Small | Huge (lower-level) |
| Best for | Most libraries | Monorepos | Modern packages | Custom build scripts |
Decision Guide
Use tsup if:
→ Publishing a new npm package (best DX, most docs)
→ Need CJS + ESM + declarations in one command
→ Team is familiar with esbuild ecosystem
→ Simple to moderate library requirements
Use unbuild if:
→ Monorepo where packages depend on each other
→ Need stub mode for instant dev turnaround
→ UnJS ecosystem (Nitro, Nuxt, H3, etc.)
→ Advanced export map customization
Use pkgroll if:
→ Want zero config (just describe exports in package.json)
→ Modern pure-ESM package
→ Simpler library with straightforward exports
Use esbuild directly if:
→ Application builds (not library)
→ Need programmatic control over build steps
→ Building as part of a larger custom pipeline
Compare tsup, unbuild, and esbuild package health on PkgPulse.