<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026/raw.md -->
<!-- Source path: content/guides/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026.mdx -->

---
og_image: "/images/guides/tsup-vs-tsdown-vs-unbuild-typescript-library-bundling-2026.webp"
title: "tsup vs tsdown vs unbuild 2026"
description: "tsup, tsdown, and unbuild compared for TypeScript library bundling in 2026. Dual ESM/CJS output, .d.ts generation, stub mode, build speed, and when to migrate."
date: "2026-03-09"
tier: 2
authors: ["team"]
tags: ["typescript", "developer-tools", "nodejs", "automation"]
---

## 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](https://tsup.egoist.dev) — zero-config library bundler:

### Setup

```bash
npm install -D tsup typescript

# Add to package.json scripts:
{
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch"
  }
}
```

### tsup.config.ts

```typescript
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

```typescript
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

```json
{
  "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

```bash
# 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](https://tsdown.egoist.dev) — 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)

```typescript
// 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"],
})
```

```bash
# Commands are the same as tsup:
npx tsdown         # Build
npx tsdown --watch # Watch mode
```

### tsup → tsdown migration

```bash
# 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](https://github.com/unjs/unbuild) — UnJS library bundler:

### Setup

```typescript
// 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,
})
```

```bash
# Build:
npx unbuild

# Stub mode (development):
npx unbuild --stub
```

### Stub mode (unique to unbuild)

```typescript
// "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

```typescript
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 →](https://www.pkgpulse.com)*

*See also: [Bun vs Vite](/compare/bun-vs-vite) and [cac vs meow vs arg 2026](/guides/cac-vs-meow-vs-arg-lightweight-cli-argument-parsers-2026), [archiver vs adm-zip vs JSZip (2026)](/guides/archiver-vs-adm-zip-vs-jszip-zip-archive-creation-2026).*
