|$ curl https://forge-ai.dev/api/markdown?path=docs/build-tools/tsup
$cat docs/tsup.md
updated Today·22-30 min read·published

tsup

Build ToolstsupLibrariesBeginner to Intermediate🎯Free Tools
Introduction

tsup is a zero-config TypeScript bundler powered by esbuild. It targets library authors who need fast builds, dual ESM/CJS output, and declaration files without standing up a full Rollup or Webpack pipeline. Under the hood it uses esbuild for JS emit and can generate .d.ts via a TypeScript/rollup-dts path.

Use tsup when you publish packages. Prefer Vite/Webpack/Rspack when you ship applications with HTML, CSS pipelines, and HMR.

info

tsup shines for SDK packages, shared UI kits (with peers externalized), CLI tools, and monorepo internal libraries that must rebuild quickly on watch.
Why tsup
NeedHow tsup helpsWhen not enough
Fast TS → JSesbuild transform + bundleNeed Babel plugin ecosystem
Dual package--format esm,cjsExotic dual-package hazards need careful exports
Types--dts / dts: trueComplex project references may need tsc alone
Zero configSensible defaults from entryHeavy CSS-in-JS app bundling
Install & First Build
install.sh
Bash
1npm install -D tsup typescript
2
3# One-shot build from CLI
4npx tsup src/index.ts --format esm,cjs --dts --clean
5
6# Watch during local development of a monorepo package
7npx tsup src/index.ts --format esm,cjs --dts --watch
package.json
JSON
1{
2 "name": "@acme/utils",
3 "version": "1.0.0",
4 "type": "module",
5 "main": "./dist/index.cjs",
6 "module": "./dist/index.js",
7 "types": "./dist/index.d.ts",
8 "exports": {
9 ".": {
10 "types": "./dist/index.d.ts",
11 "import": "./dist/index.js",
12 "require": "./dist/index.cjs"
13 }
14 },
15 "files": ["dist"],
16 "scripts": {
17 "build": "tsup",
18 "dev": "tsup --watch",
19 "typecheck": "tsc --noEmit"
20 },
21 "devDependencies": {
22 "tsup": "^8.0.0",
23 "typescript": "^5.0.0"
24 }
25}
Configuration

Prefer tsup.config.ts for typed options. Keep CLI flags for one-offs.

tsup.config.ts
TypeScript
1import { defineConfig } from "tsup";
2
3export default defineConfig({
4 entry: ["src/index.ts"],
5 format: ["esm", "cjs"],
6 dts: true,
7 sourcemap: true,
8 clean: true,
9 minify: false, // usually false for libraries — let consumers minify
10 target: "es2022",
11 splitting: false, // enable for multi-entry code-splitting libs
12 treeshake: true,
13 external: ["react", "react-dom"],
14 esbuildOptions(options) {
15 options.banner = {
16 js: '"use client";', // only if you intentionally ship RSC client markers
17 };
18 },
19});

warning

Only add "use client" banners when the package is meant for React Server Component boundaries. Most Node utilities should not include that banner.
Dual ESM / CJS

Dual packages remain common while ecosystems migrate. tsup emits both formats; you must wire exports correctly to avoid the dual-package hazard.

FieldPurposeNotes
exports["."].importESM consumersPoint at .js ESM build
exports["."].requireCJS consumersPoint at .cjs
exports["."].typesTypeScriptPut types condition first
type: "module"Default .js as ESMUse .cjs extension for require

danger

Avoid shipping the same file path for both import and require. Distinct files (ESM vs CJS) prevent Node from loading the wrong format and reduce dual-package hazard bugs.
DTS Generation

dts: true generates declaration files. For multi-entry packages, ensure each entry gets types. For stubborn cases, emit types with tsc --emitDeclarationOnly and let tsup handle JS only.

tsup.config.ts (dts strategies)
TypeScript
1import { defineConfig } from "tsup";
2
3// Strategy A — tsup owns dts
4export default defineConfig({
5 entry: { index: "src/index.ts", cli: "src/cli.ts" },
6 format: ["esm", "cjs"],
7 dts: true,
8});
9
10// Strategy B — tsc for types (run in parallel in package scripts)
11// "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly"
12

best practice

Always run tsc --noEmit in CI even when tsup emits JS. esbuild does not typecheck.
Watch Mode

In monorepos, run tsup watch in library packages while the app uses Vite/Next HMR against workspace sources or built dist — depending on whether you resolve to source via package exports.

monorepo-dev.sh
Bash
1# From repo root with Turborepo
2turbo run dev --filter=@acme/utils --filter=@acme/web
3
4# Package scripts
5# utils: "dev": "tsup --watch"
6# web: "dev": "vite"
7
8# Tip: point the app at source during local dev via package.json exports
9# conditions, or use a tool like vitest/vite that can resolve TS sources.
Externals & Peer Dependencies

By default tsup does not bundle dependencies listed in dependencies / peerDependencies (behavior can be configured). Explicitly externalize peers for UI libraries.

tsup.config.ts (externals)
TypeScript
1import { defineConfig } from "tsup";
2
3export default defineConfig({
4 entry: ["src/index.ts"],
5 format: ["esm", "cjs"],
6 dts: true,
7 external: [
8 "react",
9 "react-dom",
10 "react/jsx-runtime",
11 /^@acme\//, // keep workspace packages external if consumers resolve them
12 ],
13 noExternal: [], // force-bundle specific packages if needed
14});
Dependency typeUsuallyWhy
peerDependenciesExternalAvoid duplicate React / framework copies
tiny helpersBundle or dependFewer install surprises vs larger install graph
Node builtinsExternal / platform markDo not polyfill into browser builds accidentally
Multi-Entry Packages
tsup.config.ts (multi-entry)
TypeScript
1import { defineConfig } from "tsup";
2
3export default defineConfig({
4 entry: {
5 index: "src/index.ts",
6 server: "src/server.ts",
7 "plugins/vite": "src/plugins/vite.ts",
8 },
9 format: ["esm", "cjs"],
10 dts: true,
11 splitting: true,
12 clean: true,
13});

Mirror entries in package.json exports so consumers can import @acme/utils/server without deep relative paths.

esbuild Limits (Inherited)
  • No typechecking — pair with tsc.
  • Limited transform plugins vs Babel — avoid exotic syntax transforms.
  • Some TS features (const enums, namespaces) need care — prefer erasable syntax.
  • CSS handling is basic compared to Vite/Webpack app pipelines.
📝

note

If you need Rollup's plugin ecosystem for complex library packaging (advanced preserveModules layouts), use Rollup or unbuild instead of forcing tsup.
tsup vs Rollup vs Vite Library Mode
ToolBest forTradeoff
tsupFast zero-config TS libsLess flexible than Rollup plugins
RollupFine-grained ESM output controlMore config surface
Vite lib modeLibs that share Vite plugins/CSSHeavier than tsup for simple packages
unbuildNuxt/unjs-style packagesDifferent conventions
Publishing Checklist
publish-checklist.sh
Bash
1# 1. Build clean artifacts
2npm run build
3
4# 2. Typecheck separately
5npx tsc --noEmit
6
7# 3. Pack and inspect contents
8npm pack --dry-run
9# Ensure only dist + README + LICENSE; no src secrets
10
11# 4. Smoke test dual imports
12node -e "console.log(require('./dist/index.cjs'))"
13node --input-type=module -e "import('./dist/index.js').then(console.log)"
14
15# 5. Publish
16npm publish --access public

best practice

Set "sideEffects": falsewhen true for your package so consumers' bundlers can tree-shake more aggressively. Be honest — wrong values break CSS imports.
$Blueprint — Engineering Documentation·Section ID: BT-TSUP-01·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.