Skip to main content

Best TypeScript-First Build Tools 2026

·PkgPulse Team

TL;DR

tsup is the default for TypeScript library bundling in 2026 — zero config, esbuild-powered, 3M weekly downloads. unbuild (from the Nuxt/UnJS team) is the best choice for monorepos and cross-platform packages. pkgroll is the newest, fastest option with Rollup under the hood. If you're publishing an npm package today: tsup gets you to done fastest. If you need advanced export map control or cross-platform compatibility: unbuild.

Key Takeaways

  • tsup: ~3M downloads/week, zero config, esbuild-powered, CommonJS + ESM output
  • unbuild: ~500K downloads/week, UnJS ecosystem, advanced export maps, stub mode
  • pkgroll: ~80K downloads/week, Rollup-based, fastest builds, newer
  • esbuild (direct): 25M downloads/week — the underlying engine, use it via tsup usually
  • For most libraries: tsup (simplest DX, proven, extensive docs)
  • For monorepo packages: unbuild (stub mode = instant dev reloads)

Downloads

PackageWeekly DownloadsTrend
esbuild~25M↑ Growing
tsup~3M↑ Growing
unbuild~500K↑ Growing
pkgroll~80K↑ Growing

tsup: The Standard

npm install --save-dev tsup typescript
// tsup.config.ts — zero config or full config:
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],  // Or multiple entries
  format: ['cjs', 'esm'],   // Output both formats
  dts: true,                 // Generate .d.ts files
  splitting: false,           // No code splitting for libraries
  sourcemap: true,
  clean: true,               // Clean dist before build
  
  // Tree-shaking friendly:
  treeshake: true,
  
  // For bundling vs externalizing:
  external: ['react', 'react-dom'],  // Don't bundle peer deps
  noExternal: ['my-util'],           // Force bundle this dep
  
  // Multiple entry points with custom output names:
  // entry: {
  //   index: 'src/index.ts',
  //   cli: 'src/cli.ts',
  // },
});
// package.json for the published library:
{
  "name": "my-library",
  "version": "1.0.0",
  "main": "./dist/index.js",           // CJS
  "module": "./dist/index.mjs",         // ESM
  "types": "./dist/index.d.ts",         // Types
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch"
  }
}

tsup Watch Mode

# Rebuild on file changes (good for library development):
npx tsup --watch

# Or with type checking:
npx tsup --watch & npx tsc --watch --noEmit

unbuild: Monorepo Powerhouse

unbuild's killer feature: stub mode — instead of building, it creates a thin shim that points directly to your TypeScript source. Zero rebuild time during development.

npm install --save-dev unbuild
// build.config.ts:
import { defineBuildConfig } from 'unbuild';

export default defineBuildConfig({
  entries: ['src/index'],
  declaration: true,  // Generate .d.ts
  
  rollup: {
    emitCJS: true,      // Output CJS
    esbuild: {
      minify: false,    // Keep readable in dev
      target: 'es2020',
    },
  },
  
  // External packages (don't bundle):
  externals: ['react', 'vue', 'rollup'],
  
  // For monorepo: auto-detect and external workspace packages
  // failOnWarn: true,  // Fail CI on warnings
});

Stub Mode (unbuild's Secret Weapon)

# Instead of building, creates a stub that points to source:
npx unbuild --stub

# This creates dist/index.mjs that just re-exports from src/:
# export * from '../src/index.ts'
# (TypeScript is executed directly via tsx)
// Monorepo package.json workflow:
{
  "scripts": {
    "dev": "unbuild --stub",    // Instant — no rebuild on changes
    "build": "unbuild",          // Real build for publishing
    "prepack": "unbuild"         // Build before npm publish
  }
}

In a Turborepo: pnpm dev runs unbuild --stub in all packages simultaneously, then app packages can import them directly without waiting for builds.


pkgroll: Rollup-Powered Speed

npm install --save-dev pkgroll
// package.json — pkgroll reads exports map and builds accordingly:
{
  "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"
    }
  }
}

pkgroll reads your package.json exports map and builds whatever outputs you declare. Zero separate config file needed.


esbuild Direct (For Maximum Control)

// build.ts — direct esbuild API:
import esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  format: 'esm',
  outfile: 'dist/index.mjs',
  external: ['react', 'react-dom'],
  target: 'es2020',
  sourcemap: true,
  treeShaking: true,
  minify: process.env.NODE_ENV === 'production',
  define: {
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'development'),
  },
});

// Run for CJS:
await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  format: 'cjs',
  outfile: 'dist/index.js',
  external: ['react', 'react-dom'],
});

// Generate declarations separately (esbuild doesn't emit .d.ts):
import { execSync } from 'child_process';
execSync('tsc --emitDeclarationOnly --outDir dist');

esbuild doesn't generate TypeScript declarations — you need a separate tsc --emitDeclarationOnly step or use tsup which handles this.


Comparison Table

tsupunbuildpkgrollesbuild
Config neededMinimalMinimalZero (reads package.json)JavaScript API
Declaration (.d.ts)✅ via dts❌ (manual tsc)
Stub mode
Underlying engineesbuildRollup+esbuildRollupesbuild
SpeedFastMediumFastFastest
EcosystemLargeUnJSSmallHuge (lower-level)
Best forMost librariesMonoreposModern packagesCustom build scripts

Decision Guide

Use tsup if:
  → Publishing a new npm package (best DX, most docs)
  → Need CJS + ESM + declarations in one command
  → Team is familiar with esbuild ecosystem
  → Simple to moderate library requirements

Use unbuild if:
  → Monorepo where packages depend on each other
  → Need stub mode for instant dev turnaround
  → UnJS ecosystem (Nitro, Nuxt, H3, etc.)
  → Advanced export map customization

Use pkgroll if:
  → Want zero config (just describe exports in package.json)
  → Modern pure-ESM package
  → Simpler library with straightforward exports

Use esbuild directly if:
  → Application builds (not library)
  → Need programmatic control over build steps
  → Building as part of a larger custom pipeline

Compare tsup, unbuild, and esbuild package health on PkgPulse.

Comments

Stay Updated

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