Chapter 02

从零到第一次缓存命中

两条路:用 create-turbo 从模板起手(推荐新项目),或把 Turbo 接入已有 pnpm workspaces(老项目)。目标——跑第二次 build 看到 FULL TURBO,说明缓存跑通了。

路径 A:create-turbo 模板

pnpm dlx create-turbo@latest my-turborepo
# 交互式选:
# - Package manager: pnpm(推荐)
# - Template: default / kitchen sink / 其它

cd my-turborepo
pnpm install

生成的目录:

my-turborepo/
├── turbo.json                    ← Turbo 配置
├── pnpm-workspace.yaml
├── package.json                  ← 根
├── apps/
│   ├── web/                      ← Next.js 应用
│   └── docs/                     ← Next.js 文档
└── packages/
    ├── ui/                       ← 共享组件库
    ├── eslint-config/
    └── typescript-config/

路径 B:接入已有 pnpm workspace

pnpm add -Dw turbo

根 package.json:

{
  "name": "my-mono",
  "private": true,
  "packageManager": "pnpm@9.12.0",
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test"
  },
  "devDependencies": {
    "turbo": "^2.2.3"
  }
}

最小 turbo.json

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
tasks 的三个关键字段

^ 前缀的含义

"build": { "dependsOn": ["^build"] }

子包 package.json

// packages/ui/package.json
{
  "name": "@myorg/ui",
  "version": "0.0.1",
  "main": "./dist/index.js",
  "scripts": {
    "build": "tsc",
    "lint": "eslint src",
    "test": "vitest run"
  }
}

Turbo 不自己跑编译——它调用每个包 package.json 里定义的 script。这意味着任何已有的 npm scripts 可以直接接入

第一次跑

pnpm turbo run build
• Packages in scope: @myorg/docs, @myorg/ui, @myorg/web
• Running build in 3 packages
• Remote caching disabled

@myorg/ui:build: cache miss, executing 5a2f1e8c...
@myorg/ui:build: $ tsc
@myorg/ui:build: done in 3.2s

@myorg/web:build: cache miss, executing 7b4d8c3f...
@myorg/web:build: $ next build
@myorg/web:build: done in 18.5s

@myorg/docs:build: cache miss, executing a12b34cd...
@myorg/docs:build: $ next build
@myorg/docs:build: done in 14.2s

 Tasks:    3 successful, 3 total
Cached:    0 cached, 3 total
  Time:    22.1s

第二次跑(魔法时刻)

pnpm turbo run build
@myorg/ui:build: cache hit, replaying logs 5a2f1e8c...
@myorg/web:build: cache hit, replaying logs 7b4d8c3f...
@myorg/docs:build: cache hit, replaying logs a12b34cd...

 Tasks:    3 successful, 3 total
Cached:    3 cached, 3 total
  Time:    350ms >>> FULL TURBO

22 秒 → 350 毫秒,FULL TURBO 横幅出现——所有任务都命中缓存。Turbo 直接把上次的 dist/.next 恢复,根本不跑 tsc/next build。

什么决定 hash?
默认包括:包源码文件 + package.json + dependency 包的 hash + 相关的环境变量 + 依赖包的 lockfile。任一变化 → hash 变 → 缓存不命中 → 重跑。

给任务传参

pnpm turbo run test -- --coverage
# -- 之后的参数传给 vitest

pnpm turbo run build --filter=@myorg/web
# 只 build web

pnpm turbo run build --concurrency=1
# 串行(默认并行)

pnpm turbo run build --force
# 忽略缓存,强制重跑

看任务图

pnpm turbo run build --graph
# 输出 DOT 文件

pnpm turbo run build --graph=graph.png
# 生成 PNG(需要 graphviz)

pnpm turbo run build --dry-run
# 不执行,只显示会跑什么

pnpm turbo run build --dry-run=json
# JSON 输出,便于脚本处理

.turbo 目录

my-turborepo/
├── .turbo/                        ← 根的日志和元数据
│   ├── cookies/
│   ├── daemon/
│   └── runs/
├── apps/web/.turbo/               ← 每包各自的 cache
│   ├── cache/                     ← 缓存产物
│   │   └── 5a2f1e8c.tar.zst
│   └── turbo-build.log            ← 任务日志
└── node_modules/.cache/turbo/     ← 本地 cache 的另一位置
echo ".turbo" >> .gitignore
# .turbo 不该进 git

globalDependencies

{
  "globalDependencies": [
    ".env",
    ".env.*",
    "tsconfig.json"
  ],
  "tasks": { ... }
}

全仓 hash 依赖——这些文件改了 → 所有任务缓存失效。.env、根 tsconfig 都是典型例子。

Node/pnpm 版本约束

{
  "engines": {
    "node": ">=18.17.0",
    "pnpm": ">=9.0.0"
  },
  "packageManager": "pnpm@9.12.0"
}

.npmrc 配合

enable-pre-post-scripts=true
auto-install-peers=true

确保 pre/post 脚本被 Turbo 能正确触发。

常见启动问题

could not find turbo.json
文件要在仓库根目录,和 pnpm-workspace.yaml 同级。
task "build" not found in pipeline
某个子包 package.json 没 build script,Turbo 也要求顶层 turbo.json 里有定义。
FULL TURBO 出不来
每次 hash 都变——常因 outputs 写漏(没把实际产物全列上)或任务非确定性(比如往 dist 写时间戳)。
缓存太大
! 排除不需要缓存的子目录,如 ".next/**", "!.next/cache/**"

下一步

已经有最小可跑的 Turbo。下一章深入任务图:dependsOn 语法、^/$、交叉引用、全局依赖——把 CI 的拓扑搞对,缓存才稳定。

本章小结