Skip to main content

tsup vs tsdown vs unbuild: TypeScript Library Bundling (2026)

·PkgPulse Team

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

Featuretsuptsdownunbuild
Build engineesbuild (Go)Rolldown (Rust)Rollup (JS)
Build speedFast⚡ FastestModerate
ESM + CJS
.d.ts generation
Stub mode (no build)
Code splitting
treeshake
Plugin ecosystemesbuild pluginsRolldown pluginsRollup plugins
TypeScript configtsup.config.tstsdown.config.tsbuild.config.ts
Community size⭐ LargeGrowing fastMedium
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.

Compare build tooling and bundler packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.