本文 AI 產出,尚未審核

TypeScript 與建構工具整合:深入了解 tsupesbuild


簡介

在現代前端與 Node.js 專案中,TypeScript 已成為撰寫可維護、型別安全程式碼的首選語言。然而,僅有 TypeScript 編譯器 (tsc) 並不足以滿足所有建置需求,尤其是當我們需要 快速打包、Tree‑shaking、或 多格式輸出 時,整合更高效的建構工具就變得相當重要。

tsup 是一個以 esbuild 為核心的零設定打包工具,提供了 極速編譯、自動產生 type declaration、以及支援 ESM / CJS / UMD 多種模組格式的功能。本文將從概念說明、實作範例,到常見陷阱與最佳實踐,完整介紹如何在 TypeScript 專案中結合 tsupesbuild,讓你的套件或應用程式在開發與發佈時都能保持高效與一致。


核心概念

1. 為什麼不直接使用 tsc

項目 tsc esbuild / tsup
編譯速度 中等(依賴 TypeScript 內部的 type‑checker) 極速(使用 Go 實作的編譯器)
打包功能 需要額外工具(如 rollupwebpack 內建 bundleminifytree‑shaking
多格式輸出 需要手動配置 outDirmodule 一行指令即可同時產出 CJS、ESM、UMD
開發體驗 需要自行設定 watch、source‑map 等 開箱即用watchsourcemapdts 產生

結論:在需要快速迭代、同時支援多種發佈格式的情境下,tsup 以最小的配置成本取代傳統的 tsc + bundler 流程。

2. tsup 的工作原理

  1. 解析入口檔案:接受 entryPoints(如 src/index.ts),由 esbuild 解析依賴圖。
  2. 型別檢查:預設會呼叫 tsc --noEmit 進行型別檢查,確保編譯安全。
  3. 打包與轉譯esbuild 同時完成 TypeScript → JavaScript 的轉譯,以及模組合併(bundle)。
  4. 產出多格式:根據 format 設定,同時生成 cjsesmumd 檔案。
  5. 自動產生 .d.ts:透過 tscdts-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 個實用範例,展示在不同需求下如何結合 tsupesbuild

範例 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.jsontsup.config.ts 中明確加入 dts: true
Node 目標版本不匹配 esbuild 預設編譯至 ES2020,若套件需支援舊版 Node,會出現語法錯誤。 設定 target: ['node12'] 或使用 tsup --target node12
外部依賴被打包 未將 reactlodash 等大型依賴標記為 external,導致 bundle 體積暴增。 使用 external: ['react', 'lodash'] 或在 tsup.config.tsnoExternal: [] 反向設定。
Shebang 被移除 某些壓縮工具會刪除檔案開頭的 #!/usr/bin/env node 確認 esbuildbanner 設定或直接在 tsup 中使用 --banner
Source Map 路徑錯亂 在 monorepo 中使用相對路徑,產生的 source map 位置不正確。 使用 outDir 統一輸出目錄,並在 tsup 加上 sourcemap: true

最佳實踐

  1. 保持零設定:先以 tsup src/index.ts --format cjs,esm --dts 為起點,確保最基本的編譯與型別檔產出。
  2. 分離開發與發佈腳本npm run dev--watchnpm run build--minify--sourcemap,避免混用。
  3. 明確聲明 external:對於 peerDependencies(如 React)務必在 tsup 中排除,避免重複打包。
  4. 版本管理:使用 define 注入 __VERSION__,讓套件在執行時可以取得正確版本號。
  5. CI/CD 整合:在 CI pipeline 中加入 tsup --no-clean,確保每次建置都從乾淨環境開始,避免殘留檔案影響結果。

實際應用場景

場景 為何選擇 tsup / esbuild
NPM 套件發佈 需要同時輸出 CJS、ESM、UMD,且必須提供 .d.tstsup 一行指令即可完成。
Serverless Functions 需要 極速編譯最小化 的輸出檔案,esbuild 的打包速度可在 CI 中大幅縮短部署時間。
微前端子應用 每個子應用都獨立打包,使用 tsup--external 排除共享依賴,減少重複下載。
CLI 工具 需要保留 shebang、產出單一可執行檔,tsup 內建支援,省去額外腳本。
快速原型 開發階段只需 tsup --watch,即時看到變更,無需額外設定 webpack。

總結

  • tsup 透過 esbuild 的高速編譯與打包能力,提供了「零設定、即時編譯、完整型別支援」的完整解決方案。
  • 在需要 多模組格式快速開發迭代、或 最小化發佈檔案 的情境下,tsup 是比傳統 tsc + webpack/rollup 更佳的選擇。
  • 若需要更細緻的控制(如自訂插件、別名、或特定的編譯選項),可以直接使用 esbuild API,或在 tsup.config.ts 中透過 esbuildOptions 進行調整。

透過本文的概念說明與實作範例,你現在應該能夠:

  1. 快速上手 tsup,在 npm run build 中完成 TypeScript 到多格式 JavaScript 的全自動轉換。
  2. 自訂 esbuild 設定,滿足特殊需求(別名、外部依賴、全域變數注入)。
  3. 避免常見陷阱,並遵循最佳實踐,讓你的套件在發佈與維護上更加穩定。

祝你在 TypeScript 專案中玩得開心,並以 tsup + esbuild 為基礎,打造出更快、更小、更可靠的 JavaScript 產出! 🚀