tsup vs unbuild vs pkgroll: TypeScript Bundlers 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.
Ecosystem & Community
The TypeScript library bundling space has matured considerably. tsup dominates in raw download numbers and community mindshare — its GitHub issues are active, community guides are abundant, and it has become the de facto recommendation in the "how to publish a TypeScript library" tutorials that proliferate on Dev.to, Medium, and YouTube. When developers encounter a problem with tsup, the chance of finding a Stack Overflow answer or a GitHub issue with a solution is high.
The UnJS ecosystem has become a meaningful cluster within the JavaScript open-source world. Nuxt's move to UnJS as its foundation brought unbuild along for the ride — if you browse the devDependencies of Nitro, h3, ofetch, or defu, you'll find unbuild. This creates an interesting situation where unbuild is widely used in high-quality, high-traffic open-source packages despite lower raw download numbers than tsup. Quality of adoption matters, not just quantity.
Pkgroll is smaller but has a dedicated following among developers who value the philosophy of letting package.json be the single source of truth. It avoids the cognitive overhead of maintaining a separate build config file, which aligns well with minimalist library authors who want to reduce their toolchain surface area. The creator, Hiroki Osame, is a prolific npm package author with a track record of building focused, high-quality tools.
Real-World Adoption
tsup is used by thousands of open-source packages including popular tools like drizzle-orm, various Radix UI packages, and a significant portion of the modern React ecosystem. Its combination of speed and sensible defaults has made it the first tool many TypeScript library authors reach for when starting a new package.
unbuild powers the entire UnJS monorepo, which includes packages like unenv, consola, pathe, mlly, and c12 — packages with hundreds of thousands of weekly downloads that power Nuxt and Nitro. When you use Nuxt in 2026, you are indirectly running code that was built with unbuild. This production provenance is significant.
Several major component library authors have experimented with all three tools and tend to land on tsup for smaller utilities and unbuild for complex multi-package monorepos where the mkdist feature and fine-grained Rollup control matter more. The shadcn/ui ecosystem packages use tsup. The UnJS ecosystem uses unbuild. Most individual library authors use tsup unless they have a specific reason not to.
Developer Experience Deep Dive
TypeScript support is first-class in all three tools, but the TypeScript integration story differs. tsup uses esbuild for transpilation and the TypeScript compiler only for type declarations — this means your build is fast but your type declarations may occasionally diverge slightly from what TypeScript would produce with full compilation. For most libraries this is a non-issue, but for highly complex generic types, running tsc --noEmit separately to verify types remains important.
unbuild and pkgroll both use rollup-plugin-dts for declaration generation, which produces accurate TypeScript declarations for complex types including conditional types, mapped types, and deeply generic interfaces. If you're publishing a library with a complex TypeScript API surface, the declaration file quality difference can matter for your users.
Watch mode is essential during development, and all three tools support it. tsup's watch mode is fastest due to esbuild's incremental compilation. unbuild's watch mode works well but is slower to pick up changes. pkgroll's watch mode is similarly Rollup-speed.
The ergonomics of updating your configuration over time is worth considering. tsup's config file is explicit and easy to modify. unbuild's ability to infer from package.json exports means updating your public API surface automatically updates the build targets. pkgroll takes this furthest — there is no config file to forget to update.
Performance & Benchmarks
Build performance matters most in two contexts: local development iteration speed and CI build time. For local development, tsup's esbuild foundation is a clear winner. A medium-sized library (50-100 TypeScript source files) builds in under one second with tsup versus three to five seconds with unbuild or pkgroll. Over hundreds of iterative builds per day, this adds up.
For CI, the difference is less dramatic because build time is typically a small fraction of the total CI pipeline. A five-second build versus a one-second build rarely changes whether a CI run completes in 3 minutes or 7 minutes when you factor in dependency installation, type checking, and test execution.
Bundle output quality matters more for large libraries with complex internal module graphs. esbuild's tree-shaking, while good, can leave unused re-exports in the final bundle. Rollup's tree-shaking (used by unbuild and pkgroll) is more aggressive at eliminating dead code paths. For a library with 50 exported functions where consumers typically use 5-10, this can meaningfully reduce the bundle size impact on consumers.
Migration Guide
Migrating from a raw Rollup config to tsup:
This is the most common migration path. tsup replaces your entire rollup.config.js with a few lines of tsup.config.ts. The main consideration is ensuring your external dependencies list is correct — tsup won't automatically mark peer dependencies as external in all configurations. Explicitly setting external for packages you don't want bundled is the most important step.
Migrating from tsup to unbuild:
The primary reason to migrate is the need for custom Rollup plugins (tsup doesn't support these) or the mkdist distribution model. The migration involves replacing tsup.config.ts with build.config.ts using unbuild's defineBuildConfig and updating your package.json build script.
Migrating from any tool to pkgroll:
This is the simplest migration conceptually. Delete your build config file, install pkgroll, verify your package.json exports are correctly structured, and replace your build script with pkgroll. The constraint is that pkgroll requires your desired outputs to be expressed through package.json exports, so if your build config has non-standard outputs, you'll need to reflect those in your exports map first.
Final Verdict 2026
For most TypeScript library authors in 2026, tsup is still the right answer. It's fast, well-documented, widely adopted, and the zero-to-published-package experience is the best of the three options. The esbuild limitations only matter at the margins of library authoring.
If you're building for the UnJS/Nuxt ecosystem, or if tree-shaking output quality is a requirement (you're building a large framework-level library), reach for unbuild instead. Its deeper Rollup integration and mkdist capability solve real problems that tsup can't address.
Pkgroll is worth reaching for if you have a minimalist streak and find build config files annoying. The package.json-as-configuration philosophy has genuine merit, and the Rollup-quality output is excellent.
Compare Tsup, Unbuild, and Pkgroll package health on PkgPulse.
Related: Best Monorepo Tools 2026, Best JavaScript Testing Frameworks 2026, Best Next.js Auth Solutions 2026