首页
/ Cypress Runner 包深度解析:cypress_runner.js 的打包架构、注入机制与迁移路线

Cypress Runner 包深度解析:cypress_runner.js 的打包架构、注入机制与迁移路线

2026-09-08 19:42:13作者:史锋燃Gardner

@packages/runner 是 Cypress 仓库中负责把 @packages/driver(测试驱动)与 @packages/reporter(结果报告器)打包成可在测试 iframe 中运行的 webpack 产物的历史包,生产环境所依赖的 cypress_runner.js 仍由它产出。本文将结合 runner 包内说明 与真实源码,讲解该包的职责边界、四个 webpack 产物、加载与注入链路、跨域 iframe 通信,以及它被 @packages/app 渐进取代的现状,帮助你理解 Cypress 运行时 iframe 的组装原理。

Runner 包的角色定位与两个"遗留"职责

AGENTS.md 中,runner 被明确描述为 a legacy webpack bundle:它把 @packages/reporter@packages/driver 打包到一起,供 Cypress 的测试 iframe 使用。目前它正被 @packages/app 渐进式取代,但仍然拥有两项不可替代的职责:

  1. 产出生产环境的 cypress_runner.js bundle——测试 iframe 中一切运行逻辑(driver + reporter)的来源;
  2. 维护 runner 级样式——即 src/main.scss 下聚合的 Legacy Cypress 样式。

README.md 进一步列出了它在被完全移除前必须解决的五项遗留工作,可作为理解其边界的最权威清单:

  1. 通过 webpack 打包 @packages/reporter@packages/driver;一旦这些包可以被 @packages/app 直接导入,runner 即可移除;
  2. 打包 @packages/reporter 的样式(在 main.scss 中加载)——理想情况下 reporter 应自持样式;
  3. 包含使用 webpack 专有 loader 的 dom.js,无法被 @packages/app 的 Vite 开发服务器直接导入;
  4. 包含 Cypress Studio Recorder 相关代码(曾在 Cypress 9.x 中标记为实验特性,不属于 Cypress 10.x 首发范围,代码暂留此处但未被 app 使用);
  5. 包含大部分可被清理的 Legacy Cypress 样式。

从上述内容可以得出一个明确的工程信号:不要在此包新增功能,新功能应迁往 @packages/app

关键构建命令与产物清单

构建入口在 package.json,核心命令如下:

# 开发构建(webpack 打包,输出到 dist/)
yarn workspace @packages/runner build

# 生产构建(NODE_ENV=production,压缩产物)
yarn workspace @packages/runner build-prod

# watch 模式(带 --progress)
yarn workspace @packages/runner watch

值得注意的脚本细节:

  • prebuild 会先执行 rimraf ./dist,确保每次构建从干净目录开始;
  • build 实际执行的是根目录下的 node ../../scripts/run-webpack,即由仓库级 webpack 封装脚本驱动打包;
  • build-prod 通过 cross-env 注入 NODE_ENV=production 后再触发 build
  • postinstall 会提示 @packages/runner needs: yarn build,说明该包源码不随安装生效,必须显式构建;
  • cypress:open / cypress:run 两个脚本被故意改写为输出提示后以退出码 1 终止:These tests have been moved to @packages/app——从本包直接跑测试会报错,因为测试已被迁移到 @packages/app

webpack.config.ts 中可以看到,一次完整构建会同时产出 4 个 bundle,全部输出到 dist/

webpack entry 产物文件名 入口源文件 用途
cypress_runner dist/cypress_runner.js src/index.js 主测试 iframe 中运行的 driver + reporter 聚合包
cypress_cross_origin_runner dist/cypress_cross_origin_runner.js src/cross-origin.js 跨域(secondary origin)iframe 中的 driver
injection dist/injection.js injection/main.js 注入到主 AUT 文档 <head> 的启动脚本
injection_cross_origin dist/injection_cross_origin.js injection/cross-origin.js 注入到跨域 AUT 文档的启动脚本

此外通过 CopyWebpackPlugin 会把 @packages/icons 提供的 favicon.ico 一并复制到 dist/(配置见 webpack.config.ts)。dist/package.jsonfiles 字段声明为发布产物,属于自动生成目录,不应手工编辑。

四个产物的架构地图与作用

主 runner:cypress_runner.js 的组装

入口链为 src/index.jssrc/main.jsxunified-runner.tsx

// src/index.js
import './main.scss'   // runner 级样式(含 Legacy Cypress 样式)
import './main.jsx'
// src/main.jsx
import { UnifiedRunner } from '../unified-runner'
window.UnifiedRunner = UnifiedRunner

真正的组装发生在 unified-runner.tsx。它从 @packages/driver 取到 $Cypress(即 Cypress 驱动本体及其 $ 即 jQuery 引用),从 @packages/reporter/src/main 取到 Reporter,并暴露短命令、setReporterDocument、React、MobX 等运行时依赖,最终整体挂到 window.UnifiedRunner

export const UnifiedRunner = {
  CypressJQuery: $Cypress.$,
  CypressDriver: $Cypress,
  shortcuts,
  setReporterDocument,
  React,
  MobX,
  ReactDOM: { createRoot },
  Reporter,
}

也就是说,cypress_runner.js 是 Cypress 运行时 iframe 的"内核":driver 负责命令执行与浏览器自动化,reporter 负责测试结果的界面渲染,runner 只是把二者用 webpack 粘合起来并提供共享的 React/MobX 运行环境。该文件由 @packages/server 提供给浏览器(详见下文"加载链路")。

跨域 runner:cypress_cross_origin_runner.js

src/cross-origin.js 全文只有一行:

// this is the entry point for the cross-origin version of the driver
import '@packages/driver/src/cross-origin/cypress'

它本质上是 driver 的 cross-origin 版本的入口别名:当 AUT 存在跨域(不同 origin 的)iframe 时,该 bundle 让 Cypress 在次级 origin 的 iframe 中也能运行命令(如跨域 cy.visit / 交互),其通信细节由 driver 内的 cross-origin 实现承担。

注入脚本:injection.jsinjection_cross_origin.js

这两个 bundle 与前两个不同——它们由 webpack.config.ts 中的 getSimpleConfig()(即 @packages/web-config 提供的轻量配置)构建,强制 mode: 'production' 且刻意保持轻量、少依赖,因为它们是注入到 AUT 页面 <head> 里的脚本(由 @packages/proxy 注入,见源码注释)。

主 origin 注入 injection/main.js 的工作流:

  1. 通过 window.Cypress = parent.Cypress 从父窗口(runner iframe)继承 Cypress 全局;若缺失则直接抛错;
  2. 调用 patchXmlHttpRequest(window) 对主 AUT frame 的 XHR 打补丁;
  3. Cypress.config('removeSRIAttributes') 开启时,patchElementIntegrity(window) 剥除 <script>/<link> 的 SRI 属性,避免被 proxy 改写过的第一方资源被 SRI 拦截;
  4. AUT 自身上下文内包裹定时器(createTimers() + timers.wrap()),并用 Cypress.on('app:timers:reset' / 'app:timers:pause') 订阅父级事件——注释明确解释了为什么要在这里包裹而非在 driver 中:若在 driver 里做,timer 回调抛出的未捕获错误会被顶层 frame 的 error 处理器接走,而不是 AUT 的;
  5. 最后通过 Cypress.action('app:window:before:load', window) 通知父级 Cypress 实例。

跨域注入 injection/cross-origin.js 更为完整,涉及跨域桥接的核心机制:

  • 寻找桥接 framefindCypress() 遍历 window.parent.frames,找到"定义了 Cypress 且与自身 origin 相同"的 frame 作为通信桥(用 try/catch 吞掉跨域访问抛出的 SecurityError DOMException);
  • 监听三类事件:通过 message 事件回显自身 location.href;通过 beforeunloadparent 广播 cross:origin:before:unload(即便对应 spec bridge 尚未建立也要通知);通过 error 事件把未处理异常转发到 window.top(消息 cross:origin:aut:throw:error);
  • 按需打补丁patchDocumentCookie(模拟 cookie 场景)、patchFetch / patchXmlHttpRequest(跟踪凭据使用),且当 modifyObstructiveCode 开启时用 Object.defineProperty(window, 'frameElement', { get: () => null }) 伪装"未被 iframe 包裹",规避页面中的反 iframe 检测代码;
  • 延迟挂接:把真正的 Cypress 挂接封装成 window.__attachToCypress(Cypress),等待 spec bridge(跨域 runner)创建完成后由 Cypress 侧调用,调用后自毁(delete window.__attachToCypress),以规避 Cypress 全局尚未就绪时的竞态(代码中同时检查 Cypress && Cypress.cy)。

注:runner 包内 injection/ 目录还包含 timers、SRI integrity 以及跨域 cookie/fetch/XHR 的具体补丁实现,见 injection/patches

运行时加载链路:从 iframe 到 cypress_runner.js

runner 打包出的 dist/cypress_runner.js@packages/server 提供给浏览器。在 static/index.html 中可以看到典型的 runner iframe 页面结构:

<link rel="stylesheet" href="/{{namespace}}/runner/cypress_runner.css">
<div id="app"></div>
<script type="text/javascript" src="/{{namespace}}/runner/cypress_runner.js"></script>
<script type="text/javascript">
  // set a global so we know the 'top' window
  window.__Cypress__ = true
  setTimeout(function () {
    Runner.start(document.getElementById('app'), "{{base64Config | safe}}")
  }, 0)
</script>

要点解读:

  • 页面以 {{projectName}}{{namespace}}{{base64Config | safe}} 等模板变量注入项目名、命名空间与 base64 编码的运行配置,说明该 HTML 由服务端渲染/插值后下发;
  • window.__Cypress__ = true 标记当前窗口为顶层 Cypress 运行窗口,这也是 injection 脚本里 parent.Cypress 能取到驱动的依据之一;
  • 样式与脚本均以 /<namespace>/runner/ 为前缀,指向 server 托管的 runner 静态资源;
  • 由此印证 AGENTS.md 中"Built dist/cypress_runner.js is served by @packages/server to the browser"的集成关系。

构建配置细节与 Nx 隐式依赖

webpack 构建配置要点

webpack.config.ts 展示了若干值得留意的工程细节:

  • prismjs 语法高亮:从 @packages/web-config 的 commonConfig 中找到 babel-loader,向其插件列表追加 babel-plugin-prismjs,为 javascript / typescript / jsx / tsx 四类语言启用 line-numbersline-highlight 插件,且 css: false(样式由 runner 自身 scss 控制);
  • 模块别名(alias):为 bluebirdlodash 显式设置 require.resolve 别名,避免多实例或版本漂移;
  • 异步默认导出:构建前会 await waitUntilIconsBuilt()(见 scripts/ensure-icons),确保 favicon 等图标资产先构建完成,再读取 @packages/icons 路径进行拷贝,规避资源竞态。

Nx 隐式依赖与缓存失效

由于 driver、reporter、config 的源码会被直接打进 dist/cypress_runner.js,runner 必须向 Nx 声明这些隐式依赖,否则改动上游源码时缓存不会被正确失效、产物将过期。见 package.json

"nx": {
  "implicitDependencies": [
    "@packages/driver",
    "@packages/reporter",
    "@packages/config"
  ]
}

README.md 中的 Implicit Dependencies 一节也专门解释了这一点:正因为这些包的源码被打包进 runner 产物,必须让 Nx 感知它们的变化以触发重建。这是理解"改 driver/reporter 代码后 runner 为何自动重编"的关键。

开发者注意事项(Gotchas)

来自 AGENTS.md 的注意事项同样值得展开:

  1. 已弃用:本包正被 @packages/app 取代,不要新增功能,需要新能力请迁移到 packages/app
  2. 本地无法跑测试cypress:open/cypress:run 已被显式禁用(脚本直接 exit 1),测试已迁至 @packages/app
  3. 上游改动触发重建:上述三个隐式依赖中任一变化都会使 runner 的 Nx 缓存失效并触发重新打包;
  4. dom.js 与 Vite 不兼容:该文件依赖 webpack 专有 loader,无法被 @packages/app 的 Vite dev server 直接 import——这正是它至今仍留在 runner、无法迁走的技术原因;
  5. dist/ 不可手改:目录由 webpack 自动生成,任何手工改动都会在下次构建(含 prebuildrimraf ./dist)中被清除。

@packages/app 的关系及未来走向

理解 runner 最需要把握的一条主线是它的过渡态地位

  • 历史形态:driver 与 reporter 由 runner 用 webpack 打包后整体塞进测试 iframe,形成 cypress_runner.js
  • 演进方向:@packages/app 是新一代聚合层,最终希望直接 import driver/reporter(并可用 Vite 开发),从而让 runner 这个"webpack 粘合层"退役;
  • 现状约束:在 dom.js 摆脱 webpack 专有 loader、reporter 能自持样式、Studio 代码去向明确之前,runner 仍承担着生产环境 bundle 与样式职责。

因此,阅读 runner 相关代码时,应把它视作理解 Cypress 测试 iframe 运行时的真实产物生成链路的入口,同时在概念上接受其"渐进退场"的定位——这正是 packages/runner 三个说明文档(CLAUDE.mdAGENTS.mdREADME.md)反复传达的核心信息。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391