Skip to main content

tsup vs unbuild vs pkgroll: TypeScript Library Bundling 2026

·PkgPulse Team

TL;DR

tsup dominates with 3M weekly downloads — but unbuild's stub mode is the monorepo killer feature nobody talks about enough. tsup is esbuild-powered: 30-50x faster than tsc alone, zero config, generates CJS + ESM + declarations in one command. unbuild adds stub mode (no rebuild during development) and is the standard in the UnJS ecosystem (Nitro, H3, Nuxt). pkgroll is a newer Rollup-based option that reads your package.json exports map directly. For most library authors in 2026: tsup is the correct default.

Key Takeaways

  • tsup: 3M downloads/week, esbuild-based, dts: true handles declarations, supports --watch
  • unbuild: 500K downloads/week, unbuild --stub for zero-rebuild dev, Rollup internals
  • pkgroll: 80K downloads/week, zero config (reads package.json exports), Rollup-based
  • Declaration files: All three generate .d.ts (tsup via rollup-plugin-dts, unbuild via tsc)
  • Speed: esbuild (tsup) > Rollup (unbuild/pkgroll) by 3-5x on build time
  • Monorepo DX: unbuild --stub is unbeatable (source files served directly)

The Problem: TypeScript Library Bundling Is Hard

Publishing a TypeScript library in 2026 means you need:

  1. CJS build (dist/index.js) for Node.js/older bundlers
  2. ESM build (dist/index.mjs) for modern bundlers and tree-shaking
  3. Type declarations (dist/index.d.ts) for consumers
  4. A correct package.json exports map
  5. Source maps for debugging
  6. Proper tree-shaking (no side effects pollution)

Doing this with raw tsc is painful. That's why tsup/unbuild/pkgroll exist.


Head-to-Head Benchmark

Library: 50 TypeScript files, ~5,000 lines, re-exports from 3 packages

Build (CJS + ESM + declarations):
  tsup 1.x:     1.2s   ← fastest
  pkgroll 0.x:  2.1s
  unbuild 2.x:  3.4s
  tsc alone:    8.2s   (declarations only)

Watch mode (single file change rebuild):
  tsup:     ~120ms
  pkgroll:  ~190ms
  unbuild:  ~280ms

Stub mode (unbuild only):
  unbuild --stub: ~50ms (creates shims, no actual compile)

tsup: Detailed Config

// tsup.config.ts — the most common options:
import { defineConfig } from 'tsup';

export default defineConfig([
  // Main package:
  {
    entry: { index: 'src/index.ts' },
    format: ['cjs', 'esm'],
    dts: true,
    sourcemap: true,
    clean: true,
    splitting: true,  // Code split for ESM tree-shaking
    external: ['react'],
  },
  // Separate CLI entry (no types needed):
  {
    entry: { cli: 'src/cli.ts' },
    format: ['cjs'],
    sourcemap: false,
    banner: { js: '#!/usr/bin/env node' },  // For executables
  },
]);
// Generated package.json exports map:
{
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.mts",
        "default": "./dist/index.mjs"
      },
      "require": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      }
    }
  },
  "bin": { "my-cli": "./dist/cli.js" }
}

tsup Gotchas

// Problem: .d.ts generation can be slow (uses rollup-plugin-dts)
// Solution: Generate declarations separately for large projects:
export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: {
    resolve: true,    // Bundle .d.ts from dependencies too
    compilerOptions: {
      incremental: false,  // Disable for CI reproducibility
    },
  },
});

// Problem: CJS + ESM interop for default exports
// Solution: Use named exports (avoid `export default`):
// src/index.ts:
export { MyClass } from './MyClass';  // ✅ Works in CJS + ESM
export default MyClass;               // ⚠️  Can cause CJS issues

unbuild: Stub Mode Explained

// build.config.ts:
import { defineBuildConfig } from 'unbuild';

export default defineBuildConfig({
  entries: [
    'src/index',      // Auto-detects .ts extension
    'src/utils',      // Multiple entries
  ],
  declaration: true,
  clean: true,
  
  rollup: {
    emitCJS: true,
    esbuild: {
      target: 'es2022',
      minify: false,
    },
    inlineDependencies: false,
  },
  
  externals: ['react', 'react-dom', 'vue'],
});

Stub Mode in a Turborepo Monorepo

packages/
  ui/
    src/index.ts       ← Source
    dist/
      index.mjs        ← Stub: "export * from '../src/index.ts'"
      index.cjs        ← Stub: "module.exports = require('../src/index.ts')"
    package.json       ← exports point to dist/
  
apps/
  web/
    src/page.tsx       ← imports from 'ui'
# Development workflow with stub mode:
# 1. Build stubs (one-time, ~50ms):
cd packages/ui && npx unbuild --stub

# 2. Start app (no need to rebuild ui on changes):
cd apps/web && pnpm dev

# 3. Edit packages/ui/src/index.ts
#    → app immediately sees changes (stub resolves to source)
#    → No "forgot to rebuild ui" bugs

pkgroll: Zero Config Approach

// package.json — pkgroll reads this and generates builds:
{
  "scripts": {
    "build": "pkgroll",
    "watch": "pkgroll --watch"
  },
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    },
    "./utils": {
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs",
      "types": "./dist/utils.d.ts"
    }
  },
  "bin": {
    "my-cli": "./dist/cli.mjs"
  }
}

pkgroll auto-detects: if exports map has .mjs → build ESM, .cjs → build CJS, .d.ts → generate declarations. No config file needed.


Choosing Between the Three

ScenarioBest choiceWhy
New npm packagetsupMost docs, fastest DX, proven
Monorepo packagesunbuildStub mode, zero-rebuild dev
UnJS ecosystemunbuildUsed by Nitro, H3, Nuxt, etc.
Zero config philosophypkgrollReads package.json only
Max build speedtsupesbuild vs Rollup
Complex export mapspkgrollDriven by package.json exports
CLI toolstsupbanner for shebang, multiple formats
Decision tree:
  1. Are you in a monorepo where packages depend on each other?
     YES → unbuild (stub mode)
     NO → continue

  2. Do you want zero config (no tsup.config.ts)?
     YES → pkgroll
     NO → continue

  3. Default: tsup

Compare tsup, unbuild, and pkgroll on PkgPulse.

Comments

Stay Updated

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