tsup vs unbuild vs pkgroll: TypeScript Library Bundling 2026
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.tsdeclaration 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:
- Transpilation: TypeScript → JavaScript
- Multiple formats: CommonJS (Node.js
require) + ES Modules (import) - Type declarations:
.d.tsfiles for TypeScript consumers - Source maps: For debugging
- External dependencies: Don't bundle
react,zod, etc. - 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
mkdistfor 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.jsonis 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)
| Tool | Build 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)
| Tool | Output Quality |
|---|---|
| tsup | Good |
| unbuild | Excellent |
| pkgroll | Excellent |
Rollup's tree-shaking and module handling produce cleaner output for complex libraries.
Configuration Overhead
| Tool | Config Required |
|---|---|
| tsup | tsup.config.ts (optional, sensible defaults) |
| unbuild | build.config.ts or package.json exports |
| pkgroll | None (package.json only) |
Feature Matrix
| Feature | tsup | unbuild | pkgroll |
|---|---|---|---|
| CJS output | Yes | Yes | Yes |
| ESM output | Yes | Yes | Yes |
| .d.ts generation | Yes | Yes | Yes |
| Source maps | Yes | Yes | Yes |
| Watch mode | Yes | Yes | Yes |
| Code splitting | Yes | Yes | Partial |
| Multiple entries | Yes | Yes | Via exports |
| Banner/footer | Yes | Yes | No |
| Custom Rollup plugins | No | Yes | Yes |
| mkdist (source dist) | No | Yes | No |
| Minification | Optional | Optional | Optional |
| Package.json driven | No | Partial | Full |
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.
See the live comparison
View tsup vs. unbuild vs pkgroll on PkgPulse →