turborepo

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Turborepo - Monorepo Architecture Expert

Turborepo - 单体仓库(Monorepo)架构专家

Assumption: You know
turbo run build
. This covers architectural decisions.

前提假设:你已了解
turbo run build
。本指南聚焦架构决策相关内容。

Before Adopting Turborepo: Strategic Assessment

采用Turborepo之前:战略评估

Ask yourself these questions BEFORE committing to monorepo:
在决定使用单体仓库前,请先问自己这些问题:

1. Team & Coordination Analysis

1. 团队与协作分析

  • Team size: 1-3 engineers → Polyrepo simpler (monorepo overhead not worth it)
  • Shared code percentage: <20% → Polyrepo, >50% → Monorepo compelling
  • Coordination pain: Breaking changes require 3+ repos updated → Monorepo wins
  • Deployment coupling: Services deploy together → Monorepo, Independently → Polyrepo
  • 团队规模:1-3名工程师 → 多仓库(polyrepo)更简单(单体仓库的管理成本得不偿失)
  • 共享代码占比:<20% → 多仓库,>50% → 单体仓库更具优势
  • 协作痛点:破坏性变更需要更新3个以上仓库 → 单体仓库更合适
  • 部署耦合性:服务需一起部署 → 单体仓库,独立部署 → 多仓库

2. Technical Complexity Assessment

2. 技术复杂度评估

  • Languages: Pure JS/TS → Turborepo works, Mixed (Go/Python) → Nx or polyrepo
  • Build time: <5min total across all apps → Overhead not justified yet
  • Cache importance: Long builds (>2min per package) → Turborepo caching critical
  • CI complexity: Simple pipeline → Polyrepo easier, Complex (affected detection) → Monorepo
  • 开发语言:纯JS/TS → Turborepo适用,多语言混合(Go/Python)→ 选择Nx或多仓库
  • 构建时间:所有应用总构建时间<5分钟 → 暂不需要单体仓库的额外功能
  • 缓存重要性:构建时间长(每个包>2分钟)→ Turborepo缓存至关重要
  • CI复杂度:简单流水线 → 多仓库更易维护,复杂流水线(变更影响检测)→ 单体仓库

3. Maintenance Cost Analysis

3. 维护成本分析

  • What breaks with monorepo: Version conflicts, build order issues, cache debugging, tooling complexity
  • What breaks with polyrepo: API version hell, coordination overhead, code duplication, cross-repo changes
  • Break-even point: Monorepo worth it when 3+ apps share 30%+ code + frequent coordination needed

  • 单体仓库的风险:版本冲突、构建顺序问题、缓存调试、工具复杂度
  • 多仓库的风险:API版本混乱、协作成本高、代码重复、跨仓库变更繁琐
  • 盈亏平衡点:当3个以上应用共享30%+代码且需要频繁协作时,单体仓库的价值凸显

Critical Rule: Package Tasks, Not Root Tasks

核心规则:为包定义任务,而非根目录任务

The #1 Turborepo mistake: Putting task logic in root
package.json
.
json
// ❌ WRONG - defeats parallelization
// Root package.json
{
  "scripts": {
    "build": "cd apps/web && next build && cd ../api && tsc",
    "lint": "eslint apps/ packages/",
    "test": "vitest"
  }
}

// ✅ CORRECT - parallel execution
// apps/web/package.json
{ "scripts": { "build": "next build", "lint": "eslint .", "test": "vitest" } }

// apps/api/package.json
{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } }

// Root package.json - ONLY delegates
{ "scripts": { "build": "turbo run build" } }
Why it breaks: Turborepo can't parallelize sequential shell commands. Package tasks enable task graph parallelization.

Turborepo最常见错误:将任务逻辑放在根目录
package.json
中。
json
// ❌ 错误 - 无法实现并行化
// 根目录 package.json
{
  "scripts": {
    "build": "cd apps/web && next build && cd ../api && tsc",
    "lint": "eslint apps/ packages/",
    "test": "vitest"
  }
}

// ✅ 正确 - 支持并行执行
// apps/web/package.json
{ "scripts": { "build": "next build", "lint": "eslint .", "test": "vitest" } }

// apps/api/package.json
{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } }

// 根目录 package.json - 仅做任务代理
{ "scripts": { "build": "turbo run build" } }
问题原因:Turborepo无法并行化执行顺序shell命令。为每个包定义任务才能实现任务图的并行化。

Decision: When to Split a Package

决策:何时拆分包

Considering splitting code into package?
├─ Code used by 1 app only → DON'T split yet
│   └─ Keep in app until second consumer appears
│      WHY: Premature abstraction, overhead > benefit
├─ Code used by 2+ apps → MAYBE split
│   ├─ Stable API (rarely changes) → Split
│   ├─ Unstable (changes every sprint) → DON'T split yet
│   └─ Mixed team ownership → DON'T split (use import path instead)
│      WHY: Shared packages need stable APIs + clear owners
├─ Publishing to npm → MUST split
│   └─ External packages require independent versioning
└─ CI builds too slow (> 10min) → Split strategically
    └─ Split by stability (core vs features), not by domain
       WHY: Stable packages cache, unstable packages rebuild
Anti-pattern: Creating packages for "clean architecture" without consumers. Packages add overhead (build, test, version).

考虑拆分代码为独立包?
├─ 代码仅被1个应用使用 → 暂不拆分
│   └─ 直到出现第二个使用者前,保留在应用内
│      原因:过早抽象,成本大于收益
├─ 代码被2个以上应用使用 → 可考虑拆分
│   ├─ API稳定(极少变更)→ 拆分
│   ├─ API不稳定(每个迭代都变更)→ 暂不拆分
│   └─ 多团队共同维护 → 暂不拆分(改用导入路径)
│      原因:共享包需要稳定API和明确的负责人
├─ 需要发布到npm → 必须拆分
│   └─ 外部包需要独立版本管理
└─ CI构建过慢(>10分钟)→ 战略性拆分
    └─ 按稳定性拆分(核心代码 vs 功能代码),而非按领域拆分
       原因:稳定包可缓存,不稳定包需重新构建
反模式:为了“整洁架构”而创建无使用者的包。包会增加额外成本(构建、测试、版本管理)。

Anti-Patterns

反模式

❌ #1: Circular Dependencies

❌ #1:循环依赖

Problem: Packages depend on each other, breaks task graph
packages/ui → imports from packages/utils
packages/utils → imports from packages/ui  // ❌ Circular
Detection:
bash
turbo run build  # Fails with: "Could not resolve dependency graph"
Fix: Extract shared code to third package
packages/ui → packages/shared
packages/utils → packages/shared
Why it breaks: Turborepo builds dependencies first (topological sort). Circular deps = no valid build order.
Why this is deceptively hard to debug: Error message "Could not resolve dependency graph" doesn't mention the word "circular"—just lists package names. With 10+ packages, takes 15-20 minutes to manually trace imports and realize two packages reference each other. The import chain might be indirect (A → B → C → A), making it even harder to spot. Developers waste time checking turbo.json config and workspace setup before realizing it's an import cycle issue, not a Turborepo configuration problem.
问题:包之间互相依赖,破坏任务图
packages/ui → 导入 packages/utils
packages/utils → 导入 packages/ui  // ❌ 循环依赖
检测方法
bash
turbo run build  // 执行失败,提示:"Could not resolve dependency graph"
修复方案:将共享代码提取到第三个包
packages/ui → packages/shared
packages/utils → packages/shared
问题原因:Turborepo按拓扑排序优先构建依赖项。循环依赖导致没有有效的构建顺序。
调试难点:错误提示“Could not resolve dependency graph”未提及“循环”一词,仅列出包名。当包数量超过10个时,手动追踪导入关系并发现循环依赖需要15-20分钟。导入链可能是间接的(A→B→C→A),更难发现。开发者会先花费时间检查turbo.json配置和工作区设置,之后才意识到是导入循环问题,而非Turborepo配置问题。

❌ #2: Overly Granular Packages

❌ #2:过度细分的包

Problem: 50 micro-packages, every import crosses package boundary
packages/button/
packages/input/
packages/checkbox/
packages/radio/
packages/select/
// ... 45 more single-component packages
Symptoms:
  • Every change touches 5+ packages
  • 10+ version bumps per feature
  • pnpm workspace:*
    version hell
Fix: Group by stability/purpose
packages/ui/           # All components (changes often)
packages/ui-primitives/ # Headless components (stable)
packages/icons/        # Generated SVGs (rarely changes)
Decision rule: Package boundary = different change frequency
Why this is deceptively hard to debug: Takes weeks or months to discover the problem—not immediate. First feature seems fine (update 3 packages, publish 3 versions). Second feature touches 5 packages. Third feature hits 10 packages and you're managing workspace version conflicts for 2 hours. The pain accumulates slowly: CI gets slower (building 50 packages), version bumps become tedious (changesets for 10+ packages), developers avoid refactoring because it crosses too many boundaries. Only after 3-6 months do you realize the granularity was wrong, but by then you have 50 packages and merging them requires major migration work.
问题:50个微包,每次导入都跨包边界
packages/button/
packages/input/
packages/checkbox/
packages/radio/
packages/select/
// ... 还有45个单一组件包
症状
  • 每次变更涉及5个以上包
  • 每个功能需要更新10个以上版本
  • pnpm workspace:*
    版本管理混乱
修复方案:按稳定性/用途分组
packages/ui/           # 所有组件(频繁变更)
packages/ui-primitives/ # 无头组件(稳定)
packages/icons/        # 生成的SVG(极少变更)
决策规则:包边界 = 不同的变更频率
调试难点:问题不会立即显现,需要数周或数月才会暴露。第一个功能似乎没问题(更新3个包,发布3个版本),第二个功能涉及5个包,第三个功能涉及10个包,此时开发者需要花费2小时解决工作区版本冲突。痛苦会逐渐累积:CI构建变慢(构建50个包),版本更新繁琐(为10个以上包创建变更集),开发者因跨包边界而避免重构。3-6个月后才会意识到包的粒度错误,但此时已有50个包,合并需要大量迁移工作。

❌ #3: Missing Task Dependencies

❌ #3:缺失任务依赖

Problem: Tests run before build completes
json
// turbo.json
{
  "tasks": {
    "build": { "outputs": ["dist/**"] },
    "test": {}  // ❌ No dependsOn
  }
}

// Result: tests import from dist/ before it exists
Fix: Explicit dependencies
json
{
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
    "test": { "dependsOn": ["build"] }  // ✅ Build first
  }
}
Why:
^build
= build dependencies first.
build
= build this package first.
Why this is deceptively hard to debug: Tests pass locally (you ran
build
manually first), fail in CI with cryptic errors like "Cannot find module './dist/index.js'" or import errors. The race condition is timing-dependent—sometimes tests start before build finishes, sometimes they start after (especially with caching). Developers waste 10-15 minutes checking import paths, package.json exports, and tsconfig before realizing it's a task ordering issue. The error message points to the symptom (missing file) not the cause (missing dependency declaration).
问题:测试在构建完成前执行
json
// turbo.json
{
  "tasks": {
    "build": { "outputs": ["dist/**"] },
    "test": {}  // ❌ 未设置dependsOn
  }
}

// 结果:测试从dist/导入文件时,文件尚未生成
修复方案:明确任务依赖
json
{
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
    "test": { "dependsOn": ["build"] }  // ✅ 先执行构建
  }
}
说明
^build
= 优先构建依赖包。
build
= 先构建当前包。
调试难点:本地测试通过(已手动执行build),但CI中失败,提示“Cannot find module './dist/index.js'”或导入错误。竞争条件与时间有关——有时测试在构建完成后启动,有时在构建完成前启动(尤其是启用缓存时)。开发者会花费10-15分钟检查导入路径、package.json导出配置和tsconfig,之后才意识到是任务顺序问题。错误提示指向症状(文件缺失)而非原因(未声明依赖)。

❌ #4: Cache Miss Hell

❌ #4:缓存未命中噩梦

Problem: Cache never hits, rebuilds everything
json
// turbo.json
{
  "tasks": {
    "build": {
      "inputs": ["src/**"]  // ❌ Too broad
    }
  }
}

// Any file change (even comments) = cache miss
Fix: Exclude non-code files
json
{
  "tasks": {
    "build": {
      "inputs": [
        "src/**/*.{ts,tsx}",  // ✅ Only source files
        "!src/**/*.test.ts"   // Exclude tests
      ]
    }
  }
}
Debug cache:
bash
turbo run build --dry --graph  # Shows why cache missed
Why this is deceptively hard to debug: Cache initially works (first few builds hit), then mysteriously stops. Every run shows "cache miss" but you can't tell why. The problem: you added a README.md to src/, touched a comment, or updated a test file—non-code changes that shouldn't trigger rebuild but do because inputs include
src/**
. Developers waste 20-30 minutes checking remote cache credentials, clearing local cache, restarting daemon before realizing the input glob is too broad. The
--dry --graph
flag shows hash changed but doesn't clearly indicate WHICH file caused it (need to diff file lists manually).

问题:缓存从未命中,每次都需全量构建
json
// turbo.json
{
  "tasks": {
    "build": {
      "inputs": ["src/**"]  // ❌ 范围过宽
    }
  }
}

// 任何文件变更(即使是注释)都会导致缓存未命中
修复方案:排除非代码文件
json
{
  "tasks": {
    "build": {
      "inputs": [
        "src/**/*.{ts,tsx}",  // ✅ 仅包含源码文件
        "!src/**/*.test.ts"   // 排除测试文件
      ]
    }
  }
}
调试缓存
bash
turbo run build --dry --graph  // 显示缓存未命中原因
调试难点:缓存最初可用(前几次构建命中),之后突然失效。每次运行都显示“cache miss”但无法确定原因。问题在于:你在src/中添加了README.md、修改了注释或更新了测试文件——这些非代码变更不应触发重构,但因inputs包含
src/**
而导致缓存未命中。开发者会花费20-30分钟检查远程缓存凭证、清理本地缓存、重启守护进程,之后才意识到输入的glob模式范围过宽。
--dry --graph
标志显示哈希变更,但未明确指出是哪个文件导致的(需要手动对比文件列表)。

Decision: Monorepo vs Polyrepo

决策:单体仓库(Monorepo) vs 多仓库(Polyrepo)

Starting new project?
├─ Single team, single product → Polyrepo (simpler)
│   └─ One repo per service/app
│      WHY: Monorepo overhead not worth it for small teams
├─ Shared UI library → Monorepo
│   └─ Library + consumer apps in same repo
│      WHY: Develop library + test in consumers simultaneously
├─ Microservices (different languages) → Polyrepo
│   └─ Go service, Python service, Node service
│      WHY: Turborepo is JS/TS focused, polyrepo simpler
└─ Multiple teams, shared code → Monorepo
    └─ Need atomic changes across boundaries
       WHY: One PR changes API + all consumers
Real-world: Most projects should start polyrepo, migrate to monorepo when pain > tooling cost.

启动新项目?
├─ 单一团队,单一产品 → 多仓库(更简单)
│   └─ 每个服务/应用对应一个仓库
│      原因:小团队无需承担单体仓库的管理成本
├─ 共享UI库 → 单体仓库
│   └─ 库和消费应用在同一仓库
│      原因:可同时开发库并在消费应用中测试
├─ 微服务(多语言)→ 多仓库
│   └─ Go服务、Python服务、Node服务
│      原因:Turborepo专注于JS/TS,多仓库更简单
└─ 多团队,共享代码 → 单体仓库
    └─ 需要跨边界的原子变更
       原因:一个PR即可更新API和所有消费端
实际经验:大多数项目应从多仓库开始,当协作痛苦超过工具成本时再迁移到单体仓库。

Package Boundary Patterns

包边界模式

Pattern 1: By Stability

模式1:按稳定性划分

packages/
  core/         # Changes quarterly (semantic versioning)
  features/     # Changes weekly (workspace protocol)
  utils/        # Changes monthly
Benefit: Stable packages cache longer, ship to npm independently.
packages/
  core/         # 每季度变更(语义化版本管理)
  features/     # 每周变更(工作区协议)
  utils/        # 每月变更
优势:稳定包可长期缓存,可独立发布到npm。

Pattern 2: By Consumer

模式2:按消费者划分

packages/
  public-api/   # External consumers
  internal/     # Internal apps only
Benefit: Clear API surface, different versioning strategies.
packages/
  public-api/   # 外部消费者
  internal/     # 仅内部应用使用
优势:API边界清晰,可采用不同的版本管理策略。

Pattern 3: By Team

模式3:按团队划分

packages/
  team-platform/
  team-growth/
  team-infra/
Warning: Only works if teams rarely share code. Otherwise creates silos.

packages/
  team-platform/
  team-growth/
  team-infra/
警告:仅适用于团队间极少共享代码的场景,否则会造成信息孤岛。

Turborepo vs Alternatives

Turborepo vs 替代方案

Choose Turborepo when:
✅ JS/TS monorepo (React, Next.js, Node)
✅ Need remote caching (Vercel, self-hosted)
✅ Task graph parallelization important
✅ Using pnpm workspaces or npm workspaces

Choose Nx when:
✅ Need project graph visualization
✅ Polyglot (JS + Python + Go)
✅ Want opinionated project structure
✅ Need plugin ecosystem

Choose Rush when:
✅ Very large monorepo (100+ packages)
✅ Need phantom dependencies detection
✅ Publishing to npm is primary use case
Real-world: Turborepo wins for Next.js/React apps, Nx wins for complex polyglot, Rush wins for library publishers.

选择Turborepo的场景:
✅ JS/TS单体仓库(React、Next.js、Node)
✅ 需要远程缓存(Vercel、自托管)
✅ 任务图并行化至关重要
✅ 使用pnpm工作区或npm工作区

选择Nx的场景:
✅ 需要项目图可视化
✅ 多语言混合(JS + Python + Go)
✅ 偏好约定式项目结构
✅ 需要插件生态系统

选择Rush的场景:
✅ 超大规模单体仓库(100+包)
✅ 需要检测幽灵依赖
✅ 主要用途是发布到npm
实际经验:Turborepo适合Next.js/React应用,Nx适合复杂多语言项目,Rush适合库发布者。

Debugging Commands

调试命令

Visualize task graph

可视化任务图

bash
turbo run build --dry --graph=graph.html
bash
turbo run build --dry --graph=graph.html

Opens browser with task dependency visualization

在浏览器中打开任务依赖可视化图

undefined
undefined

Find cache misses

查找缓存未命中

bash
turbo run build --dry=json | jq '.tasks[] | select(.cache.status == "MISS")'
bash
turbo run build --dry=json | jq '.tasks[] | select(.cache.status == "MISS")'

Check package dependency order

检查包依赖顺序

bash
turbo run build --dry --graph | grep "→"
bash
turbo run build --dry --graph | grep "→"

Test cache without running tasks

测试缓存(不执行任务)

bash
turbo run build --dry  # Shows what would run

bash
turbo run build --dry  # 显示将要执行的任务

Error Recovery Procedures

错误恢复流程

When Cache Never Hits (Cache Miss Hell)

缓存从未命中(缓存未命中噩梦)

Recovery steps:
  1. Diagnose: Run
    turbo run build --dry=json | jq '.tasks[0].hash'
    to see current hash
  2. Identify culprit: Add
    --log-order=grouped
    to see which files changed the hash
  3. Fix inputs: Narrow glob patterns to exclude non-code files (tests, docs, configs)
  4. Fallback: If still missing, disable cache for that task temporarily:
    "cache": false
    in turbo.json, then debug without cache pressure
恢复步骤
  1. 诊断:执行
    turbo run build --dry=json | jq '.tasks[0].hash'
    查看当前哈希
  2. 定位问题:添加
    --log-order=grouped
    参数查看哪些文件导致哈希变更
  3. 修复输入配置:缩小glob模式范围,排除非代码文件(测试、文档、配置)
  4. 备选方案:若仍未解决,临时禁用该任务的缓存:在turbo.json中设置
    "cache": false
    ,然后在无缓存压力下调试

When Circular Dependency Error Occurs

出现循环依赖错误

Recovery steps:
  1. Visualize: Run
    turbo run build --dry --graph=graph.html
    and open in browser
  2. Trace cycle: Look for packages that appear in each other's dependency chains (A → B → ... → A)
  3. Extract shared: Create new package (e.g.,
    packages/shared
    ) and move common code there
  4. Fallback: If cycle is complex (3+ packages), use dependency graph tool like
    madge
    to visualize:
    npx madge --circular --extensions ts,tsx packages/
恢复步骤
  1. 可视化:执行
    turbo run build --dry --graph=graph.html
    并在浏览器中打开
  2. 追踪循环:查找在彼此依赖链中出现的包(A→B→...→A)
  3. 提取共享代码:创建新包(如
    packages/shared
    )并将公共代码迁移至此
  4. 备选方案:若循环复杂(涉及3个以上包),使用依赖图工具
    madge
    可视化:
    npx madge --circular --extensions ts,tsx packages/

When Tests Fail in CI But Pass Locally

CI中测试失败但本地通过

Recovery steps:
  1. Check task order: Run
    turbo run test --dry --graph
    to see if build runs before test
  2. Add dependencies: Add
    "dependsOn": ["build"]
    to test task in turbo.json
  3. Verify: Run
    turbo run test --force
    (bypass cache) to confirm tests pass when build runs first
  4. Fallback: If still failing, check for race condition in parallel tests: add
    "cache": false
    to test task temporarily and see if issue persists
恢复步骤
  1. 检查任务顺序:执行
    turbo run test --dry --graph
    查看构建是否在测试前执行
  2. 添加依赖:在turbo.json的test任务中添加
    "dependsOn": ["build"]
  3. 验证:执行
    turbo run test --force
    (绕过缓存)确认先构建再测试可通过
  4. 备选方案:若仍失败,检查并行测试的竞争条件:临时禁用测试任务的缓存
    "cache": false
    ,查看问题是否持续

When Overly Granular Packages Cause Version Hell

过度细分的包导致版本混乱

Recovery steps:
  1. Audit changes: Run
    git log --oneline --since="1 month ago" -- packages/
    to count package version bumps
  2. Identify clusters: Look for packages that always change together (5+ times in last month)
  3. Merge packages: Combine related packages into single package with internal structure
  4. Fallback: If merging is too risky, use
    workspace:*
    protocol to auto-link versions and reduce manual bumps

恢复步骤
  1. 审计变更:执行
    git log --oneline --since="1 month ago" -- packages/
    统计包版本更新次数
  2. 识别集群:查找总是一起变更的包(过去1个月变更5次以上)
  3. 合并包:将相关包合并为单个包,保留内部结构
  4. 备选方案:若合并风险过高,使用
    workspace:*
    协议自动链接版本,减少手动更新

When to Load Full Reference

何时查阅完整参考文档

MANDATORY - READ ENTIRE FILE:
references/cli-options.md
when:
  • Encountering 3+ unknown CLI flags in error messages or commands
  • Need advanced filtering across 10+ packages (--filter patterns, --affected usage)
  • Setting up 5+ complex task pipeline options (--concurrency, --continue, --output-logs)
  • Troubleshooting CLI behavior that's not covered in this core framework
MANDATORY - READ ENTIRE FILE:
references/remote-cache-setup.md
when:
  • Setting up remote cache for team with 3+ developers
  • Debugging 5+ cache authentication or connection errors
  • Configuring self-hosted remote cache with custom storage backend
  • Implementing cache security policies (signature verification, access control)
Do NOT load references for:
  • Basic architecture decisions (use this core framework)
  • Single cache miss debugging (use Error Recovery section above)
  • Deciding whether to adopt monorepo (use Strategic Assessment section)

必须阅读完整文档
references/cli-options.md
适用于以下场景:
  • 在错误信息或命令中遇到3个以上未知CLI标志
  • 需要对10个以上包进行高级过滤(--filter模式、--affected用法)
  • 设置5个以上复杂任务流水线选项(--concurrency、--continue、--output-logs)
  • 排查本核心框架未覆盖的CLI行为问题
必须阅读完整文档
references/remote-cache-setup.md
适用于以下场景:
  • 为3名以上开发者的团队设置远程缓存
  • 排查5个以上缓存认证或连接错误
  • 配置自定义存储后端的自托管远程缓存
  • 实施缓存安全策略(签名验证、访问控制)
无需查阅参考文档
  • 基础架构决策(使用本核心框架即可)
  • 单一缓存未命中调试(使用上述错误恢复部分)
  • 决定是否采用单体仓库(使用战略评估部分)

Resources

资源