--filter 语法
pnpm turbo run build --filter=@myorg/web # 只 build web 这一个包 pnpm turbo run build --filter=@myorg/web --filter=@myorg/api # 两个包 pnpm turbo run build --filter="@myorg/*" # scope 通配
依赖图过滤
pnpm turbo run build --filter="@myorg/web..." # web 和它所有的依赖(含 ui/utils) pnpm turbo run build --filter="...@myorg/utils" # utils 和所有依赖 utils 的包(ui/web/docs) pnpm turbo run build --filter="...@myorg/utils..." # utils 的整个连通子图(上下游都要)
pkg...
pkg 和它的依赖(下游)——"想 build 这个,先把它依赖的 build 好"。
...pkg
pkg 和依赖它的(上游)——"改了这个包,哪些受影响"。
...pkg...
两边都要,整个传染链。
changed-since 过滤
pnpm turbo run build --filter="[origin/main]" # 相对于 origin/main 分支,动过代码的包 pnpm turbo run build --filter="...[origin/main]" # 改过的包 + 依赖这些包的包(传染) pnpm turbo run build --filter="[HEAD^1]" # 相对于上一个 commit
PR CI 最常用的组合——只跑和本次 PR 相关的任务:
# .github/workflows/ci.yml - name: Build affected run: pnpm turbo run build test lint --filter="...[origin/main]"
PR CI 时间暴降的核心
100 个包的 monorepo,一个 PR 往往只改 1-2 个——
100 个包的 monorepo,一个 PR 往往只改 1-2 个——
--filter=...[origin/main] 让 CI 只跑相关的几个,配合远程缓存,PR CI 从 10 分钟降到 30 秒。
路径过滤
pnpm turbo run build --filter="./apps/*" # 按路径 glob,不是包名 pnpm turbo run build --filter="./packages/ui-*" # packages 下 ui- 前缀的子包
排除
pnpm turbo run build --filter="@myorg/*" --filter="!@myorg/docs" # 所有 myorg 包,除了 docs
复杂组合
pnpm turbo run test \ --filter="@myorg/web" \ --filter="@myorg/api..." \ --filter="!@myorg/legacy" # web 自己 + api 和它的所有依赖 + 排除 legacy
和 pnpm --filter 的区别
| pnpm -F | turbo --filter | |
|---|---|---|
| 目的 | 选包跑 npm script | 选任务图的子集 |
| 知道任务图 | 否(只看包依赖) | 是(含 dependsOn) |
| 缓存 | 无 | 有 |
| 典型用法 | pnpm -F web dev | turbo run test --filter=... |
两者可以共存——pnpm -F 用来手工启个 dev,turbo --filter 用来跑任务管线。
turbo prune 做什么
场景:Docker 部署 web app。你有 50 个子包,但 web 只用到其中 5 个。
传统做法: COPY . . ← 拷贝整个 monorepo(500MB) RUN pnpm install ← 装所有包的依赖(20 分钟) turbo prune 做法: pnpm turbo prune @myorg/web --docker → 生成 out/json(只有必要的 package.json 和 lockfile) → 生成 out/full(精简的源码) → Dockerfile 分层拷贝,缓存 pnpm install
prune 命令
pnpm turbo prune @myorg/web --docker # 输出到 out/ 目录
out/
├── json/ ← 只有 package.json 结构(含 lockfile)
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── pnpm-workspace.yaml
│ ├── apps/web/package.json
│ ├── packages/ui/package.json
│ └── packages/utils/package.json
└── full/ ← 完整源码(只含需要的包)
├── package.json
├── pnpm-lock.yaml
├── turbo.json
├── apps/web/
│ ├── src/
│ └── package.json
└── packages/
├── ui/
└── utils/
apps/docs、packages/legacy 等和 web 无关的都被裁掉——web 根本不需要它们。
Docker 多阶段构建
# Dockerfile FROM node:20-alpine AS base RUN corepack enable # 1. 裁剪 monorepo FROM base AS pruner WORKDIR /app COPY . . RUN pnpm turbo prune @myorg/web --docker # 2. 装依赖(只装 web 需要的) FROM base AS installer WORKDIR /app COPY --from=pruner /app/out/json/ ./ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ pnpm install --frozen-lockfile # 3. 构建 COPY --from=pruner /app/out/full/ ./ RUN pnpm turbo run build --filter=@myorg/web # 4. 产物镜像 FROM node:20-alpine AS runner WORKDIR /app COPY --from=installer /app/apps/web/.next/standalone ./ COPY --from=installer /app/apps/web/.next/static ./apps/web/.next/static CMD ["node", "apps/web/server.js"]
为什么要分 json 和 full
镜像 layer 缓存原理: - 先 COPY package.json → RUN pnpm install → 缓存 - 再 COPY 源码 → RUN pnpm build - 下次只改源码,package.json 没变 → install 层命中缓存
out/json 只含 package.json/lockfile,out/full 才是源码——分两步拷贝,让 install 层可以缓存。
prune 的效果对比
| 指标 | 无 prune | turbo prune |
|---|---|---|
| Docker context | 500MB | 20MB |
| pnpm install 依赖数 | 1200 | 280 |
| install 时间 | 90秒 | 25秒 |
| 镜像大小 | 800MB | 180MB |
| docker build(改源码) | 3 分钟 | 35 秒 |
prune 不是 dev 用的
不要在开发机跑 prune
prune 是一次性的部署裁剪——产物放在 out/,不改原仓库。本地开发还是用完整 monorepo。把 out/ 加到 .gitignore。
prune 是一次性的部署裁剪——产物放在 out/,不改原仓库。本地开发还是用完整 monorepo。把 out/ 加到 .gitignore。
prune 和 Next.js standalone
// next.config.js module.exports = { output: 'standalone', experimental: { outputFileTracingRoot: path.join(__dirname, '../../'), }, }
Next standalone 模式把必要的 node_modules 复制进 .next/standalone——配合 prune,最终镜像可以不用再装 node_modules,只要 COPY standalone 就行。
filter 实战组合
# 开发期:只跑前端和 API 的 dev pnpm turbo run dev --filter="@myorg/web" --filter="@myorg/api" # CI PR:只 build/test 受影响的包 pnpm turbo run build test lint --filter="...[origin/main]" # 部署前:只构建要发的包 pnpm turbo run build --filter="@myorg/web^..." # 发版:只跑 changesets 识别的 pnpm changeset version pnpm turbo run build --filter="[main~1]"
filter 的陷阱
--filter + globalDependencies 变了
globalDeps(如根 .env、tsconfig.base)变化 → 所有任务 hash 变 → 即使 filter 只圈了一个包,也会重跑。因为 hash 的构成里有全局依赖。
PR 分支没 fetch main
[origin/main] 要求本地有 main 分支,CI 里 actions/checkout 默认只拉一个 commit——必须 fetch-depth: 0。filter 过度收窄
只写了
@myorg/web,没写 @myorg/web... → 依赖包的 build 没跑 → web 用到的 ui 还是旧产物。一般 ...@myorg/web 更安全。verbose 模式调试
pnpm turbo run build --filter="...[origin/main]" --dry-run # 看看到底哪些包被选中了
Packages in scope: @myorg/ui, @myorg/web Tasks to Run: @myorg/ui#build, @myorg/web#build
如果选出来的包数不对,用 git log 看看 main 的 HEAD 是哪——可能只是本地过时了。
Monorepo Explorer
pnpm turbo ls # 列出所有 workspace 包 pnpm turbo ls --filter="...@myorg/utils" # 所有依赖 utils 的包 pnpm turbo ls @myorg/web --dependencies # web 的依赖树
Turbo 2.x 新命令——不跑任务,只做 workspace 查询。排查"这个包被谁用了"时很方便。
本章小结
--filter三大写法:包名(pkg)、上下游(pkg.../...pkg)、分支对比([origin/main])- PR CI 推荐
--filter="...[origin/main]"——只跑受影响的包 turbo prune --docker裁 monorepo 为单 app 部署包,镜像/时间降一个量级- prune 产出 out/json(package.json 层)+ out/full(源码层),配合 Docker 分层缓存
fetch-depth: 0是 changed-since filter 的前提,CI 必须配