Cypress Runner 包深度解析:cypress_runner.js 的打包架构、注入机制与迁移路线
@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 渐进式取代,但仍然拥有两项不可替代的职责:
- 产出生产环境的
cypress_runner.jsbundle——测试 iframe 中一切运行逻辑(driver + reporter)的来源; - 维护 runner 级样式——即
src/main.scss下聚合的 Legacy Cypress 样式。
README.md 进一步列出了它在被完全移除前必须解决的五项遗留工作,可作为理解其边界的最权威清单:
- 通过 webpack 打包
@packages/reporter与@packages/driver;一旦这些包可以被@packages/app直接导入,runner 即可移除; - 打包
@packages/reporter的样式(在main.scss中加载)——理想情况下 reporter 应自持样式; - 包含使用 webpack 专有 loader 的
dom.js,无法被@packages/app的 Vite 开发服务器直接导入; - 包含 Cypress Studio Recorder 相关代码(曾在 Cypress 9.x 中标记为实验特性,不属于 Cypress 10.x 首发范围,代码暂留此处但未被 app 使用);
- 包含大部分可被清理的 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.json 的 files 字段声明为发布产物,属于自动生成目录,不应手工编辑。
四个产物的架构地图与作用
主 runner:cypress_runner.js 的组装
入口链为 src/index.js → src/main.jsx → unified-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.js 与 injection_cross_origin.js
这两个 bundle 与前两个不同——它们由 webpack.config.ts 中的 getSimpleConfig()(即 @packages/web-config 提供的轻量配置)构建,强制 mode: 'production' 且刻意保持轻量、少依赖,因为它们是注入到 AUT 页面 <head> 里的脚本(由 @packages/proxy 注入,见源码注释)。
主 origin 注入 injection/main.js 的工作流:
- 通过
window.Cypress = parent.Cypress从父窗口(runner iframe)继承 Cypress 全局;若缺失则直接抛错; - 调用
patchXmlHttpRequest(window)对主 AUT frame 的 XHR 打补丁; - 当
Cypress.config('removeSRIAttributes')开启时,patchElementIntegrity(window)剥除<script>/<link>的 SRI 属性,避免被 proxy 改写过的第一方资源被 SRI 拦截; - 在 AUT 自身上下文内包裹定时器(
createTimers()+timers.wrap()),并用Cypress.on('app:timers:reset' / 'app:timers:pause')订阅父级事件——注释明确解释了为什么要在这里包裹而非在 driver 中:若在 driver 里做,timer 回调抛出的未捕获错误会被顶层 frame 的error处理器接走,而不是 AUT 的; - 最后通过
Cypress.action('app:window:before:load', window)通知父级 Cypress 实例。
跨域注入 injection/cross-origin.js 更为完整,涉及跨域桥接的核心机制:
- 寻找桥接 frame:
findCypress()遍历window.parent.frames,找到"定义了Cypress且与自身 origin 相同"的 frame 作为通信桥(用 try/catch 吞掉跨域访问抛出的SecurityErrorDOMException); - 监听三类事件:通过
message事件回显自身location.href;通过beforeunload向parent广播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.jsis served by@packages/serverto the browser"的集成关系。
构建配置细节与 Nx 隐式依赖
webpack 构建配置要点
webpack.config.ts 展示了若干值得留意的工程细节:
- prismjs 语法高亮:从
@packages/web-config的 commonConfig 中找到 babel-loader,向其插件列表追加babel-plugin-prismjs,为javascript / typescript / jsx / tsx四类语言启用line-numbers、line-highlight插件,且css: false(样式由 runner 自身 scss 控制); - 模块别名(alias):为
bluebird与lodash显式设置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 的注意事项同样值得展开:
- 已弃用:本包正被
@packages/app取代,不要新增功能,需要新能力请迁移到 packages/app; - 本地无法跑测试:
cypress:open/cypress:run已被显式禁用(脚本直接 exit 1),测试已迁至@packages/app; - 上游改动触发重建:上述三个隐式依赖中任一变化都会使 runner 的 Nx 缓存失效并触发重新打包;
dom.js与 Vite 不兼容:该文件依赖 webpack 专有 loader,无法被@packages/app的 Vite dev server 直接 import——这正是它至今仍留在 runner、无法迁走的技术原因;dist/不可手改:目录由 webpack 自动生成,任何手工改动都会在下次构建(含prebuild的rimraf ./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.md、AGENTS.md、README.md)反复传达的核心信息。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00