TypeScript 與建構工具整合:深入了解 tsup 與 esbuild
簡介
在現代前端與 Node.js 專案中,TypeScript 已成為撰寫可維護、型別安全程式碼的首選語言。然而,僅有 TypeScript 編譯器 (tsc) 並不足以滿足所有建置需求,尤其是當我們需要 快速打包、Tree‑shaking、或 多格式輸出 時,整合更高效的建構工具就變得相當重要。
tsup 是一個以 esbuild 為核心的零設定打包工具,提供了 極速編譯、自動產生 type declaration、以及支援 ESM / CJS / UMD 多種模組格式的功能。本文將從概念說明、實作範例,到常見陷阱與最佳實踐,完整介紹如何在 TypeScript 專案中結合 tsup 與 esbuild,讓你的套件或應用程式在開發與發佈時都能保持高效與一致。
核心概念
1. 為什麼不直接使用 tsc?
| 項目 | tsc |
esbuild / tsup |
|---|---|---|
| 編譯速度 | 中等(依賴 TypeScript 內部的 type‑checker) | 極速(使用 Go 實作的編譯器) |
| 打包功能 | 需要額外工具(如 rollup、webpack) |
內建 bundle、minify、tree‑shaking |
| 多格式輸出 | 需要手動配置 outDir、module 等 |
一行指令即可同時產出 CJS、ESM、UMD |
| 開發體驗 | 需要自行設定 watch、source‑map 等 | 開箱即用 的 watch、sourcemap、dts 產生 |
結論:在需要快速迭代、同時支援多種發佈格式的情境下,
tsup以最小的配置成本取代傳統的tsc + bundler流程。
2. tsup 的工作原理
- 解析入口檔案:接受
entryPoints(如src/index.ts),由esbuild解析依賴圖。 - 型別檢查:預設會呼叫
tsc --noEmit進行型別檢查,確保編譯安全。 - 打包與轉譯:
esbuild同時完成 TypeScript → JavaScript 的轉譯,以及模組合併(bundle)。 - 產出多格式:根據
format設定,同時生成cjs、esm、umd檔案。 - 自動產生
.d.ts:透過tsc或dts-bundle-generator產出型別宣告檔。
3. 安裝與基本設定
# 使用 npm
npm i -D tsup
# 或使用 pnpm / yarn
pnpm i -D tsup
# yarn add -D tsup
在 package.json 中加入簡易指令:
{
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --watch --format cjs,esm"
}
}
提示:
--dts會自動產生dist/*.d.ts,省去手動跑tsc的步驟。
4. esbuild 直接使用的情境
雖然 tsup 已封裝了大部分需求,但有時候我們只想要 一次性編譯、或在 自訂腳本 中使用 esbuild API。以下示範如何在 Node.js 腳本中呼叫 esbuild:
// build.js
const { build } = require('esbuild');
build({
entryPoints: ['src/index.ts'],
bundle: true,
platform: 'node', // target: node (commonjs)
format: 'cjs',
outfile: 'dist/index.cjs.js',
sourcemap: true,
minify: false,
target: ['node14'], // 指定 Node 版本
// 產生型別宣告需要額外跑 tsc,這裡僅示範編譯
}).catch(() => process.exit(1));
執行:
node build.js
程式碼範例
以下提供 5 個實用範例,展示在不同需求下如何結合 tsup 與 esbuild。
範例 1:最小化的 CLI 工具
src/cli.ts
#!/usr/bin/env node
import { program } from 'commander';
import { greet } from './greet';
program
.name('hello')
.description('A simple greeting CLI')
.argument('<name>', 'who to greet')
.action((name: string) => {
console.log(greet(name));
});
program.parse();
src/greet.ts
export function greet(name: string): string {
return `👋 Hello, ${name}!`;
}
package.json 設定:
{
"bin": {
"hello": "dist/cli.cjs.js"
},
"scripts": {
"build": "tsup src/cli.ts --format cjs --minify --dts"
}
}
重點:
tsup會自動保留 shebang (#!/usr/bin/env node) 並產出可直接執行的cjs檔案。
範例 2:同時輸出 CJS、ESM 與 UMD
{
"scripts": {
"build": "tsup src/index.ts --format cjs,esm,umd --global-name MyLib --dts"
}
}
src/index.ts
export * from './utils';
export { default as version } from './version';
產出結構:
dist/
├─ index.cjs.js
├─ index.esm.js
├─ index.umd.js
└─ index.d.ts
說明:
--global-name只在 UMD 模式下有效,會把套件掛載至全域變數MyLib。
範例 3:使用 esbuild 插件(如 esbuild-plugin-alias)
先安裝插件:
npm i -D esbuild-plugin-alias
build.js
const { build } = require('esbuild');
const alias = require('esbuild-plugin-alias');
build({
entryPoints: ['src/main.ts'],
bundle: true,
platform: 'node',
outfile: 'dist/main.cjs.js',
plugins: [
alias({
'@/utils': require('path').resolve(__dirname, 'src/utils')
})
]
}).catch(() => process.exit(1));
在程式碼中使用別名:
// src/main.ts
import { foo } from '@/utils/foo';
console.log(foo());
技巧:使用別名可以讓專案結構更清晰,且
esbuild插件的載入方式與webpack類似,學習成本低。
範例 4:在 tsup 中啟用自訂 esbuild 選項
tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
sourcemap: true,
esbuildOptions(options) {
// 針對所有輸出加入 define
options.define = {
__VERSION__: JSON.stringify('1.0.0')
};
// 設定目標環境
options.target = ['es2018'];
},
});
在程式碼中使用:
export const version = __VERSION__;
說明:
esbuildOptions讓我們在tsup內部直接調整底層的esbuild設定,彈性更高。
範例 5:結合 watch 與自動重啟(開發時的 Hot Reload)
{
"scripts": {
"dev": "tsup src/index.ts --watch --format cjs,esm && nodemon dist/index.cjs.js"
}
}
流程:
tsup監看檔案變動並重新編譯,nodemon監看編譯後的檔案自動重啟程式,形成簡易的 Hot Reload 開發環境。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 型別檔未產生 | 只執行 tsup 而忘記加 --dts,導致套件發佈缺少 .d.ts。 |
在 package.json 或 tsup.config.ts 中明確加入 dts: true。 |
| Node 目標版本不匹配 | esbuild 預設編譯至 ES2020,若套件需支援舊版 Node,會出現語法錯誤。 |
設定 target: ['node12'] 或使用 tsup --target node12。 |
| 外部依賴被打包 | 未將 react、lodash 等大型依賴標記為 external,導致 bundle 體積暴增。 |
使用 external: ['react', 'lodash'] 或在 tsup.config.ts 中 noExternal: [] 反向設定。 |
| Shebang 被移除 | 某些壓縮工具會刪除檔案開頭的 #!/usr/bin/env node。 |
確認 esbuild 的 banner 設定或直接在 tsup 中使用 --banner。 |
| Source Map 路徑錯亂 | 在 monorepo 中使用相對路徑,產生的 source map 位置不正確。 | 使用 outDir 統一輸出目錄,並在 tsup 加上 sourcemap: true。 |
最佳實踐
- 保持零設定:先以
tsup src/index.ts --format cjs,esm --dts為起點,確保最基本的編譯與型別檔產出。 - 分離開發與發佈腳本:
npm run dev用--watch,npm run build用--minify、--sourcemap,避免混用。 - 明確聲明 external:對於 peerDependencies(如 React)務必在
tsup中排除,避免重複打包。 - 版本管理:使用
define注入__VERSION__,讓套件在執行時可以取得正確版本號。 - CI/CD 整合:在 CI pipeline 中加入
tsup --no-clean,確保每次建置都從乾淨環境開始,避免殘留檔案影響結果。
實際應用場景
| 場景 | 為何選擇 tsup / esbuild |
|---|---|
| NPM 套件發佈 | 需要同時輸出 CJS、ESM、UMD,且必須提供 .d.ts。tsup 一行指令即可完成。 |
| Serverless Functions | 需要 極速編譯 與 最小化 的輸出檔案,esbuild 的打包速度可在 CI 中大幅縮短部署時間。 |
| 微前端子應用 | 每個子應用都獨立打包,使用 tsup 的 --external 排除共享依賴,減少重複下載。 |
| CLI 工具 | 需要保留 shebang、產出單一可執行檔,tsup 內建支援,省去額外腳本。 |
| 快速原型 | 開發階段只需 tsup --watch,即時看到變更,無需額外設定 webpack。 |
總結
- tsup 透過 esbuild 的高速編譯與打包能力,提供了「零設定、即時編譯、完整型別支援」的完整解決方案。
- 在需要 多模組格式、快速開發迭代、或 最小化發佈檔案 的情境下,
tsup是比傳統tsc + webpack/rollup更佳的選擇。 - 若需要更細緻的控制(如自訂插件、別名、或特定的編譯選項),可以直接使用
esbuildAPI,或在tsup.config.ts中透過esbuildOptions進行調整。
透過本文的概念說明與實作範例,你現在應該能夠:
- 快速上手
tsup,在npm run build中完成 TypeScript 到多格式 JavaScript 的全自動轉換。 - 自訂
esbuild設定,滿足特殊需求(別名、外部依賴、全域變數注入)。 - 避免常見陷阱,並遵循最佳實踐,讓你的套件在發佈與維護上更加穩定。
祝你在 TypeScript 專案中玩得開心,並以 tsup + esbuild 為基礎,打造出更快、更小、更可靠的 JavaScript 產出! 🚀