Cypress Monorepo 的 ESLint 迁移实战:从 TSLint 到统一的 @packages/eslint-config
本文基于 Cypress 仓库中的迁移指南 guides/eslint-migration.md,讲解如何把 Cypress 大型 monorepo 中几十个包从各自为政的旧 ESLint/TSLint 配置,迁移到统一的 flat config 方案 @packages/eslint-config。读完本文,你将掌握分批次迁移的完整策略、每个包的标准迁移步骤、迁移期间 lint-staged 的双轨制配置,以及 jiti 版本、TypeScript project service、pre-commit 规则错配等 7 类典型故障的排查方法,并结合仓库源码看清共享配置的真实规则集。
1. 为什么迁移:四个动因
迁移指南开宗明义给出了四点理由(见 guides/eslint-migration.md):
- 一致性(Consistency):在整个 monorepo 的所有包中强制同一套 linting 规则与插件;
- 简洁性(Simplicity):移除对自定义插件
@cypress/eslint-plugin-dev的依赖; - 可维护性(Maintainability):让规则的更新与维护更容易——规则只改一处;
- 现代化(Modernization):用
@typescript-eslint替代已经过时的 TSLint。
Cypress 仓库的规模决定了这不是一次性的体力活:从迁移清单看,涉及 npm/ 下十余个发布包、packages/ 下近二十个内部包(server、driver、reporter、frontend-shared、app 等),以及 cli、system-tests、scripts 等目录。因此指南的核心思想是"分批次、小 PR、可回滚"。
2. 迁移目标:@packages/eslint-config 共享配置包
迁移的终点是每个包都指向同一个共享配置包 packages/eslint-config。它的 package.json 中几个关键事实值得注意(见 packages/eslint-config/package.json):
- 包名为
@packages/eslint-config,private: true,版本固定为0.0.0-development(不参与 npm 发布,仅 monorepo 内部以文件路径引用); main直接指向src/index.ts,即运行时加载 TypeScript 配置文件,这正是后文 jiti 问题的根源;- ESLint 被声明为 peerDependency(
"eslint": "^9.31.0"),所以每个迁移的包必须自行声明eslint的 devDependency; jiti: "^2.4.2"被声明为这个包自身的依赖——这是 ESLint 9 加载eslint.config.ts所需的 TypeScript 转译器。
2.1 基础规则集 baseConfig 的真实内容
共享配置的主体在 packages/eslint-config/src/baseConfig.ts,它是一个 InfiniteDepthConfigWithExtends[] 类型的 flat config 数组,从源码结构看,组合了以下插件预设:
| 来源 | 作用 |
|---|---|
@eslint/js 的 recommended |
ESLint 核心推荐规则 |
typescript-eslint 的 configs.recommended |
TypeScript 推荐规则(带 project service 的类型感知检查) |
eslint-plugin-cypress 的 recommended |
Cypress 测试专用规则 |
eslint-plugin-mocha 的 recommended |
Mocha 测试规范 |
eslint-plugin-vue 的 flat/recommended |
Vue 单文件组件 |
eslint-plugin-react 的 flat.recommended |
React(settings.react.version: '18') |
@stylistic/eslint-plugin 的 customize({ braceStyle: '1tbs', arrowParens: true }) |
代码风格 |
eslint-plugin-import-x 的 flatConfigs.typescript |
import/export 校验(TypeScript 场景) |
除了预设,源码中还内置了一批有实际约束力的定制规则,理解它们有助于你预判迁移后 eslint --fix 会自动改掉什么、会报出什么错:
- 解析器配置:对
**/*.{ts,js,jsx,tsx,vue}统一设置parser: tsParser、extraFileExtensions: ['.vue']、ecmaFeatures.jsx: true; no-console: 'error':基础配置默认禁止console(这也是 packages/eslint-config/src/cliOverrides.ts 存在的原因——CLI 类包需要把它关掉,cliOverrides目前仅包含'no-console': 'off'一条规则);no-restricted-properties(warn):限制process.geteuid()(Windows 上会抛错)和os.userInfo()(Docker 中以--user 12345运行时可能无/etc/passwd条目);no-restricted-syntax(warn):用 esquery 选择器禁止同步fs.*Sync调用(existsSync除外),提示"Synchronous fs calls should not be used in Cypress. Use an async API instead.";padding-line-between-statements(error):return前必须有空行、声明/控制流语句后必须空行等排版规则;- 一批"金标准但违规太多"的规则被临时关闭:如
prefer-const、@typescript-eslint/no-explicit-any、mocha/no-skipped-tests、cypress/no-unnecessary-waiting等设为off,源码注释明确写着"these are off while developing eslint, but should eventually be enabled"; - 环境分治的 globals:
.cy.{js,ts}、.{j,t}sx、.vue文件自动获得browserglobals;vite.config.mjs、webpack.config.*获得nodeglobals;所有文件共享shared-node-browser+es2020基线; - 为 v8 snapshot 打包做的约束:对
lib/**、src/**源码强制import-x/consistent-type-specifier-style: prefer-top-level(注释说明 v8 snapshot bundling 无法处理内联的 type + value 混合导入),并对所有*.{js,ts,tsx,vue}文件强制@typescript-eslint/consistent-type-imports(fixStyle: 'separate-type-imports'); - 默认忽略:
.releaserc.js、dist/**/*、**/__snapshots__/**/*、test/.mocharc.js。
src/index.ts 还额外导出了一个 globals 映射(在 globals 包基础上追加了 specHelper 环境:sinon、expect、lib 等只读全局),供测试脚手架类包复用。
2.2 每个包的最小配置
迁移后每个包只需一个两行的 eslint.config.ts(与 packages/eslint-config/README.md 的用法一致):
import baseConfig from '@packages/eslint-config'
export default baseConfig
3. 迁移路线图:分批次清单
指南将全部工作量切成 5 个批次(当前进度以文档中的勾选状态为准,见 guides/eslint-migration.md):
| 批次 | 范围 | 文档中标注的进度 |
|---|---|---|
| Batch 1 | 小型 npm 工具:npm/grep、npm/puppeteer、npm/mount-utils、npm/cypress-schematic |
全部完成 |
| Batch 2 | 框架适配器:npm/react、npm/vue、npm/svelte |
全部完成 |
| Batch 3 | 构建相关:npm/webpack-batteries-included-preprocessor、npm/webpack-preprocessor、npm/webpack-dev-server、npm/vite-plugin-cypress-esm、npm/vite-dev-server |
进行中 |
| Batch 4a | 核心包(一):packages/frontend-shared、packages/icons、packages/launcher、packages/https-proxy、packages/proxy、packages/net-stubbing、packages/driver、packages/reporter、packages/server |
packages/server 已完成 |
| Batch 4b | 核心包(二):packages/runner、packages/extension、packages/network、packages/socket、packages/telemetry、packages/launchpad、packages/errors、packages/data-context、packages/app |
packages/errors 已完成 |
| Batch 4c | 核心包(三):cli、packages/config、packages/root、packages/resolve-dist、packages/packherd-require、packages/v8-snapshot-require、packages/web-config、packages/types、packages/example |
cli 已完成 |
| Batch 5 | 测试与脚本目录:system-tests、scripts |
未开始 |
批次划分的原则是按目录/类型切分以保持 PR 可管理、降低风险,每个 PR 控制在 4–8 个包、按相似度归组,一个批次一个 PR。
4. 单包标准迁移步骤
指南为每个包定义了 8 步操作,以下逐步展开并结合仓库中已完成迁移的 npm/grep 佐证。
4.1 删除旧配置与旧插件引用
- 删除包内
.eslintrc、.eslintrc.json、.eslintrc.js; - 移除
package.json中对@cypress/eslint-plugin-dev的引用(如有); - 删除 TSLint 配置:移除
tslint.json,并移除package.json中的tslint依赖。
4.2 新增 eslint.config.ts
在包根目录创建 eslint.config.ts(最小形式见 2.2 节)。对于需要定制的包,指南建议扩展基础配置:
import baseConfig from '@packages/eslint-config'
export default [
...baseConfig,
{
files: ['**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx'],
rules: {
'no-console': 'off',
'no-restricted-syntax': 'off',
},
},
{
files: ['cypress/**/*.js', 'cypress/**/*.ts'],
languageOptions: {
globals: {
Cypress: 'readonly',
cy: 'readonly',
},
},
},
]
已迁移的 npm/grep/eslint.config.ts 正是这种形态的完整实现,它还演示了三个进阶技巧:
- 合并
cliOverrides(import { baseConfig, cliOverrides } from '@packages/eslint-config')来关闭no-console; - 对类型感知检查配置
parserOptions: { allowDefaultProject: true, tsconfigRootDir: __dirname }(tsconfigRootDir显式锚定到包目录,避免 project service 向上误找仓库根的 tsconfig); - 为
cypress/测试目录注入Cypress、cy、window只读 globals,为src/注入process只读 global。
4.3 整理依赖
- 移除现在由共享配置提供的包内 ESLint 插件;
- 移除 TSLint 相关依赖(新配置已含
@typescript-eslint); - 把共享配置加为 devDependency:
@packages/eslint-config: "0.0.0-development"(未发布到 npm,monorepo 内走相对文件路径解析); - 显式加
eslintdevDependency:因为它是 peer dependency。文档建议"eslint": "^9.18.0";从当前仓库看,已完成迁移的包实际使用"eslint": "^9.31.0",与共享配置包的 peer 版本对齐(见 npm/grep/package.json)。
关于 jiti,文档的"必要依赖"小节原文给出:
{
"devDependencies": {
"@packages/eslint-config": "0.0.0-development",
"eslint": "^9.18.0",
"jiti": "^2.4.2"
}
}
为什么需要 jiti? ESLint 9 在运行时通过 jiti 加载 TypeScript 的 flat config 文件(eslint.config.ts),而不是静态 import 进包源码。若不加显式声明,Yarn 可能提升到(hoist)一个更老的 jiti 版本导致运行时报错(见 6.1)。从当前仓库的实现看,这个目标以更精炼的方式达成:jiti 直接声明为 packages/eslint-config/package.json 的 dependencies,而 npm/grep/package.json 则用 "resolutions": { "jiti": "^2.4.2" } 锁定版本——殊途同归,都是为了确保 lint 运行时拿到 ≥ 2.2.0 的 jiti。文档还解释了为什么 yarn health-check/knip 不会把它当未使用依赖清掉:knip 把 eslint 二进制与 eslint.config.ts 关联为"配置基础设施",不会标记 jiti,尽管没有任何源码文件 import 它。
4.4 添加包级 lint-staged 配置
在包的 package.json 中加入:
{
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": "eslint --fix"
}
}
这样文件被暂存提交时,会用包本地的 ESLint 配置自动修复。npm/grep/package.json 中实际的写法为 "**/*.{js,jsx,ts,tsx,json}": "eslint --fix"(多覆盖了 JSON 文件),与 packages/eslint-config/package.json 的自 lint 配置一致。
4.5 运行 lint 与 autofix
在包根目录运行:
npx eslint . --ext .js,.ts,.tsx,.jsx --fix
然后手动修复剩余的 lint 错误。注意脚本形式的变化:ESLint 9 会自动探测文件扩展名,包的 lint 脚本从旧写法 "lint": "eslint . --ext .js,.ts" 简化为 "lint": "eslint"——npm/grep/package.json 与 packages/server/package.json 的 scripts 中均为 "lint": "eslint",验证了这一变化。
4.6 校验 TypeScript 配置
- 确保包有一个与新 ESLint 配置兼容的
tsconfig.json; - 运行
npx tsc --noEmit检查编译错误; - 确认新 ESLint 配置能正确解析包内 TypeScript 文件(细节见 6.2 的 project service 问题)。
4.7 运行包测试
运行该包的测试套件,确认没破坏任何行为。
4.8 提交并开启 PR
提交信息示例:
chore(npm/grep): migrate to @packages/eslint-config and remove legacy eslint-plugin-dev
PR 要求:一个批次一个 PR,PR 描述列出所有受影响的包,并为每个包附上检查清单:
- [ ] 移除旧 ESLint 配置
- [ ] 添加新配置
- [ ] 添加 lint-staged 配置
- [ ] 运行 lint 并修复错误
- [ ] 运行测试
如果某个包需要自定义 override,写在本地 eslint.config.ts 中,但指南强调能上游到共享配置就尽量上游。
5. 迁移期间的双轨制 lint-staged 策略
迁移期最大的工程难题是:部分包用新配置、部分包还用根配置,而 pre-commit 钩子从仓库根目录运行 ESLint。指南的解法是在根 package.json 中为每个目录写显式 lint-staged 模式:
- 已迁移的包:走包级 lerna/
yarn lint:fix路径,让 ESLint 在包目录内用包的配置运行; - 未迁移的包:走根目录
eslint --fix,使用根配置; - 根目录文件:由
*.{js,jsx,ts,tsx,json,eslintrc,vue}兜底。
当前 package.json 中的实际配置印证了这套"逐目录显式声明"的思路:
"lint-staged": {
"npm/vue/**/*.{js,jsx,ts,tsx,vue}": "yarn workspace @cypress/vue eslint --fix",
"npm/svelte/**/*.{js,jsx,ts,tsx}": "yarn workspace @cypress/svelte eslint --fix",
"npm/react/**/*.{js,jsx,ts,tsx}": "yarn workspace @cypress/react eslint --fix",
"npm/!(vue|svelte|react)/**/*.{js,jsx,ts,tsx,json,eslintrc,vue}": "eslint --fix",
"cli/**/*.{js,jsx,ts,tsx,json,eslintrc,vue}": "eslint --fix",
"packages/**/*.{js,jsx,ts,tsx,json,eslintrc,vue}": "eslint --fix",
"scripts/**/*.{js,jsx,ts,tsx,json,vue}": "eslint --fix",
"system-tests/**/*.{js,jsx,ts,tsx,json,vue}": "eslint --fix",
"tooling/**/*.{js,jsx,ts,tsx,json,vue}": "eslint --fix",
"*.{js,jsx,ts,tsx,json,eslintrc,vue}": "eslint --fix",
".circleci/*.yml": "circleci config validate"
}
框架适配器(vue/svelte/react)被特殊处理,直接用 yarn workspace 在各自 workspace 内执行 eslint --fix,其余已迁移目录(如 npm/!(vue|svelte|react) 兜住 grep、puppeteer 等)则回落根配置。指南明确说明:这份冗长配置是迁移期的临时状态,全部迁移完成后应收敛为一条全局模式:
{
"lint-staged": {
"**/*.{js,jsx,ts,tsx,json,eslintrc,vue}": "eslint --fix"
}
}
6. 故障排查指南(Troubleshooting)
指南收录了 7 类高频问题,这里完整保留并按重要性展开。
6.1 jiti 版本兼容错误
报错:Error: You are using an outdated version of the 'jiti' library. Please update to the latest version of 'jiti' to ensure compatibility and access to the latest features.
原因与解法:ESLint 9.x 要求 jiti ≥ 2.2.0,但 monorepo 依赖树中可能被提升到更老的版本。修复方式是在包内显式声明 jiti: "^2.4.2"(devDependency 或 resolutions,见 4.3 节的仓库实证)。
6.2 TypeScript project service 找不到文件
报错:was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject
三种解法,按优先级:
-
创建/更新包的
tsconfig.json,继承 monorepo 基础 tsconfig(packages/ts/tsconfig.json),并显式列出覆盖范围:{ "extends": "../../packages/ts/tsconfig.json", "compilerOptions": { "esModuleInterop": true, "allowJs": true, "checkJs": false }, "include": ["src/**/*", "cypress/**/*", "*.js", "*.ts", "*.jsx", "*.tsx"], "exclude": ["node_modules", "dist"] } -
Cypress 测试目录的
cypress/tsconfig.json需保证 include 覆盖所有测试文件:{ "compilerOptions": { "types": ["cypress"] }, "include": ["**/*.ts", "**/*.js"] } -
对确实难以纳入 tsconfig 的文件,在
eslint.config.ts中开启allowDefaultProject:{ files: ['**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx'], languageOptions: { parserOptions: { allowDefaultProject: true }, }, }实际落地时(如 npm/grep/eslint.config.ts)会同时设置
tsconfigRootDir: __dirname,把解析根锚定在包目录内,这是 monorepo 场景下防止 project service 找错 tsconfig 的关键细节。
6.3 skip-comment 规则违规
旧插件 @cypress/eslint-plugin-dev 的 skip-comment 规则会在 it.skip() 处报 eslint-disable-next-line @cypress/dev/skip-comment。迁移后应移除旧插件并改用带解释的注释:
// NOTE: This test is skipped for demonstration purposes
it.skip('first test', () => {})
这与共享配置中 mocha/no-skipped-tests: 'off'(基础配置暂未开启该规则)的现状是自洽的:跳过测试本身合法,但要求写明原因。
6.4 pre-commit 钩子中的规则错配
现象:报出的规则违规与包的配置不符——例如包配置里 no-console 是关闭的,钩子却报 Unexpected console statement。
根因:pre-commit 钩子从仓库根目录运行 ESLint,用的是根级配置而不是包级配置。
解法:即第 5 节的显式 lint-staged 模式——已迁移包走包目录内的 lint(yarn lint:fix / yarn workspace <pkg> eslint --fix),未迁移包走根配置。指南再次强调这是迁移期临时方案,全部迁移后统一简化。
6.5 ESLint 9 的脚本写法变化
ESLint 9.x 自动探测文件扩展名,--ext 标志不再需要:
// 迁移前
"lint": "eslint . --ext .js,.ts"
// 迁移后
"lint": "eslint"
6.6 包依赖的最终形态
见 4.3 节给出的完整 JSON 与 jiti 原理说明,此处不再重复。
6.7 包级自定义规则
需要定制时扩展基础配置(完整示例见 4.2 节),核心模式是 ...baseConfig 展开后追加 override 对象,通过 files 精确限定作用域,避免"一刀切"改动污染其他文件。
7. 单包迁移检查清单模板
每个包迁移完成前,对照以下模板逐项确认(指南原文):
- [ ] 已删除
.eslintrc*文件 - [ ] 已创建配置正确的
eslint.config.ts - [ ] 已添加必需依赖(
eslint、@packages/eslint-config、jiti) - [ ] 已在
package.json中配置 lint-staged - [ ] 已创建/更新继承基础配置的
tsconfig.json - [ ] 已更新 ESLint 脚本(移除
--ext标志) - [ ]
yarn lint成功运行 - [ ] 已运行测试确认无破坏
指南还附了一个"批次迁移示例"流程:选定批次(如 Batch 1 的 npm/grep、npm/puppeteer、npm/mount-utils、npm/cypress-schematic)→ 对每个包删除 .eslintrc*、添加 eslint.config.ts、移除本地插件依赖、运行 lint 修复错误、运行测试 → 提交并开 PR。
8. 收尾阶段:废弃旧插件与配置简化
当所有包迁移完成后,指南还规划了三步收尾:
- 废弃并移除旧插件:从仓库和 CI 中删除
@cypress/eslint-plugin-dev; - 简化 lint-staged:根
package.json收敛为单一全局模式(见第 5 节末尾的 JSON); - 更新 Lerna/Monorepo 配置与文档:确保所有包在
package.json/eslint.config.ts中引用新配置,同步更新开发者上手文档。
指南末尾的协作建议同样值得保留:用 tracking issue 或项目板跟踪进度(本文档头部的批次清单即其产物);噪音特别大的包单独拆 PR;与团队沟通迁移时间表;push 之前先测试 pre-commit 钩子,确认 lint-staged 行为正确。
9. 小结
这篇指南的价值在于它把一个"听起来简单"的 lint 配置统一,拆解成了可执行、可核查的工程流程:共享配置包(packages/eslint-config)集中了 @eslint/js + typescript-eslint + cypress/mocha/vue/react/stylistic/import-x 的规则集,并以 flat config + TypeScript 文件 + jiti 运行时加载的形态分发;批次化 PR(4–8 包/PR)控制风险;双轨 lint-staged 保证迁移期 pre-commit 不出错;7 类故障排查手册覆盖了 jiti 版本、project service、规则错配等 monorepo 特有坑。对任何正在做类似 lint 统一的大型仓库,这套"先共享配置包、再分批次、用显式钩子配置过渡、最后收敛"的路径都具有直接参考价值。
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 StartedRust0623
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