Cypress 单仓库 ESM 迁移实战:TypeScript 化、Vitest 化与 ESM/CJS 双格式构建
本文基于 Cypress 官方维护的迁移指南 guides/esm-migration.md,完整拆解 Cypress monorepo 从 CommonJS 向 ES Module 演进的四阶段路线图。读完后你将了解:为什么 Cypress 内部包要逐一转为 TypeScript 与 Vitest、v8-snapshot 的旧式模块解析如何成为 exports 字段落地的阻碍、以及 scaffold-config、socket、telemetry 三个包是如何以“多入口、多 tsconfig”的方式构建出 browser/node 双端产物的。
迁移总览:四个阶段的路线图
Cypress 的 ESM 迁移被划分为四个依次推进的阶段,当前仓库代码与文档中的状态勾选共同反映了这一进度:
- Phase 1:将各包转换为 TypeScript——覆盖所有 NPM 包与 binary 包;
- Phase 2:将各包的单测从 Mocha 迁移到 Vitest——接近全部完成;
- Phase 3:为 NPM 包打包 ESM/CJS 双版本——文档标注“细节将在 Phase 2 结束后明确”;
- Phase 4:让 Cypress server 以 ESM 包形式运行——同样标注为待定。
整体思路清晰:先把源码统一到 TypeScript、把测试统一到 Vitest,消除语言与测试框架层面的历史包袱,再谈产物格式(ESM/CJS)的统一。因为一个包的构建产物能否被 v8-snapshot 正确解析,取决于它的源码与配置是否已经完全“模块化”。
Phase 1:包级 TypeScript 转换
验收标准
原文档给出了判断一个包是否完成 Phase 1 的三条硬标准:
.ts文件中不允许再出现require语句;- 该包内的单元测试必须用 TypeScript 编写;
- 该包不包含 scripts 或 system test 的迁移(这两类留在仓库顶层统一管理)。
迁移状态清单
以下状态直接继承自 guides/esm-migration.md,并结合当前仓库结构核对。
NPM 包(npm/ 目录与 cli):
| 包 | 状态 |
|---|---|
| cli | 已完成 |
| npm/angular | 已完成 |
| npm/cypress-schematic | 已完成 |
| npm/eslint-plugin-dev | 未完成(Phase 2 已迁至 Vitest) |
| npm/grep | 已完成 |
| npm/mount-utils | 已完成 |
| npm/puppeteer | 已完成 |
| npm/react | 已完成 |
| npm/svelte | 已完成 |
| npm/vite-dev-server | 已完成 |
| npm/vite-plugin-cypress-esm | 已完成 |
| npm/vue | 已完成 |
| npm/webpack-batteries-included-preprocessor | 已完成 |
| npm/webpack-dev-server | 已完成 |
| npm/webpack-preprocessor | 已完成 |
Binary 包(packages/ 目录,打进 Cypress 二进制的部分):
| 包 | 状态 |
|---|---|
| packages/app | PARTIAL,低优先级(前端包) |
| packages/config | 已完成 |
| packages/data-context | PARTIAL,入口仍是 JS |
| packages/driver | 源码已完成,Cypress 测试待迁移 |
| packages/electron | 已完成 |
| packages/errors | 已完成 |
| packages/eslint-config | 已完成 |
| packages/example | 未完成 |
| packages/extension | 已完成 |
| packages/frontend-shared | PARTIAL,入口仍是 JS |
| packages/https-proxy | 已完成 |
| packages/icons | 已完成 |
| packages/launcher | 已完成 |
| packages/launchpad | 已完成 |
| packages/net-stubbing | 已完成 |
| packages/network | 已完成 |
| packages/network-tools | 已完成 |
| packages/packherd-require | 已完成 |
| packages/proxy | PARTIAL,入口仍是 JS |
| packages/reporter | 已完成 |
| packages/resolve-dist | 已完成 |
| packages/root | 已完成 |
| packages/runner | 已完成 |
| packages/scaffold-config | 已完成 |
| packages/server | PARTIAL,大量源码/测试仍是 JS,最高优先级 |
| packages/socket | 已完成 |
| packages/stderr-filtering | 已完成 |
| packages/telemetry | 已完成 |
| packages/ts | PARTIAL,最终目标是移除该包,转换价值不大 |
| packages/types | 已完成 |
| packages/v8-snapshot-require | 已完成 |
| packages/web-config | 已完成 |
关键阻碍:v8-snapshot 的旧式模块解析
原文档在 Notes 中点出了本次迁移中最棘手的技术债:从 ts-node 入口迁走的包,很难同时提供 browser 与 node 两个独立入口。根源在于 tooling/v8-snapshot/tsconfig.json 继承了 packages/ts/tsconfig.json,而后者使用的是旧式模块解析:
// packages/ts/tsconfig.json
"module": "commonjs", // L27
"moduleResolution": "node", // L29
moduleResolution: "node" 这套老式解析规则不会读取 package.json 里的 exports 字段,只能退而求其次地识别 main / module / browser 等顶层键。这意味着:只要二进制内的打包链(v8-snapshot 生成 V8 字节码快照所用的构建配置)还停留在这套解析下,包就无法用“一份源码 + exports 条件分支”的现代方式同时服务浏览器与 Node。
文档给出的临时方案是:为 browser 端与 node 端分别打包,让两份产物互不包含,从而绕开单一 bundle 混入双端代码的问题。文档明确这是临时措施——等所有包都能以 ESM 构建后,将重新评估 Cypress 二进制与 v8-snapshot 的构建方式,让各包以更规范的方式导出内容。目前采用这一套“双端打包”模式的正是下面三个包。
三个双端构建案例:scaffold-config、socket、telemetry
案例一:scaffold-config(最完整的参考实现)
packages/scaffold-config/package.json 是文档中特别点名“从 ts-node 入口迁出”的示例包,其 package.json 同时声明了三个产物入口:
{
"main": "cjs/index.js", // Node/CommonJS 入口
"module": "esm/index.js", // ESM 入口
"types": "cjs/index.d.ts",
"scripts": {
"build": "yarn build:cjs && yarn build:esm && yarn build:browser",
"build:browser": "rimraf browser && tsc -p tsconfig.browser.json",
"build:cjs": "rimraf cjs && tsc -p tsconfig.cjs.json",
"build:esm": "rimraf esm && tsc -p tsconfig.esm.json",
"test": "vitest run"
}
}
一份 src/ 源码(packages/scaffold-config/src)对应三套 tsconfig,正是文档所说“browser/node 代码不进同一 bundle”的落地方式:
- tsconfig.cjs.json:
"module": "CommonJS"、"outDir": "./cjs",产出 Node 端 CJS 产物; - tsconfig.esm.json:
"module": "ES2022"、"target": "ES2022"、"outDir": "./esm",产出 ESM 产物; - tsconfig.browser.json:最关键的一处——
"include": ["src/dependencies.ts"],只把浏览器端真正需要的dependencies.ts单独编进browser/目录,而不是打包整个src/,从而与 node 产物彻底解耦。该配置还单独指定了"types": ["cypress"],说明浏览器入口依赖 Cypress 的类型环境。
三套配置均 target: ES2022、moduleResolution: node,与 v8-snapshot 工具链的解析习惯保持一致——这正是“临时 workaround”的具体形态:产物格式先行对齐,解析规则随后再升级。
案例二:socket(最典型的 browser/node 分离)
packages/socket/package.json 展示了另一种更直白的双端划分——用目录区分入口,而不只是格式:
{
"main": "cjs/node/index.js", // Node 端
"browser": "browser/client/index.js", // 浏览器端(利用 browser 键)
"module": "esm/node/index.js",
"scripts": {
"build": "yarn build:browser && yarn build:node && yarn build:node:esm"
}
}
它把 node 端代码放在 cjs/node/、esm/node/,浏览器端放在 browser/client/,配合 browser 顶层键,让不理解 exports 的旧解析器也能拿到正确入口。此外该包通过 workspaces.nohoist 锁住了 socket.io 全家桶(socket.io、engine.io 系列)的版本,保证 binary 内打包版本稳定——这与 ESM 迁移无直接关系,但体现了双端构建包对依赖隔离的额外要求。
案例三:telemetry(Node 用 tsc,浏览器用 Rollup)
packages/telemetry/package.json 是第三种变体:
{
"main": "cjs/node.js",
"module": "esm/node.js",
"scripts": {
"build": "yarn build:esm && yarn build:cjs && yarn build:browser",
"build:browser": "rimraf browser && rollup -c rollup.config.mjs",
"build:cjs": "rimraf cjs && tsc -p tsconfig.cjs.json",
"build:esm": "rimraf esm && tsc -p tsconfig.esm.json"
}
}
与 scaffold-config 全部用 tsc 不同,telemetry 的浏览器产物交给 rollup.config.mjs 打包——当浏览器端需要把 OpenTelemetry SDK(@opentelemetry/* 系列依赖)收敛成单个 bundle 时,Rollup 比逐文件 tsc 更合适。三个案例合起来展示了 Cypress 在“exports 尚未被 v8-snapshot 支持”这一约束下的三种双端构建姿势:多 tsconfig 分目录输出、browser 键 + 目录隔离、以及 Rollup 打包浏览器产物。
顶层 CLI 的 exports 实践
值得注意的是,已经完成的 cli 包在 cli/package.json 中已经使用了现代的 exports 条件导出(L128-L133):
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
// ... /vue、/react、/svelte 等子路径同样区分 import/require
}
import 指向 .mjs、require 指向 .js 的写法,正是 Phase 3(NPM 包 ESM/CJS 双版本)要推广到所有 NPM 包的样板。CLI 由 Rollup 统一打包("build": "rollup -c",见 cli/rollup.config.mjs),不再依赖 v8-snapshot 逐包解析,因此可以先行落地 exports。这也印证了迁移的顺序逻辑:不受 v8-snapshot 解析约束的包先走现代格式,受约束的 binary 包先用多入口 workaround 过渡。
Phase 2:单测从 Mocha 迁移到 Vitest
Phase 2 的清单显示绝大多数包已完成迁移,两个例外值得注意:
- packages/server 未完成——与 Phase 1 一致,server 是剩余工作量最大的包;
- packages/ts 不迁——文档注明其最终目标就是移除(它只是为整个 monorepo 提供共享的 tsconfig.json 与 tslint 配置),转换没有价值。
一个特殊的分支决策:packages/data-context 选择了 Jest 而非 Vitest,从 mocha/sinon/chai 迁到了 jest(见 packages/data-context/jest.config.ts),文档要求读者查阅其 README 了解选择 Jest 的原因。这说明路线图允许个别包基于自身测试形态(GraphQL schema 生成、快照量等)偏离统一框架,只要迁移动机写清楚。
其余包则统一收敛到 Vitest,例如 packages/scaffold-config/package.json 与 cli/package.json 中的测试脚本均为:
"test": "vitest run",
"test-debug": "vitest --inspect-brk --no-file-parallelism --test-timeout=0"
统一的调试参数(--inspect-brk、--no-file-parallelism、--test-timeout=0)在各包中高度一致,说明 monorepo 层面为 Vitest 迁移定下了标准化的调试姿势。
Phase 3 与 Phase 4:待 Phase 2 收尾后展开
文档对最后两个阶段只留下了原则性描述,这里如实呈现其状态与前提:
- Phase 3:为 NPM 包打包 ESM/CJS 双版本——“细节将在 Phase 2 结束后更清晰”。从 cli/package.json 的
exports字段与 packages/scaffold-config 的main/module双入口看,目标形态(import/require条件导出 + 双产物)已有可参照的实现; - Phase 4:让 Cypress server 以 ESM 包运行——同样待定。而 packages/server 在 Phase 1/2 中双双处于 PARTIAL/未完成状态且被标为最高优先级,可以推断 server 的 TS 化与测试迁移是解锁 Phase 4 的前置条件。
小结与延伸阅读
这条迁移路线可以概括为:先统一语言(TypeScript)、再统一测试(Vitest)、在解析链升级前用多入口产物规避 exports 不可用的问题、最后统一产物格式(ESM/CJS)并让 server 以 ESM 运行。深入阅读时建议按以下路径对照源码:
- 迁移指南原文:guides/esm-migration.md
- v8-snapshot 工具链配置:tooling/v8-snapshot/tsconfig.json、共享基线 packages/ts/tsconfig.json
- 双端构建范例:packages/scaffold-config/package.json 及同目录三套 tsconfig、packages/socket/package.json、packages/telemetry/package.json
exports条件导出现状:cli/package.json
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00