跳到主要內容

Turborepo 入門:獨立開發者也能用的 Monorepo 架構

你有一個 Next.js 前端、一個 admin dashboard、幾個共用的 utility package。一開始放在不同 repo 裡,每次改一個 shared type 就要跨 repo 發 PR、等 publish、更新版本號。三個 repo 的 ESLint 和 TypeScript config 慢慢飄開,最後沒人記得哪個 repo 用了哪個版本。

Monorepo 解決的就是這個問題。

Monorepo 是什麼

Monorepo 是把多個 project 放在同一個 Git repository 裡,共用一份 dependency tree、一套 config、一條 CI pipeline。它不是把所有 code 塞進同一個資料夾——而是在一個 repo 裡維護清楚的專案邊界。

什麼時候值得用:

  • 你有 2 個以上的 app 共用邏輯(UI 元件、型別、工具函式)
  • 你花太多時間在跨 repo 同步 config 和版本
  • 你希望一個 PR 能同時改 app 和它依賴的 package

什麼時候不需要:

  • 你只有一個 app,沒有共用模組
  • 團隊成員之間的 codebase 完全獨立
  • 你的 CI/CD 跑不了 monorepo(不過現在幾乎都支援了)

獨立開發者一個人維護 2-3 個 app 加幾個 shared package,monorepo 反而比多 repo 更省事。

Turborepo vs Nx

Monorepo 工具有兩大選擇:Turborepo 和 Nx。

TurborepoNx
定位Task runner + caching完整 monorepo 框架
學習成本低,設定一個 turbo.json中高,有自己的 plugin 系統
設定量極少,幾行 JSON較多,需要理解 project graph
Generator無內建內建 code generator
Plugin 生態無(刻意精簡)豐富(React、Angular、Node 等)
Remote CacheVercel 原生支援Nx Cloud
適合中小型專案、想保持簡單大型專案、需要嚴格的架構約束

Turborepo 的哲學是「做好一件事」——它只管 task orchestration 和 caching,其他都交給你現有的工具鏈。如果你已經有 pnpm workspaces 跑得好好的,加上 Turborepo 只需要一個設定檔。

Nx 功能更全,但學習曲線也更陡。對獨立開發者或小團隊來說,Turborepo 的簡單是優勢。

從零建立:pnpm workspaces + Turborepo

Step 1:初始化專案

mkdir my-monorepo && cd my-monorepo
pnpm init

建立 pnpm-workspace.yaml

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'

這告訴 pnpm:apps/packages/ 下的每個資料夾都是一個獨立的 workspace package。

Step 2:安裝 Turborepo

pnpm add -D turbo -w

-w 是安裝到 workspace root。

Step 3:建立目錄結構

my-monorepo/
├── apps/
│   ├── web/              # 主要前端 (Next.js)
│   │   ├── package.json
│   │   └── ...
│   └── admin/            # Admin dashboard (Next.js)
│       ├── package.json
│       └── ...
├── packages/
│   ├── ui/               # 共用 UI 元件
│   │   ├── package.json
│   │   └── src/
│   ├── utils/            # 共用工具函式
│   │   ├── package.json
│   │   └── src/
│   └── config/           # 共用 ESLint、TypeScript config
│       ├── eslint/
│       └── typescript/
├── turbo.json
├── pnpm-workspace.yaml
└── package.json

每個 apps/*packages/* 下的資料夾都有自己的 package.json,定義自己的 name、dependencies 和 scripts。

Step 4:設定 package 之間的依賴

apps/web/package.json 裡引用共用 package:

{
  "name": "@apps/web",
  "dependencies": {
    "@packages/ui": "workspace:*",
    "@packages/utils": "workspace:*"
  }
}

workspace:* 是 pnpm 的語法,表示「用 workspace 裡的版本」。不需要 publish,不需要版本號,改了立刻生效。

turbo.json:定義 task pipeline

這是 Turborepo 的核心設定:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"]
    }
  }
}

幾個關鍵概念:

  • dependsOn: ["^build"]^ 表示先跑所有上游依賴的 build。當你跑 turbo run build,Turborepo 會先 build packages/uipackages/utils,再 build apps/web
  • outputs — 告訴 Turborepo 哪些檔案是 build 產物,用於 cache
  • cache: falsedev 不需要 cache,因為它是 long-running process
  • persistent: true — 標記 dev 是持續運行的 task,不會阻塞其他 task

pnpm turbo run build,Turborepo 會自動解析依賴圖、平行執行、跳過沒有變更的 package。

常用 packages 拆法

packages/ui

共用 React 元件。Button、Card、Modal、Input 這些在多個 app 都會用到的元件放這裡:

{
  "name": "@packages/ui",
  "exports": {
    "./*": "./src/*.tsx"
  }
}

在 app 裡直接 import:

import { Button } from '@packages/ui/button'

packages/utils

純函式工具:日期格式化、字串處理、type guards。不要放 React 相關的東西,保持 runtime agnostic。

packages/config

共用設定檔。ESLint config、TypeScript base config、Tailwind preset。每個 app 的 config 都 extend 這裡的 base:

// apps/web/tsconfig.json
{
  "extends": "@packages/config/typescript/nextjs.json",
  "compilerOptions": {
    "outDir": "dist"
  }
}

一個地方改,所有 app 同步更新。不會再出現 A 專案用 strict mode、B 專案沒用的情況。

Remote Caching

Turborepo 最實用的功能之一。本地 cache 只對你自己有效,remote cache 讓整個 CI 和團隊共享 build 結果。

# 登入 Vercel(Turborepo 的 remote cache 由 Vercel 提供)
pnpm turbo login

# 連結到你的 Vercel team
pnpm turbo link

設定完之後,CI 第一次 build 的結果會上傳到 remote cache。下次任何人(或 CI runner)跑同樣的 build,如果 input 沒變,直接從 cache 拿結果,build 時間從幾分鐘變成幾秒。

對獨立開發者來說,最大的好處是 CI 加速。你的 GitHub Actions 不用每次都重新 build 所有 package。

實際效益

用 Turborepo 管理一個有 3 個 app + 5 個 package 的 monorepo,日常開發的體感差異:

  • 改一個 shared type — 以前:改 package → publish → 更新三個 app 的版本 → 三個 PR。現在:改一次,三個 app 自動拿到
  • 跑 build — 以前:三個 repo 各跑一次。現在:turbo run build,沒改的 package 直接用 cache
  • 加新 app — 以前:從零設定 ESLint、TypeScript、CI。現在:建個資料夾,extend 共用 config,done
  • CI 時間 — 啟用 remote cache 後,只有改到的 package 會重新 build

Monorepo 不是銀彈,但對「一個人維護多個相關專案」的場景,它是目前最實用的架構選擇。


想看實際的 monorepo 架構怎麼搭配 Supabase、Polar 和 Next.js 建構完整的學習平台?到 Docs 查看我們的架構設計和實作指南。