Skip to main content

tsup vs unbuild vs pkgroll: TypeScript Library Bundling 2026

·PkgPulse Team

Eighty percent of TypeScript library authors have settled on tsup. But the libraries powering Nuxt.js, UnJS, and major open-source projects use unbuild. And a growing number of package authors use pkgroll because it derives its entire configuration from package.json. All three tools solve the same problem — bundling a TypeScript library for npm — with meaningfully different tradeoffs.

TL;DR

tsup for the best default experience: fast builds, zero-config, excellent DX. unbuild when you need superior tree-shaking or are in the UnJS/Nuxt ecosystem. pkgroll when you want your package.json exports to be the single source of truth. For 90% of npm libraries in 2026, tsup is the right choice.

Key Takeaways

  • tsup: 1.2M weekly downloads, esbuild-based (~50x faster builds than Rollup-based tools)
  • unbuild: 800K weekly downloads, Rollup-based (better tree-shaking output)
  • pkgroll: 100K weekly downloads, Rollup-based (package.json-driven, zero config files)
  • All three generate CJS + ESM output and .d.ts declaration files
  • Build speed: tsup wins by 5-10x; bundle quality: unbuild/pkgroll win for complex code
  • tsup is the default choice for the majority of new TypeScript packages

The Problem They All Solve

Publishing a TypeScript library requires:

  1. Transpilation: TypeScript → JavaScript
  2. Multiple formats: CommonJS (Node.js require) + ES Modules (import)
  3. Type declarations: .d.ts files for TypeScript consumers
  4. Source maps: For debugging
  5. External dependencies: Don't bundle react, zod, etc.
  6. Tree-shaking friendly output: Let consumers eliminate unused code

Without a dedicated tool, you'd need 50+ lines of Rollup or webpack config to handle all this correctly.

tsup

Package: tsup Weekly downloads: 1.2M GitHub stars: 10K Creator: EGOIST (also created Vite plugins) Underlying: esbuild

Installation

npm install -D tsup typescript

Minimal Configuration

// tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  clean: true,
});

package.json

{
  "name": "my-lib",
  "version": "1.0.0",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
      "require": { "types": "./dist/index.cjs.d.ts", "default": "./dist/index.cjs" }
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch",
    "type-check": "tsc --noEmit"
  }
}

Build Output

dist/
  index.js        # ES Module
  index.cjs       # CommonJS
  index.d.ts      # Types for ESM
  index.cjs.d.ts  # Types for CJS
  index.js.map    # Source map
  index.cjs.map   # Source map

Advanced Configuration

import { defineConfig } from 'tsup';

export default defineConfig({
  // Multiple entry points
  entry: {
    index: 'src/index.ts',
    cli: 'src/cli.ts',
    'utils/string': 'src/utils/string.ts',
  },

  format: ['cjs', 'esm'],
  dts: true,
  splitting: true,        // Code split between entry points
  sourcemap: true,
  clean: true,
  minify: false,          // Don't minify libraries (let consumers do it)

  // Don't bundle these
  external: ['react', 'react-dom', 'next'],

  // Node.js built-ins for Node libraries
  platform: 'node',

  // ES target
  target: 'es2022',

  // Add shims for __dirname, __filename in ESM
  shims: true,

  // Run after build
  onSuccess: 'node dist/cli.cjs --help',
});

DTS Mode Options

export default defineConfig({
  dts: true,                // Generate .d.ts via TypeScript compiler
  // OR:
  dts: 'only',              // Only generate .d.ts, don't bundle JS
});

tsup Limitations

  • esbuild tree-shaking is less aggressive than Rollup's — some dead code may remain in output
  • For complex barrel exports (export * from './utils'), output quality can be suboptimal
  • ESM/CJS interop can have edge cases with circular imports

unbuild

Package: unbuild Weekly downloads: 800K GitHub stars: 2.5K Creator: UnJS team (Sébastien Chopin, Pooya Parsa — Nuxt founders) Underlying: Rollup + MKDist

Installation

npm install -D unbuild typescript

Minimal Configuration

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

export default defineBuildConfig({
  entries: ['./src/index'],
  declaration: true,
  rollup: {
    emitCJS: true,
  },
});

Or no config file at all — unbuild infers from package.json:

// package.json — unbuild reads exports automatically
{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}
# Just run:
npx unbuild

Advanced Configuration

import { defineBuildConfig } from 'unbuild';

export default defineBuildConfig({
  entries: ['./src/index'],
  outDir: 'dist',
  declaration: true,
  clean: true,

  rollup: {
    emitCJS: true,
    cjsBridge: true,      // Better CJS/ESM interop

    // Advanced Rollup settings:
    alias: {
      '@utils': './src/utils',
    },

    resolve: {
      preferBuiltins: true,
    },

    esbuild: {
      minify: process.env.NODE_ENV === 'production',
      target: 'es2022',
    },
  },

  externals: ['react', 'vue', /^@nuxt/],
});

MKDist Mode

unbuild's unique feature: mkdist distributes TypeScript source files (with .d.ts alongside them), useful for libraries where source is the distribution:

import { defineBuildConfig } from 'unbuild';

export default defineBuildConfig({
  entries: [
    { input: './src/', builder: 'mkdist' }, // Dist source files
    { input: './src/index', builder: 'rollup' }, // Also bundle entry point
  ],
  declaration: true,
});

Why unbuild?

  • Superior Rollup tree-shaking produces cleaner output
  • mkdist for libraries that want to expose source files
  • Automatic configuration from package.json exports
  • Part of the UnJS ecosystem — if you're using Nuxt, Nitro, or H3, unbuild feels native

pkgroll

Package: pkgroll Weekly downloads: 100K GitHub stars: 1.5K Creator: Hiroki Osame Underlying: Rollup

Installation

npm install -D pkgroll typescript

The pkgroll Philosophy

pkgroll has no configuration file. It reads your package.json and builds exactly what the exports field describes:

{
  "name": "my-lib",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs"
    }
  },
  "scripts": {
    "build": "pkgroll",
    "dev": "pkgroll --watch"
  }
}
npx pkgroll
# Outputs:
# dist/index.js    (ESM)
# dist/index.cjs   (CJS)
# dist/index.d.ts  (types)
# dist/utils.js    (ESM)
# dist/utils.cjs   (CJS)
# dist/utils.d.ts  (types)

No config file. Just package.json.

pkgroll with Source Maps

{
  "scripts": {
    "build": "pkgroll --sourcemap"
  }
}

Minification

{
  "scripts": {
    "build": "pkgroll --minify"
  }
}

Why pkgroll?

  • Zero configuration files — package.json is the single source of truth
  • Rollup-based (excellent tree-shaking)
  • If your package.json exports change, builds automatically adapt
  • Minimal opinions beyond what package.json already defines

Head-to-Head Comparison

Build Speed (100 TypeScript files, ESM + CJS + .d.ts)

ToolBuild Time
tsup~0.8s
unbuild~4s
pkgroll~3.5s

tsup wins by 4-5x because esbuild is dramatically faster than Rollup.

Bundle Quality (complex library with barrel exports)

ToolOutput Quality
tsupGood
unbuildExcellent
pkgrollExcellent

Rollup's tree-shaking and module handling produce cleaner output for complex libraries.

Configuration Overhead

ToolConfig Required
tsuptsup.config.ts (optional, sensible defaults)
unbuildbuild.config.ts or package.json exports
pkgrollNone (package.json only)

Feature Matrix

Featuretsupunbuildpkgroll
CJS outputYesYesYes
ESM outputYesYesYes
.d.ts generationYesYesYes
Source mapsYesYesYes
Watch modeYesYesYes
Code splittingYesYesPartial
Multiple entriesYesYesVia exports
Banner/footerYesYesNo
Custom Rollup pluginsNoYesYes
mkdist (source dist)NoYesNo
MinificationOptionalOptionalOptional
Package.json drivenNoPartialFull

The Verdict

Default choice: tsup — fastest, most ergonomic, biggest community. Works perfectly for 90% of TypeScript libraries.

Choose unbuild when: You need mkdist, you're in the UnJS ecosystem, you need complex Rollup plugin integration, or Rollup's tree-shaking quality matters for your consumers.

Choose pkgroll when: You want no build config files and package.json as the sole source of truth. Simple, opinionated, Rollup-quality output.

The Right package.json Setup for Any of These Tools

{
  "name": "my-library",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  },
  "files": ["dist", "src"],
  "sideEffects": false
}

sideEffects: false enables tree-shaking by bundlers that consume your library.

Compare these packages on PkgPulse.

Comments

Stay Updated

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