最基础:按名字选
pnpm --filter @myorg/web build pnpm -F @myorg/web build # 简写 # glob 模式 pnpm -F "@myorg/*" build # 所有 @myorg 下的包 pnpm -F "./apps/*" build # apps 目录下所有包(按路径)
依赖图过滤语法
| 语法 | 含义 | 例子 |
|---|---|---|
pkg | 只这个包 | -F @myorg/web |
pkg... | 这个包 + 它依赖的所有(包含自己) | -F "@myorg/web..." |
...pkg | 这个包 + 所有依赖它的 | -F "...@myorg/ui" |
pkg^... | 只它的依赖(不含自己) | -F "@myorg/web^..." |
...^pkg | 只依赖它的(不含自己) | -F "...^@myorg/ui" |
记忆口诀
... 在后面就是「往下看」(依赖树);在前面就是「往上看」(被谁依赖)。加 ^ 表示不包含自己。
直观例子
依赖关系: web → ui → utils docs → ui → utils # 发 ui 改动 → 要先构建 utils 保证最新,再构建 ui pnpm -F "@myorg/ui..." build # 命中:utils, ui # 发 ui 改动 → 下游 web/docs 也要重测 pnpm -F "...@myorg/ui" test # 命中:ui, web, docs # ui 改了,我想一次搞完上下游 pnpm -F "...@myorg/ui..." build # 命中:utils, ui, web, docs(整条链)
按路径过滤
# 所有在 apps 下的包 pnpm -F "./apps/**" build # 某个具体路径 pnpm -F "./packages/ui" test
glob 相对工作目录;名字和路径两种写法可以混用。
按变更过滤(CI 的灵魂)
# 相对 main 分支有改动的包 pnpm -F "...[origin/main]" build # 改动 + 下游都测 pnpm -F "...[origin/main]" test # 最近一次 commit 开始算 pnpm -F "...[HEAD~1]" build
[ref] 语法让 pnpm 用 git 比较:从 ref 到当前 HEAD 变动过的包——结合 ... 自动扩展到下游。
changed-since 是 CI 加速的秘诀
PR CI 只对改动影响的包跑 build/test,其余跳过。100 个包的 monorepo 改 1 个包,CI 从 10 分钟降到 30 秒。配合 Turborepo 缓存更省。
PR CI 只对改动影响的包跑 build/test,其余跳过。100 个包的 monorepo 改 1 个包,CI 从 10 分钟降到 30 秒。配合 Turborepo 缓存更省。
排除
# 除了 docs 都跑 pnpm -F "!@myorg/docs" build # 组合:所有变更包,但不包含 docs pnpm -F "...[origin/main]" -F "!@myorg/docs" build
并行和拓扑
pnpm -r build # 拓扑序(被依赖先跑) pnpm -r --parallel dev # 忽略拓扑,全部一起 pnpm -r --workspace-concurrency=4 # 最多 4 个并发 pnpm -r --no-bail test # 遇到错误继续跑其它
几个常见 CI 模式
1. Build 只跑受影响的
# .github/workflows/ci.yml - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 # 需要完整 git 历史 - run: pnpm install --frozen-lockfile - run: pnpm -F "...[origin/main]" lint - run: pnpm -F "...[origin/main]" test - run: pnpm -F "...[origin/main]" build
2. 发布前只发改动的
# 只对修改过的 package 跑 changeset version / publish pnpm changeset publish # Changesets 自己识别谁变了、谁没变——很聪明
3. 启动开发环境
pnpm -F "@myorg/web..." --parallel dev # 启动 web 和它依赖的所有包(watch mode)
--stream 看每个包的输出
pnpm -r --stream build # 每行前面带包名前缀,多个输出交织但能区分 pnpm -r --reporter=append-only build # CI 用,不显示进度条,日志更干净
exec:在 workspace 里跑任意命令
# 在 @myorg/web 目录下跑 ls pnpm -F @myorg/web exec ls # 所有 workspace 跑 eslint pnpm -r exec eslint src
exec 不依赖 package.json 的 scripts——直接跑命令,常用在临时操作。
run 的区别
| 命令 | 用途 |
|---|---|
pnpm -F pkg build | 跑 pkg 的 scripts.build |
pnpm -F pkg run build | 同上,显式 |
pnpm -F pkg exec eslint . | 跑任意命令,不查 scripts |
pnpm -F pkg add axios | 给 pkg 加依赖 |
filter 语法支持正则风格
pnpm -F "@myorg/{ui,utils}" test pnpm -F "@myorg/*[test]" test # 名字带 test 的 pnpm -F "!@myorg/*-config" build # 排除 -config 结尾
cwd 作为隐式 filter
# cd 到 apps/web 目录 pnpm build # 等价于 pnpm -F @myorg/web build(只跑当前包)
在子包目录里执行的命令默认只作用于当前包——符合直觉。
排错:我的 filter 不生效
# 先用 list 确认命中哪些 pnpm -F "...[origin/main]" ls --depth=-1 # 或 why 看依赖关系 pnpm why @myorg/ui
命中 0 个
检查 glob 是否需要引号(shell 会先展开);
[ref] 要求 git 能 resolve,浅克隆会命中不到——CI 用 fetch-depth: 0。依赖没被带上
确认 package.json 的 dependencies 用
workspace:* 声明;写成 ^ 可能匹配到 npm 版本反而不算 workspace 内部依赖。改了 README 也触发 build
ignored-changed-files-patterns 可忽略某些文件变化,或在 CI 用 changed-files-ignore-pattern=**/*.md。忽略特定文件的变化
# .npmrc changed-files-ignore-pattern[]=**/*.md changed-files-ignore-pattern[]=**/*.test.ts
CI 中,当只改了 README/测试文件时不触发下游重建。
真实 monorepo 的 CI 设计
jobs: lint: steps: - uses: actions/checkout@v4 with: { fetch-depth: 0 } - uses: pnpm/action-setup@v4 - run: pnpm install --frozen-lockfile - run: pnpm -F "...[origin/main]" lint test: needs: lint steps: - uses: actions/checkout@v4 with: { fetch-depth: 0 } - uses: pnpm/action-setup@v4 - run: pnpm install --frozen-lockfile - run: pnpm -F "...[origin/main]" test build: needs: test steps: - run: pnpm -F "...[origin/main]" build - run: pnpm -F "@myorg/web" deploy
三阶段 pipeline,每阶段只对改动影响到的包操作——100 包仓库改 1 个,5 分钟变 40 秒。
本章小结
pkg...向下(含依赖),...pkg向上(含反向依赖),^不含自己...[origin/main]是 CI 加速的秘诀——只跑 git 变更影响到的包!pattern排除,支持{a,b}多选和路径 glob-r --parallel忽略拓扑,--workspace-concurrency控制并发数- CI 用
fetch-depth: 0确保 git 历史完整,否则 changed-since 会命中 0 个