news 2026/9/20 13:21:44

Angular CLI 仓库的 Bazel 构建实践:Workspaces 依赖同步、Windows 兼容与 jasmine_node_test 调试指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Angular CLI 仓库的 Bazel 构建实践:Workspaces 依赖同步、Windows 兼容与 jasmine_node_test 调试指南

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/clipackages/angular_devkit/core这样的子包虽然各自声明依赖,但实际安装时共享同一棵依赖树。

在本文仓库的当前版本中,包管理已迁移为 pnpm,但 Workspaces 的理念被完整继承,见根目录 pnpm-workspace.yaml:它以packages:字段显式列出全部工作区成员(packages/angular/clipackages/angular_devkit/*packages/schematics/angularmodules/testing/buildertests等),并设置hoist: false,注释明确说明这是为了让 pnpm 在 Bazel 之外铺出的node_modulesrules_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.bzlnpm_translate_lock读取根目录pnpm-lock.yaml生成 Bazel 侧的 NPM 依赖仓库。
  • .bazelrc 中设置了build --symlink_prefix=dist/,将 Bazel 输出目录(bazel-outdist/bin等)统一符号链接到仓库根目录的dist/前缀下,避免污染源码树。
  • 根目录 package.json 提供"bazel": "bazelisk"脚本,因此仓库内统一用pnpm bazel来调用 Bazel(bazelisk 负责按.bazelversion下载匹配的 Bazel 版本)。

二、依赖同步规则:根 package.json 与子包 package.json 的“双份维护”

这是混合模式下最容易踩坑、也最需要遵守的规则:

  1. NPM 注册表依赖必须在根package.json声明。仓库中的yarn_install(现为npm_translate_lock)规则只读取根目录package.json(以及pnpm-lock.yaml)。因此,任何 Bazel 目标如果需要依赖从 NPM 下载的包,该依赖就必须出现在根package.json中,否则 Bazel 解析//:node_modules/<pkg>时会失败。

  2. 运行时依赖还需同步更新到对应子包的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.mtslatest-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=disabled

test: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 的要求检查:

  1. 新增 NPM 依赖:是否同时写入了根package.json(Bazel 解析用)?若是运行时依赖,子包package.json是否也同步声明(NPM 发布用)?
  2. Windows 兼容:Bazel 目标内所有 Node 文件查找是否都走了require.resolve?不要依赖 Linux 上符号链接带来的“恰好能用”。
  3. 调试本地聚焦测试:是否记得用--config=no-sharding(或--flaky_test_attempts=1)关闭 flaky 重跑,避免聚焦测试被反复执行?是否用--test_output=streamed关闭分片?
  4. 修改 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 13:21:41

豆包 LeetCode 96. 不同的二叉搜索树 Rust实现

LeetCode 96. 不同的二叉搜索树 Rust 实现 函数签名&#xff1a; pub fn num_trees(n: i32) -> i32 解法1&#xff1a;动态规划 O(n) rust pub struct Solution; impl Solution { pub fn num_trees(n: i32) -> i32 { let n n as usize; let mut dp vec![0; n 1]; dp[0…

作者头像 李华
网站建设 2026/9/20 13:21:40

Ruffle Flash 模拟器实战指南:3 条路让老游戏与老动画再次跑起来

Ruffle Flash 模拟器实战指南&#xff1a;3 条路让老游戏与老动画再次跑起来 【免费下载链接】ruffle A Flash Player emulator written in Rust 项目地址: https://gitcode.com/GitHub_Trending/ru/ruffle 打开五年前收藏的页面&#xff0c;动画该出现的位置只剩灰白方…

作者头像 李华
网站建设 2026/9/20 13:19:13

Excel Power Query自动获取股票历史数据实战指南

1. 为什么我最终放弃了手动更新股票数据做股票复盘这件事&#xff0c;我坚持了快六年。前三年一直用最笨的办法&#xff1a;每天收盘后打开行情软件&#xff0c;把自选股的收盘价、成交量、涨跌幅一个个敲进Excel表格里。十几只股票还好&#xff0c;后来自选池扩到五六十只&…

作者头像 李华
网站建设 2026/9/20 13:19:01

通达信L2资金流向函数实战:从原理到公式编写与参数调优

1. 先搞清楚L2资金流向到底在算什么很多人一看到“L2资金流向”就觉得是个黑箱&#xff0c;券商软件里红红绿绿的柱子&#xff0c;到底怎么来的&#xff1f;其实拆开看并不复杂。通达信的L2数据本质上是逐笔成交的委托队列快照&#xff0c;它比普通Level-1行情多了两样东西&…

作者头像 李华
网站建设 2026/9/20 13:18:14

把 OpenClaw 的 Base URL 改到 TaoToken 后,中间件怎么统计请求耗时

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华