Skip to main content

Guide

tsup vs tsdown vs unbuild 2026

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.

·PkgPulse Team·
0

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

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).

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.