关于我
Monorepo 工程范式第一部分 · Rush 实战

选择器与 affected(大纲 · 待成文)

改一行代码,凭什么能只重跑受影响的那几个包?这一章讲透 Rush 的项目选择器与 affected 增量:它怎么在依赖图上圈出子集,又为什么说它是启发式剪枝、而非无漏网证明。

上一章我们把 Rush 装了起来、接上了 CI,那条 CI 里跑的是一句全量 rush build——每次提交,不管改了什么,都把全仓从头构建一遍。仓库小的时候没人在意,几十秒的事;可包一多,这个时间就跟着仓库规模往上涨——哪怕你只改了一个不起眼的工具函数,CI 仍要把整仓的 build、test、lint 从头跑一遍。这就是全量的代价:耗时只和仓库多大有关,和你这次改了多少无关。

对应的思路是增量:只处理这次改动真正波及的那一部分,改得少就跑得少。要做到这点,得先回答一个问题——改了某个包,到底会影响哪些包?这个问题依赖图已经替我们答了:它记录了所有项目之间"谁依赖谁"的关系,顺着这层关系,就能推出一次改动的波及范围。

这一章讲两件事:Rush 的项目选择器,一套在依赖图上圈出子集来跑命令的统一语法;以及建立在它之上的 affected 构建,根据 git diff 自动圈出这个子集,不用你手动指定。读完你会知道那句 rush build --to git:origin/main 到底做了什么,也会知道它为什么快——以及它快在哪、又漏在哪。

1. 为什么能只跑一个子集

依赖图既然记录了"谁依赖谁",那么自然可以反向推导出:改了某个包,会波及到谁。

用一张图看最直观。假设这次只改了 @ui/components,顺着依赖图往下推,需要执行校验的就是它自己加上依赖它的那些项目(下图绿色),其余跟这次改动无关的原样跳过(灰色):

@app/blog @ui/components(改动源) @infra/utils @app/admin @app/docs

图里绿色的三个——改动源 @ui/components,以及依赖它的 @app/blog、@app/admin——都要重新校验;@ui/components 用加粗的粉色描边单独标出,提示它是这次改动的起点。灰色的两个跟这次改动无关:@infra/utils 既没被改也不依赖改动源;@app/docs 只用到 @infra/utils,不受这次改动影响。两个都能跳过。

这里有一条规则要记住,后面几节都建立在它之上:波及是沿着依赖的反方向、往下游传播的。A 依赖 B,B 变了会影响 A,反过来 A 变了不影响 B。所以从改动源出发,要往"谁依赖我"的方向扩,而不是"我依赖谁"。

把这条规则落到操作上,就有了两个入口。一个是你自己在图上手动圈出子集——明确告诉工具"跑这个包和它的下游",这就是选择器;另一个是让工具根据 git diff 的结果自动确定这个子集——"这次改了这些文件,你自己算波及范围",这就是 affected。它们算的是同一件事,区别只在"谁来指定改动源":前者你说,后者 git 说。下面两节分别展开。

2. 选择器:在依赖图上圈一个子集

在依赖图上圈子集执行命令,是主流框架的通用能力(pnpm --filter、Turborepo --filter、Nx affected),本质都是从整张图过滤出一个子图,命令只在子图上跑。Rush 把它叫项目选择器;所有批量命令共用同一套参数,学一次到处用。

2.1 --to / --from

  • --to X = X + 它的所有上游依赖(往"我依赖谁"走)。
  • --from X = X 的所有下游 + 这些项目所需的上游(往"谁依赖我"走)。

2.2 --only / --impacted-by(unsafe)

  • --only X = 只有 X,依赖不管。
  • --impacted-by X = X + 下游,但不含它的依赖。
  • 这两个 + --impacted-by-except 标 unsafe:可能跑在未构建的依赖上而失败,赌错了 rush build 兜回。

2.3 selector 不止项目名

.(当前目录项目)/ git:<ref>(自某 ref 有改动的项目)/ tag:<name> / subspace:<name>。多条件可叠加,取并集(Rush 只做并集,不做减法)。

其中 git: 是通往 affected 的桥。


3. affected:让 git diff 自动圈出子集

affected 不是独立 flag,就是把 git:<base> 喂进选择参数——"手动指定"升级成"自动推断"。

3.1 一条命令

rush build --to git:origin/main

三步:git diff 出改了哪些文件 → 落到哪些项目 → 顺依赖图扩到下游。

3.2 放到 CI 上

对照第 4 章那条"全量 build" step:base 取目标分支,只验受影响子集。给一个可跑示例(git fetch base → rush build --to git:<base>)。

3.3 affected 是启发式剪枝,不是无漏网证明 ★深度落点

它只认两种输入:git 能 diff 到的源文件 + 依赖图上显式声明的边。漏网三类:

  1. 没写进 package.json 的幽灵依赖——图上没这条边。
  2. 非代码输入:配置 / 环境变量 / 生成产物,diff 与图都覆盖不到。
  3. 跨项目运行时耦合(约定、magic string)——图上没有对应的边。

归到母命题:affected 没消灭全量的必要,只是把"保证正确"的责任搬到"依赖显式、完整"这条纪律上——复杂度守恒的又一例。

有边界的判断:PR 上用 affected 拿快速反馈;主干合并 / 发布前留一道全量兜底。


4. AI 锚点

affected 圈出的子集 = AI 改完之后的"最小验证集":改了哪些项目,正好该回归谁,不必整仓重跑。埋点第 14 章(接进 AI 改动回路)。


小结

  1. 选择器是在依赖图上圈子集的统一语法;affected 是它 + git diff 的自动化。
  2. 工作量跟改动范围走,不跟仓库规模走——大仓 CI 还跑得动的前提。
  3. affected 是启发式剪枝,信它的前提是依赖图诚实完整;守不住就从"快"变成"漏"。
  4. 下一章:子空间——当"一份 lockfile"这个前提撑不住时怎么办。

成文核对清单(写作时用,成稿删)

  • 修正第 3 章 §4:旧表述"--from = 包 + 依赖它的下游"漏了"及其上游依赖";5.38.0 起 --from 会带上上游(旧版行为等同 --impacted-by)。
  • 纠偏埋点:第 3 章 §4 写"展开见第 6 章",实为本章。
  • §2.3 git: 机理:diff 工作区 vs 引用 commit → 改动文件路径 → 匹配 rush.json 项目文件夹(依据 selecting_subsets.md)。
  • §3.1 辨析 --changed-projects-only:rush build 另一个 unsafe flag,只建改动项目本身、不扩下游,和 selector 是两套东西。
  • §3.3 呼应第 3 章 §5 增量"输入"四类:源文件 / 依赖 / 上游产物 / 命令行参数。
  • §1 图成文时可换成正式静态配图。
  • 边界:CI 只讲到"引导 + 增量",构建缓存留第 13 章;subspace 留第 6 章。

On this page