Angular CLI 仓库的 Bazel 构建实践:Workspaces 依赖同步、Windows 兼容与 jasmine_node_test 调试指南
【免费下载链接】angular-cliCLI tool for Angular项目地址: https://gitcode.com/gh_mirrors/an/angular-cli
本文是 angular-cli 仓库 docs/process/bazel.md 的深度解读与实践指南,核心主题是:在基于 pnpm/Yarn Workspaces 的 monorepo 中引入 Bazel 构建系统后,如何正确同步 NPM 依赖、如何在 Windows 上规避 runfiles 解析陷阱,以及如何高效调试jasmine_node_test测试目标。读完本文,你将掌握该仓库依赖声明的双份同步规则、require.resolve的跨平台意义,以及一套可直接复制的远程断点调试命令。
一、背景:Workspaces 与 Bazel 的“混搭”架构
1.1 包管理基础:Yarn Workspaces
angular-cli仓库最初采用 Yarn workspaces 组织多包架构:仓库中各个package.json的依赖被统一链接并安装在一起,形成一个共享的node_modules。这意味着像packages/angular/cli、packages/angular_devkit/core这样的子包虽然各自声明依赖,但实际安装时共享同一棵依赖树。
在本文仓库的当前版本中,包管理已迁移为 pnpm,但 Workspaces 的理念被完整继承,见根目录 pnpm-workspace.yaml:它以packages:字段显式列出全部工作区成员(packages/angular/cli、packages/angular_devkit/*、packages/schematics/angular、modules/testing/builder、tests等),并设置hoist: false,注释明确说明这是为了让 pnpm 在 Bazel 之外铺出的node_modules与rules_js在 Bazel 内部铺出的结构保持一致(不产生隐藏的node_modules/.pnpm/node_modules)。此外还设置了autoInstallPeers: false,避免 pnpm 自动安装 peer 依赖,确保版本完全显式可控。
1.2 Bazel 的引入与约束
后来,仓库引入 Bazel 来管理部分构建依赖。但Bazel 并不支持 Yarn Workspaces,因为 Bazel 需要铺设多个node_modules目录(每个 Bazel 包在自己的执行环境中解析依赖),这与 Workspaces 的单一共享node_modules模型冲突。因此仓库长期处于“混合模式”(mixed mode):pnpm 负责工作区安装,Bazel 负责构建与测试。在这种模式下,开发者必须格外小心地同步依赖声明。
从仓库的 Bazel 配置可以印证这一点:
- MODULE.bazel 声明了
rules_nodejs(6.7.5)、aspect_rules_js(3.4.1)、aspect_rules_ts(3.10.1)、aspect_rules_jasmine(2.0.4)、aspect_rules_esbuild等规则集,并通过@aspect_rules_js//npm:extensions.bzl的npm_translate_lock读取根目录pnpm-lock.yaml生成 Bazel 侧的 NPM 依赖仓库。 - .bazelrc 中设置了
build --symlink_prefix=dist/,将 Bazel 输出目录(bazel-out、dist/bin等)统一符号链接到仓库根目录的dist/前缀下,避免污染源码树。 - 根目录 package.json 提供
"bazel": "bazelisk"脚本,因此仓库内统一用pnpm bazel来调用 Bazel(bazelisk 负责按.bazelversion下载匹配的 Bazel 版本)。
二、依赖同步规则:根 package.json 与子包 package.json 的“双份维护”
这是混合模式下最容易踩坑、也最需要遵守的规则:
NPM 注册表依赖必须在根
package.json声明。仓库中的yarn_install(现为npm_translate_lock)规则只读取根目录的package.json(以及pnpm-lock.yaml)。因此,任何 Bazel 目标如果需要依赖从 NPM 下载的包,该依赖就必须出现在根package.json中,否则 Bazel 解析//:node_modules/<pkg>时会失败。运行时依赖还需同步更新到对应子包的
package.json。如果某个依赖在运行时(非 devDependencies)也被使用,那么所在包的package.json也必须同步声明。原因很实际:当用户从 NPM 下载发布版本时,他们不会使用 Bazel,只能靠子包自身的package.json来安装依赖。保持两个package.json同步是开发者的责任。
这条规则的底层原因可以从 MODULE.bazel 的npm_translate_lock调用看出:它的data参数不仅包含//:package.json和//:pnpm-workspace.yaml,还显式列出了//modules/testing/builder:package.json、//packages/angular/cli:package.json等全部子包。也就是说,Bazel 侧依赖解析完全以这些package.json的锁定结果为输入,根与子包的声明缺一不可。
从源码看依赖的 Bazel 声明方式
以核心包为例,packages/angular/cli/BUILD.bazel 中ts_project目标的deps形如:
deps = [ ":node_modules/@angular-devkit/architect", ":node_modules/@angular-devkit/core", ":node_modules/@inquirer/prompts", "//:node_modules/@types/node", "//:node_modules/typescript", ... ]其中:node_modules/...来自该 BUILD 文件顶部的npm_link_all_packages()(由@npm//:defs.bzl生成,链接本包package.json中声明的依赖),而//:node_modules/...则链接根package.json中声明的依赖。这种区分正是上述“双份同步”规则在 BUILD 文件中的直观体现:本包声明的走:node_modules,根声明的走//:node_modules。
发布时的包依赖治理
发布侧同样有保障机制:tools/bazel/npm_package.bzl 与 packages/angular/cli/BUILD.bazel 中的npm_package(name = "pkg")通过pkg_deps声明该包对@angular-devkit/*、@schematics/angular等兄弟包的依赖关系;scripts/release-checks/dependency-ranges/ 目录下的发布检查脚本(peer-deps-check.mts、latest-versions-check.mts)会在发布前校验依赖范围,防止发布出去的包出现依赖缺失或版本漂移。
三、Windows 支持:为什么必须使用require.resolve
3.1 runfiles 机制与require.resolve
在 Bazel 中,任何形式的 Node 文件查找都应该使用require.resolve。这是因为rules_nodejs借助 Bazel 的runfiles机制来解析路径:一个给定的 Bazel 目标只能访问其依赖产生的输出,而不是整个仓库任意路径下的文件。
3.2 Linux 与 Windows 的差异
- Linux:Bazel 会在目标实际运行的位置铺设一层symlink forest(符号链接树),把依赖文件链接到位。由于文件确实“在那里”,无论是否使用
require.resolve,大部分路径都能被正确解析。因此,漏写require.resolve在 Linux 上几乎不会被察觉。 - Windows:Bazel 默认不铺设 symlink forest。部分文件(如其他规则产出的构建产物)即使不用
require.resolve也能找到,但node_modules 依赖和数据文件必须通过 runfiles 目录查找,此时就只能依赖require.resolve。
3.3 后果:问题延迟到 Windows 才爆发
由于 Linux 上要求宽松、Windows 上要求严格,实际发生的情况是:代码中缺少require.resolve的问题在 Linux 上长期潜伏,直到有人尝试在 Windows 上运行时才突然崩溃。这是本仓库开发流程中反复强调“一律使用require.resolve”的根本原因。
仓库源码可以佐证这一约定已成为实践:例如 packages/angular/build/src/tools/esbuild/javascript-transformer.ts 使用require.resolve('./javascript-transformer-worker')定位 worker 文件;packages/angular/build/src/builders/dev-server/tests/setup.ts 通过require.resolve('@angular/build/package.json')反查包根目录。这些用法正是为了保证在 Bazel 的 runfiles 布局(尤其是 Windows 下)中也能稳定解析。
补充:关于 Windows 开发环境,docs/DEVELOPER.md 还额外建议在 Windows 上贡献代码时使用 WSL(Windows Linux Subsystem),并在 WSL 内按 Linux 流程操作;但
ngCLI 对终端用户的 Windows 支持仍然完整并持续经过测试。
四、调试 jasmine_node_test:从沙箱到断点
4.1 关闭沙箱、找到中间产物
在 Linux 上,Bazel 测试默认在沙箱(sandbox)中运行以实现隔离。调试时需要关闭沙箱:
- 在规则上添加
local = True属性; - 或在命令行强制本地执行并流式输出:
--test_output=streamed。
关闭沙箱后,中间测试文件位于bazel-out/k8-fastbuild/bin目录下,紧接着是测试目标路径(即<workspace>_test/<包路径>/<目标名>结构,具体以--symlink_prefix=dist/配置下dist/bin的实际布局为准)。
4.2 分片(sharding)与 flaky 重试的坑
shard_count分片:如果测试被分片(例如shard_count = 4),而你在本地减少或聚焦了测试用例(如用fit/fdescribe聚焦),部分分片可能被执行 0 个测试,导致测试进程以错误码退出、整条命令失败。仓库中确实大量使用分片:例如 packages/angular/build/BUILD.bazel 的shard_count = 4,以及 packages/angular_devkit/build_angular/BUILD.bazel 中根据LARGE_SPECS配置为不同规格测试分配shards数量。flaky = True重试:标记为 flaky 的测试失败后会自动重跑。由于聚焦测试(focused tests)会让 jasmine 以非零码退出,被标记 flaky 的测试会被反复重跑,造成大量无意义输出。仓库中也有多处flaky = True的真实用例,见 packages/angular/build/BUILD.bazel 与 packages/angular/ssr/test/BUILD.bazel。
调试期间的两种对策:
- 在 BUILD 文件中临时移除
shard_count(取消分片)与flaky(取消重试)属性; - 或完全通过命令行参数解决:
--test_output=streamed会禁用分片;--flaky_test_attempts=1会禁用 flaky 测试的重跑。
命令行方案的好处是不需要改动 BUILD 文件,也便于与--config=debug组合使用(见下节)。此外,仓库根目录 .bazelrc 中已经预置了test:no-sharding配置:--flaky_test_attempts=1 --test_sharding_strategy=disabled,其注释明确指出这是为配合fit/fdescribe聚焦调试而准备的。
4.3 远程调试配置(--config=debug)
仓库在 .bazelrc 中预置了完整的调试配置段:
test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results test:no-sharding --flaky_test_attempts=1 --test_sharding_strategy=disabledtest:debug的含义逐项拆解:
| 参数 | 作用 |
|---|---|
--test_arg=--node_options=--inspect-brk | 让测试进程以--inspect-brk启动 Node,等待调试器附加 |
--test_output=streamed | 流式输出测试日志,同时禁用分片 |
--test_strategy=exclusive | 独占执行,避免并行干扰断点 |
--test_timeout=9999 | 测试不会因超时被杀死 |
--nocache_test_results | 强制真实重跑,不命中缓存结果 |
于是,针对 CLI 包测试目标的调试命令为:
# 使用 --config=debug 启动远程调试(会暂停等待调试器附加) pnpm bazel test --config=debug //packages/angular/cli:angular-cli_test # 同时禁用被标记为 flaky 的测试的重跑 pnpm bazel test --config=debug --config=no-sharding //packages/angular/cli:angular-cli_test第二条命令组合了两个配置:--config=debug负责断点与流式输出,--config=no-sharding通过--flaky_test_attempts=1关闭 flaky 重试(此时 sharding 已被 streamed 关闭,no-sharding 主要起兜底作用)。命令中的目标名//packages/angular/cli:angular-cli_test对应 packages/angular/cli/BUILD.bazel 中的jasmine_test(name = "test", ...)——注意实际目标名是test,仓库文档与命令示例中的angular-cli_test为历史/示例命名,以你仓库中pnpm bazel query "tests(//packages/...)"的输出为准。
4.4 jasmine_test 封装与 Chrome 断点体验
仓库通过 tools/defaults.bzl 中的jasmine_test封装了@aspect_rules_jasmine的规则:它会自动注入CHROME_BIN/CHROME_PATH/CHROMEDRIVER_BIN环境变量与 Chromium 工具链(供涉及浏览器的测试使用),并固定传入--require=../node_modules/source-map-support/register.js和**/*spec.{js,mjs,cjs}的测试匹配参数。调试时启动--config=debug后,Node 会暂停在--inspect-brk断点,你可以:
- 在 IDE 的 Node 调试配置中附加到
chrome://inspect列出的测试进程; - 或使用
debugger;语句在感兴趣的源码处设置硬断点(CLI 会动态require()文件,预先设断点不一定可靠,这是 docs/DEVELOPER.md 推荐的兜底方案)。
五、快速自查清单
在向仓库提交涉及 Bazel 的改动前,对照 docs/process/bazel.md 的要求检查:
- 新增 NPM 依赖:是否同时写入了根
package.json(Bazel 解析用)?若是运行时依赖,子包package.json是否也同步声明(NPM 发布用)? - Windows 兼容:Bazel 目标内所有 Node 文件查找是否都走了
require.resolve?不要依赖 Linux 上符号链接带来的“恰好能用”。 - 调试本地聚焦测试:是否记得用
--config=no-sharding(或--flaky_test_attempts=1)关闭 flaky 重跑,避免聚焦测试被反复执行?是否用--test_output=streamed关闭分片? - 修改 BUILD 属性:不要忘记
local = True只应在调试时临时使用;shard_count/flaky的移除应在上交前还原。
六、相关阅读
- 构建与测试总览:docs/DEVELOPER.md
- 发布流程中的 Bazel 打标签(stamp)配置:docs/process/release.md
- Bazel 全局构建/测试配置:.bazelrc
- 依赖锁定与规则声明:MODULE.bazel、pnpm-workspace.yaml、package.json
- 测试规则封装:tools/defaults.bzl
- 包 BUILD 示例:packages/angular/cli/BUILD.bazel
【免费下载链接】angular-cliCLI tool for Angular项目地址: https://gitcode.com/gh_mirrors/an/angular-cli
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考