tsup vs tsdown vs unbuild: TypeScript Library Bundling (2026)
TL;DR
tsup is the most popular TypeScript library bundler — zero-config, generates ESM + CJS + .d.ts types automatically, used by thousands of npm packages. tsdown is the next-generation successor to tsup — built on Rolldown (Vite's Rust-based bundler), significantly faster, same DX but better performance. unbuild is from the UnJS ecosystem — supports multiple build presets, stub mode for development (no build step), and is used by Nuxt, Nitro, and UnJS libraries internally. In 2026: tsup is the safe choice with the largest community, tsdown is the emerging performance leader, unbuild if you're in the Nuxt/UnJS ecosystem.
Key Takeaways
- tsup: ~6M weekly downloads — esbuild-based, zero-config, ESM + CJS + types in one command
- tsdown: ~500K weekly downloads — Rolldown-based (Rust), 3-5x faster than tsup, tsup-compatible API
- unbuild: ~3M weekly downloads — UnJS, stub mode, multiple presets, Rollup-based
- All three generate dual ESM/CJS output — required for modern npm packages
- All three generate TypeScript declaration files (
.d.ts) automatically - tsdown is rapidly gaining adoption in 2026 as the fast tsup replacement
Why Library Bundling Matters
Problem: publishing TypeScript to npm requires:
1. Compile TypeScript → JavaScript
2. Generate .d.ts type declarations
3. Create both ESM (import) and CJS (require) versions
4. Tree-shaking to minimize bundle size
5. Correct package.json "exports" map
Without a bundler:
tsc --outDir dist → CJS only, no bundling
+ manually maintain package.json exports
+ separately configure dts generation
+ no tree-shaking
With tsup/tsdown/unbuild:
One command → dist/index.mjs + dist/index.cjs + dist/index.d.ts
tsup
tsup — zero-config library bundler:
Setup
npm install -D tsup typescript
# Add to package.json scripts:
{
"scripts": {
"build": "tsup",
"dev": "tsup --watch"
}
}
tsup.config.ts
import { defineConfig } from "tsup"
export default defineConfig({
entry: ["src/index.ts"], // Entry point(s)
format: ["esm", "cjs"], // Output ESM + CommonJS
dts: true, // Generate .d.ts files
splitting: false, // Code splitting (for multiple entry points)
sourcemap: true, // Generate source maps
clean: true, // Clean dist/ before build
minify: false, // Don't minify libraries (let consumers decide)
external: ["react", "vue"], // Don't bundle peer dependencies
treeshake: true, // Remove unused code
target: "es2020", // Output target
outDir: "dist",
})
Multiple entry points
import { defineConfig } from "tsup"
export default defineConfig({
// Multiple entry points (for sub-path exports):
entry: {
index: "src/index.ts",
server: "src/server.ts",
client: "src/client.ts",
},
format: ["esm", "cjs"],
dts: true,
splitting: true, // Share code between entry points
})
// Generates:
// dist/index.mjs + dist/index.js + dist/index.d.ts
// dist/server.mjs + dist/server.js + dist/server.d.ts
// dist/client.mjs + dist/client.js + dist/client.d.ts
package.json for dual ESM/CJS
{
"name": "my-library",
"version": "1.0.0",
"main": "./dist/index.js", // CJS entry (legacy)
"module": "./dist/index.mjs", // ESM entry (bundlers)
"types": "./dist/index.d.ts", // TypeScript types
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./server": {
"import": "./dist/server.mjs",
"require": "./dist/server.js"
}
},
"files": ["dist"],
"scripts": {
"build": "tsup",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"tsup": "^8.0.0",
"typescript": "^5.0.0"
}
}
Watch mode for development
# Rebuild on file changes:
tsup --watch
# Or in parallel with your dev server:
# package.json:
{
"scripts": {
"dev": "concurrently \"tsup --watch\" \"node dist/index.js\""
}
}
tsdown
tsdown — the Rolldown-based tsup successor:
Why tsdown is faster
tsup uses: esbuild (Go) → fast, but JS orchestration overhead
tsdown uses: Rolldown (Rust) → faster bundler + faster orchestration
Build time comparison (real-world library with 50 files):
tsup: ~2.5s
tsdown: ~0.6s
(varies by project size and machine)
Setup (nearly identical to tsup)
// tsdown.config.ts
import { defineConfig } from "tsdown"
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true,
clean: true,
sourcemap: true,
external: ["react"],
})
# Commands are the same as tsup:
npx tsdown # Build
npx tsdown --watch # Watch mode
tsup → tsdown migration
# Install:
npm uninstall tsup
npm install -D tsdown
# Rename config file:
mv tsup.config.ts tsdown.config.ts
# Update import:
# - import { defineConfig } from "tsup"
# + import { defineConfig } from "tsdown"
# Update package.json scripts:
# - "build": "tsup"
# + "build": "tsdown"
unbuild
unbuild — UnJS library bundler:
Setup
// build.config.ts
import { defineBuildConfig } from "unbuild"
export default defineBuildConfig({
entries: ["src/index"],
rollup: {
emitCJS: true, // Also emit CommonJS
},
declaration: true, // Generate .d.ts
clean: true,
})
# Build:
npx unbuild
# Stub mode (development):
npx unbuild --stub
Stub mode (unique to unbuild)
// "Stub mode" — generates proxy files that require/import the source directly
// No build step needed during development!
// dist/index.mjs (stub):
// export * from "../src/index.ts"
// dist/index.js (stub):
// module.exports = require("../src/index.ts") // via jiti
// Benefits:
// - No watch mode needed — file changes are picked up immediately
// - Faster feedback loop when developing a library locally
// - Link the package to a consumer with npm link — changes are live
// Production build:
// npx unbuild ← produces real bundles (no stub)
Multiple presets
import { defineBuildConfig } from "unbuild"
export default defineBuildConfig([
// Main package:
{
entries: ["src/index"],
declaration: true,
rollup: { emitCJS: true },
},
// CLI entry (no types needed):
{
entries: [{ input: "src/cli", name: "cli" }],
declaration: false,
rollup: {
emitCJS: false,
inlineDependencies: true, // Bundle everything into the CLI binary
},
},
])
Used by the UnJS ecosystem
unbuild is used by:
- nuxt → @nuxt/... packages
- nitro → the Nuxt server engine
- h3 → the HTTP framework
- ofetch → the fetch wrapper
- Most @unjs/* packages
If you're contributing to or building in this ecosystem, unbuild
is the natural choice.
Feature Comparison
| Feature | tsup | tsdown | unbuild |
|---|---|---|---|
| Build engine | esbuild (Go) | Rolldown (Rust) | Rollup (JS) |
| Build speed | Fast | ⚡ Fastest | Moderate |
| ESM + CJS | ✅ | ✅ | ✅ |
| .d.ts generation | ✅ | ✅ | ✅ |
| Stub mode (no build) | ❌ | ❌ | ✅ |
| Code splitting | ✅ | ✅ | ✅ |
| treeshake | ✅ | ✅ | ✅ |
| Plugin ecosystem | esbuild plugins | Rolldown plugins | Rollup plugins |
| TypeScript config | tsup.config.ts | tsdown.config.ts | build.config.ts |
| Community size | ⭐ Large | Growing fast | Medium |
| Weekly downloads | ~6M | ~500K | ~3M |
When to Use Each
Choose tsup if:
- The safe, battle-tested choice — most tutorials and examples use it
- Large community, most Stack Overflow answers, most plugins
- Works for 95% of library use cases out of the box
Choose tsdown if:
- Build speed is a priority (large libraries, frequent CI builds)
- You're migrating from tsup — API is nearly identical
- On the cutting edge of tooling in 2026
Choose unbuild if:
- Working in the Nuxt, Nitro, or UnJS ecosystem
- Want stub mode for instant development without watch rebuilds
- Need Rollup-specific plugins not available in esbuild/Rolldown
Also consider:
- Vite Library Mode — for libraries that need Vite plugins (CSS modules, etc.)
- pkgroll — minimal bundler for packages with simple needs
- microbundle — smaller alternative, but less actively maintained in 2026
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on tsup v8.x, tsdown v0.x, and unbuild v2.x.