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
Community Adoption in 2026
tsup leads at approximately 6 million weekly downloads, reflecting its long track record since 2021 and its role as the default recommendation in virtually every "how to build a TypeScript library" tutorial. The community has produced extensive examples, plugins, and documentation. For a developer new to TypeScript library publishing, tsup remains the path of least resistance.
unbuild reaches around 3 million weekly downloads, driven heavily by the UnJS ecosystem's prevalence. Every project that depends on h3, ofetch, nitro, or nuxt transitively depends on packages built with unbuild. Stub mode is unbuild's truly unique feature — the ability to develop a library without running a watch process — and it has no equivalent in tsup or tsdown.
tsdown is at approximately 500,000 weekly downloads but growing rapidly as of early 2026, particularly in projects that have adopted Rolldown. Evan You (Vite) has publicly signaled that tsdown is intended as the long-term path forward as Vite migrates its production bundler from Rollup to Rolldown. The migration from tsup to tsdown is intentionally frictionless — most libraries can migrate by changing the import in the config file from "tsup" to "tsdown" and renaming the file — which has accelerated early adoption.
TypeScript Library Publishing Workflow
Publishing a TypeScript library to npm requires more than bundling — it requires correct declaration file generation, dual CJS/ESM output, correct package.json exports fields, and a reproducible build that consumers can trust. Each of the three bundlers handles this end-to-end workflow differently.
tsup has the most opinionated defaults for library publishing. Running tsup src/index.ts --format cjs,esm --dts produces dist/index.js (CJS), dist/index.mjs (ESM), and dist/index.d.ts (declarations) in a single command. The package.json exports field can then point to each output. Many tsup users pair it with publint and arethetypeswrong in CI to validate that the published package resolves correctly in both environments. tsup handles re-exports, path aliases, and CSS bundling for component libraries in its default configuration, making it self-sufficient for most library authors.
unbuild's stub mode is particularly valuable during local development. Rather than running a watch process that re-bundles on every change, unbuild --stub creates thin re-export files that redirect imports directly to your TypeScript source. This means changes to the library source are reflected immediately in consuming packages without a rebuild step. The tradeoff is that the stub output is not suitable for publishing — you still run a full build before releasing. For monorepos where packages consume each other during development, stub mode eliminates the "build loop" problem entirely.
tsdown inherits tsup's configuration format intentionally, so the tsdown.config.ts file structure is nearly identical. The primary difference visible to library authors is build speed — tsdown's Rolldown engine shows 3-10x faster build times for libraries with many entry points or complex dependency graphs. For most single-entry libraries, the speed difference is small in absolute terms but meaningful in CI where multiple packages build sequentially.
A practical consideration for library authors: declaration file generation (--dts) is the slowest part of any TypeScript bundler build. tsup and tsdown delegate declaration generation to tsc or the @microsoft/api-extractor API surface, while unbuild uses Rollup's TypeScript plugin. For large libraries with deep type hierarchies, declaration generation often takes longer than the JavaScript bundling itself, and this cost is constant regardless of which bundler you choose.
For library authors targeting both ESM and CJS consumers in 2026, the exports field in package.json is the canonical way to declare outputs. A typical configuration specifies separate entry points for require() (CJS) and import (ESM): "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs" } }. All three bundlers produce these dual outputs — the difference is how they handle edge cases like re-exported types, CJS interop for default exports, and declaration maps. Tools like publint and are-the-types-wrong validate these fields at publish time and are strongly recommended as part of any library's CI pipeline regardless of which bundler is used.
Validating Your Published Package
Publishing a library without validating the output is one of the most common sources of user-reported bugs. A build that compiles successfully can still produce a package that fails for consumers using different module resolution strategies. Two tools have become the community standard for catching these issues before release.
publint lints the package.json exports field against the built output and flags mismatches: missing .mjs files referenced in import conditions, wrong file extensions, or main pointing to a nonexistent path. are-the-types-wrong specifically checks that TypeScript consumers get correct type declarations for both ESM and CJS import paths. Running both tools as part of a prepublishOnly script catches the majority of publishing issues before they reach npm. All three bundlers — tsup, tsdown, and unbuild — are fully compatible with these validators, and adding them takes under five minutes.
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on tsup v8.x, tsdown v0.x, and unbuild v2.x. All three tools are actively maintained with regular releases as of Q1 2026, reflecting the rapid pace of the TypeScript tooling ecosystem.
Compare build tooling and bundler packages on PkgPulse →
See also: Bun vs Vite and cac vs meow vs arg 2026, archiver vs adm-zip vs JSZip (2026).