iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0

day27_title

前言

隨著專案規模成長,越來越多團隊選擇將前端、後端、共用套件放進同一個 repository 管理,
也就是所謂的 Monorepo
過去大家熟悉的組合可能是 pnpm workspaces + Turborepo,或是 Yarn workspaces + Lerna
但自從 Bun 把套件管理員這塊做得又快又完整之後,越來越多團隊開始評估「能不能直接用 Bun 來管 Monorepo?」

今天這篇要來聊聊 Bun Workspaces 的實際用法、常見的專案結構,以及幾個上線前一定要注意的最佳實踐。


為什麼考慮用 Bun 管理 Monorepo?

以官方展示的資料來說,Bun 安裝 Remix 這種規模的 Monorepo 大約只要 500ms,
比 npm 快 28 倍、比 Yarn v1 快 12 倍、比 pnpm 快 8 倍

(雖然最近 bun 到 1.4.0 版了 所以這數據有可能過時)

除了速度之外,Bun Workspaces 還有幾個吸引人的特點:

  • 文字格式 lockfile(bun.lock:diff 起來乾淨易讀,在 PR review 時很好對照
  • 原生支援 workspace: 協議:套件間互相依賴不需要額外設定
  • 內建 filter 機制:可以針對特定套件安裝或執行指令
  • 透過 bunfig.toml 做細部設定:不需要額外的 monorepo 工具就能滿足基本需求

不過要先說清楚一個常見誤解:Bun workspaces 本身不是完整的 Monorepo 工具
它負責的是套件安裝與本地連結,但不會做 task 快取,
也不會偵測某次 commit 影響了哪些套件——這代表如果你只用 Bun,
CI 每次都會重新跑過所有套件的任務,不管改動有多小。如果專案規模變大,
通常還是需要在 Bun 之上疊加 Turborepo 或 Nx 這類工具來做任務編排與快取。

基本專案結構

一個典型的 Bun Monorepo 長這樣:

<root>
├── bun.lock
├── package.json
├── tsconfig.json
└── packages
    ├── pkg-a
    │   ├── index.ts
    │   └── package.json
    ├── pkg-b
    │   ├── index.ts
    │   └── package.json
    └── pkg-c
        ├── index.ts
        └── package.json

根目錄的 package.jsonworkspaces 欄位定義 glob pattern:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["packages/*"]
}

幾個實務上的重點:

  1. 根目錄 package.json 不要放任何 dependencies:只用來定義 workspace 範圍與共用開發腳本,避免依賴關係混亂
  2. 一定要加上 "private": true:這是慣例,用來避免不小心把根目錄套件發布到 npm
  3. 每個子套件都要有自己獨立的 package.json:明確宣告自己的依賴,不要依賴 hoist 上去的隱性套件

如果你的專案同時有 apps(可執行的應用程式)跟 packages(共用邏輯),也可以拆成兩個目錄:

├── apps/
│   ├── web/
│   ├── api/
│   └── admin/
├── packages/
│   ├── ui/
│   ├── utils/
│   └── config/
└── package.json

Bun 的 workspaces 欄位也支援 negative pattern,可以用 ! 排除特定目錄,
例如 ["packages/*", "!packages/legacy"]

套件間的相互依賴:workspace: 協議

要讓 monorepo 裡的套件互相引用,在 package.json 的 dependencies 裡指定 workspace: 開頭的版本:

{
  "name": "@myorg/frontend",
  "dependencies": {
    "@myorg/shared": "workspace:*"
  }
}

Bun 支援三種寫法:

  • workspace:* — 永遠指向本地最新版本
  • workspace:^ — 發布時會轉換成 ^ 語義化版本範圍
  • workspace:~ — 發布時會轉換成 ~ 語義化版本範圍

值得注意的是,發布套件到 npm 時,Bun 會自動把 workspace: 替換成實際的版本號,
例如 workspace:* 會變成 1.0.1,這讓本地開發與正式發布可以無縫切換,不需要手動改版本字串。

加完依賴後,記得在專案根目錄執行一次 bun install,才會把本地套件連結(symlink)起來並更新 lockfile。

安裝與過濾指令

全域安裝

# 在根目錄安裝所有 workspace 的依賴
bun install

# CI 環境建議使用 frozen lockfile,避免意外更新版本
bun install --frozen-lockfile

只安裝或操作特定套件

大型 monorepo 常常不需要每次都動到全部套件,這時候 --filter 就很好用:

# 安裝所有以 pkg- 開頭、但排除 pkg-c 的套件
bun install --filter "pkg-*" --filter "!pkg-c"

# 也可以用路徑寫法
bun install --filter "./packages/pkg-*" --filter "!pkg-c"

針對單一 workspace 新增依賴

cd packages/backend
bun add express

# 或從根目錄直接指定 workspace
bun add lodash --filter "@myorg/shared"

Bun 會自動偵測你目前在哪個 workspace 底下,把依賴加進對應套件的 package.json,同時更新根目錄的 lockfile。


共用依賴版本:Catalogs

多套件專案最常遇到的痛點之一,就是同一個依賴(例如 reacttypescript
在不同套件裡版本不一致,導致重複安裝或行為不一致。Bun 提供 Catalogs 機制來解決這個問題:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["packages/*"],
  "catalog": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "typescript": "^5.0.0",
    "zod": "^3.22.0"
  }
}

各子套件只要引用 catalog: 即可:

{
  "name": "@myorg/frontend",
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:",
    "zod": "catalog:"
  }
}

如果需要針對不同用途(例如測試工具)維護不同的版本集合,也可以定義多組 catalog:

{
  "catalogs": {
    "default": { "typescript": "^5.0.0" },
    "testing": {
      "vitest": "^1.0.0",
      "playwright": "^1.40.0"
    }
  }
}

這個機制的好處很直接:版本統一維護在一個地方,改一次全專案生效,也讓 code review 時很容易看出誰改了共用版本。


TypeScript 設定的最佳實踐

在 Monorepo 裡,TypeScript 的設定策略會直接影響開發體驗與 CI 效能。常見的兩種做法:

1. Path Alias(簡單但沒有真正模組化)

在根目錄的 tsconfig.jsonpaths 指向各套件原始碼:

{
  "compilerOptions": {
    "paths": {
      "@myorg/shared/*": ["packages/shared/src/*"]
    }
  }
}

這種方式設定簡單,但整個 workspace 仍然被當成一個大型 TypeScript 專案處理,型別檢查與編譯效能不會因此變好,套件之間也沒有真正的邊界隔離。

2. TypeScript Project References(建議用於較大型專案)

透過 references 把每個套件拆成獨立的 TypeScript 專案:

{
  "references": [
    { "path": "../shared" },
    { "path": "../ui" }
  ]
}

搭配 tsc --build 做增量編譯,好處是:

  • 記憶體使用量較低,對 CI 這種資源有限的環境特別有感
  • 編輯器的型別檢查與自動完成回應更快,因為 TypeScript 只需要處理真正相關的專案
  • 套件邊界更清楚,避免不小心跨套件直接引用內部檔案

如果專案套件數量不多(單位數),path alias 通常已經夠用;但一旦套件數量成長到十幾二十個,project references 帶來的效能差異會越來越明顯。

測試:Bun Test Runner 在 Monorepo 的整合

Bun 內建的 test runner 速度很快,但放到 Monorepo 環境下需要注意幾件事:

  1. 統一測試相關依賴版本:測試框架、mock 工具的版本如果在各套件間不一致,容易出現模組解析錯誤,這正是 Catalogs 機制可以派上用場的地方。
  2. 善用生命週期 hook:可以在每個套件內設定 setup / teardown,確保測試環境的一致性,不需要在每個測試檔案裡重複寫初始化邏輯。
  3. 搭配 --filter 平行跑測試:不需要每次都跑全部套件的測試,針對有變動的套件執行即可,大幅縮短本地開發與 CI 的等待時間。
# 只跑特定套件的測試
bun test --filter "@myorg/api"

CI/CD 的幾個注意事項

1. 快取 Bun 的安裝快取目錄

Bun 有自己的全域快取(預設在 ~/.bun/install/cache),CI 環境把這個目錄快取起來,可以大幅縮短重複安裝的時間:

# GitHub Actions 範例
- uses: actions/cache@v4
  with:
    path: ~/.bun/install/cache
    key: bun-${{ hashFiles('bun.lock') }}

2. 一定要用 --frozen-lockfile

CI 環境安裝依賴時務必加上 --frozen-lockfile,避免 CI 過程意外更新 lockfile 導致「本地測試過但 CI 壞掉」的狀況。

3. 只跑受影響的套件(Affected)

這是 Bun workspaces 目前比較弱的一塊——它不會幫你判斷「這次改動影響了哪些套件」。
如果專案套件數量還不多,全部重跑問題不大;
但套件一多,每次 push 都全跑會讓 CI 時間快速膨脹。這時候通常有兩個方向:

  • 疊加 Turborepo 或 Nx,取得 affected 偵測與遠端快取
  • 用 git diff 搭配自訂 script,簡易判斷哪些 workspace 有變動

什麼時候該考慮升級到完整的 Monorepo 工具?

Bun workspaces 很適合中小型專案,但當團隊或程式碼庫成長到一定程度,會開始感受到以下痛點:

  • CI 每次都要重跑所有套件的 build / test,即使改動只有一行
  • 沒有 remote cache,多個開發者、多台 CI machine 之間無法共享編譯結果
  • 缺乏依賴圖分析,難以精準判斷一個改動的影響範圍

如果符合以上情況,可以考慮在 Bun 之上疊加 Turborepo 或 Nx
好消息是,這類工具通常設計成可以直接讀取既有的 package.json scripts 與 workspace 結構,
遷移成本不算太高——你不需要放棄 Bun 帶來的安裝速度優勢,只是多加一層任務編排與快取。


小結:該不該用 Bun 管理 Monorepo?

整理一個簡單的判斷原則:

情境 建議
小型 team、套件數量不多、追求開發迭代速度 Bun workspaces 已足夠
中大型專案、需要 task caching 與 affected 偵測 Bun workspaces + Turborepo / Nx
大規模企業級、上百位開發者、對穩定性要求極高 可評估 pnpm 生態圈的成熟度優勢

Bun 在 2026 年的套件管理生態已經相當成熟,workspace: 協議、Catalogs、--filter
這些機制讓中小型 Monorepo 的日常開發體驗非常流暢。但它終究是「套件管理員」而非「Monorepo 建置系統」,
如果你的專案已經大到需要精確的任務快取與依賴影響分析,記得適時導入專門的 Monorepo 工具,
而不是硬把所有邏輯塞進 shell script 裡自己維護。


上一篇
bun:ffi × Rust —— 用 cdylib 打造原生擴充模組
下一篇
Bun 效能壓測:用 wrk 打爆你的 Server,再優化它
系列文
不只是快 —— Bun 30 天:從底層架構、全套工具鏈到生產部署28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言