Chapter 03

把任务画成一张图

Turbo 的核心是任务 DAG(有向无环图):节点是"某包的某任务",边是 dependsOn 关系。画对了图,Turbo 才能最大化并行 + 不漏跑。

dependsOn 的三种写法

{
  "tasks": {
    "build": {
      "dependsOn": [
        "^build",              // 1. 依赖包的 build
        "lint",                 // 2. 自己的 lint
        "//#generate-schema"    // 3. 根 workspace 的 generate-schema
      ]
    }
  }
}
语法含义
taskName同一个 workspace 的 task
^taskName依赖包的 task(上游)
//#taskName根 package.json 里的 script(不属于任何子包)

经典 DAG 示例

workspace 依赖:
  web → ui → utils
  docs → ui → utils

turbo.json:
  build: dependsOn ["^build"]
  test:  dependsOn ["build"]
  lint:  dependsOn []

pnpm turbo run test 的执行顺序:
  1. utils:build                   (utils 无上游,先跑)
  2. utils:test                    (自己的 build 完了)
  3. ui:build                      (等 utils:build 完)
  4. ui:test
  5. web:build  + docs:build       (并行)
  6. web:test   + docs:test        (并行)

Turbo 自动推导这个顺序——你只需写 dependsOn ["^build"],剩下的拓扑排序它搞定。

跨任务依赖

{
  "tasks": {
    "build": { "dependsOn": ["^build"] },
    "typecheck": { "dependsOn": ["^build"] },
    "test": {
      "dependsOn": ["build", "typecheck"]
    }
  }
}

test 同时等自己的 build 和 typecheck——Turbo 只在两者都完成后才跑 test。

根 task(//# 前缀)

// 根 package.json
{
  "scripts": {
    "generate-schema": "openapi-generate --out ./schemas"
  }
}
// turbo.json
{
  "tasks": {
    "//#generate-schema": {
      "inputs": ["openapi.yaml"],
      "outputs": ["schemas/**"]
    },
    "build": {
      "dependsOn": ["//#generate-schema", "^build"]
    }
  }
}

在根 workspace 跑一个"生成 schema"的任务,完成之后所有子包才开始 build——适合代码生成、migration 类前置步骤。

特殊的 inputs 和 outputs

{
  "build": {
    "inputs": [
      "$TURBO_DEFAULT$",          // 当前包的默认 inputs
      "../shared-config/**"        // 跨包引用文件
    ],
    "outputs": [
      "dist/**",
      ".next/**",
      "!.next/cache/**"              // 排除
    ]
  }
}
$TURBO_DEFAULT$
代表当前包里所有被 git 追踪的文件——大多数时候的起点。
$TURBO_ROOT$
仓库根路径,用来引用根目录的文件,如 $TURBO_ROOT$/tsconfig.base.json
!pattern
排除——和 .gitignore 一样语义。

dry-run 验证图

pnpm turbo run build --dry-run
Tasks to Run
  @myorg/utils#build
    Task                          = build
    Package                       = @myorg/utils
    Hash                          = 5a2f1e8c...
    Cached (Local)                = false
    Command                       = tsc
    Dependencies                  = []
    Dependendents                 = @myorg/ui#build
    Inputs Files Considered       = 23
    Env Vars                      = []

  @myorg/ui#build
    Task                          = build
    Package                       = @myorg/ui
    Dependencies                  = [@myorg/utils#build]
    Dependendents                 = [@myorg/web#build, @myorg/docs#build]
    ...

每个任务的 hash、依赖、被依赖、输入文件数——调试任务图的最佳工具。

JSON 输出分析

pnpm turbo run build --dry-run=json | jq '.tasks[] | {task: .taskId, hash: .hash, cached: .cache.local}'

适合 CI 里分析哪个任务 hash 变了、为什么没命中缓存。

图可视化

pnpm turbo run build --graph
# 输出 DOT 格式到 stdout

pnpm turbo run build --graph=graph.html
# 生成 HTML + Mermaid 交互图

pnpm turbo run build --graph=graph.svg
# SVG(需 graphviz)
HTML 图最实用
Turbo 2.x 生成的 HTML 图带 zoom/pan、hover 显示 hash——大 monorepo 排查"为什么这个任务跑了"时非常直观。

避免循环依赖

// ❌ 循环
{
  "tasks": {
    "build": { "dependsOn": ["test"] },
    "test":  { "dependsOn": ["build"] }
  }
}
// Turbo 会报错:Cyclic dependency detected: build → test → build

DAG 必须无环——设计时,把任务当成管线节点,前后关系清晰就不会成环。

并行度控制

pnpm turbo run build --concurrency=10
# 最多 10 个任务同时跑

pnpm turbo run build --concurrency=1
# 串行(debug 时有用)

pnpm turbo run build --concurrency=100%
# 用满 CPU(默认)

workspace 级别的覆盖

// turbo.json
{
  "tasks": {
    "build": {
      "outputs": ["dist/**"]
    },
    "@myorg/web#build": {           // 只针对 web
      "dependsOn": ["^build", "@myorg/api#generate"],
      "outputs": [".next/**", "!.next/cache/**"]
    }
  }
}

顶层的 build 是默认,具体包的 @myorg/web#build 覆盖——写特殊逻辑不影响其它包。

with-* 写法

{
  "tasks": {
    "dev": {
      "cache": false,
      "persistent": true,
      "with": ["api#dev", "worker#dev"]
    }
  }
}

with 是 Turbo 2.x 新增——跑 web dev 时并发拉起 api dev 和 worker dev,适合开发期"前端+API+队列"三件套同时启动。

实战:一个完整的 turbo.json

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [".env", "tsconfig.base.json"],
  "globalEnv": ["NODE_ENV", "VERCEL_ENV"],
  "tasks": {
    "build": {
      "dependsOn": ["^build", "//#generate-schema"],
      "inputs": ["$TURBO_DEFAULT$", "!**/*.md"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "env": ["NEXT_PUBLIC_*"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "typecheck": {
      "dependsOn": ["^build"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    },
    "//#generate-schema": {
      "inputs": ["openapi.yaml"],
      "outputs": ["packages/api-types/src/**"]
    }
  }
}

读完这份配置,任何人都能理解整个 monorepo 的构建流程——比读 300 行 bash 脚本清晰得多。

本章小结