首页
/ Cypress Monorepo 的 ESLint 迁移实战:从 TSLint 到统一的 @packages/eslint-config

Cypress Monorepo 的 ESLint 迁移实战:从 TSLint 到统一的 @packages/eslint-config

2026-09-06 17:34:53作者:毕习沙Eudora

本文基于 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/ 下近二十个内部包(serverdriverreporterfrontend-sharedapp 等),以及 clisystem-testsscripts 等目录。因此指南的核心思想是"分批次、小 PR、可回滚"。

2. 迁移目标:@packages/eslint-config 共享配置包

迁移的终点是每个包都指向同一个共享配置包 packages/eslint-config。它的 package.json 中几个关键事实值得注意(见 packages/eslint-config/package.json):

  • 包名为 @packages/eslint-configprivate: 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/jsrecommended ESLint 核心推荐规则
typescript-eslintconfigs.recommended TypeScript 推荐规则(带 project service 的类型感知检查)
eslint-plugin-cypressrecommended Cypress 测试专用规则
eslint-plugin-mocharecommended Mocha 测试规范
eslint-plugin-vueflat/recommended Vue 单文件组件
eslint-plugin-reactflat.recommended React(settings.react.version: '18'
@stylistic/eslint-plugincustomize({ braceStyle: '1tbs', arrowParens: true }) 代码风格
eslint-plugin-import-xflatConfigs.typescript import/export 校验(TypeScript 场景)

除了预设,源码中还内置了一批有实际约束力的定制规则,理解它们有助于你预判迁移后 eslint --fix 会自动改掉什么、会报出什么错:

  • 解析器配置:对 **/*.{ts,js,jsx,tsx,vue} 统一设置 parser: tsParserextraFileExtensions: ['.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-anymocha/no-skipped-testscypress/no-unnecessary-waiting 等设为 off,源码注释明确写着"these are off while developing eslint, but should eventually be enabled";
  • 环境分治的 globals.cy.{js,ts}.{j,t}sx.vue 文件自动获得 browser globals;vite.config.mjswebpack.config.* 获得 node globals;所有文件共享 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-importsfixStyle: 'separate-type-imports');
  • 默认忽略.releaserc.jsdist/**/***/__snapshots__/**/*test/.mocharc.js

src/index.ts 还额外导出了一个 globals 映射(在 globals 包基础上追加了 specHelper 环境:sinonexpectlib 等只读全局),供测试脚手架类包复用。

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/grepnpm/puppeteernpm/mount-utilsnpm/cypress-schematic 全部完成
Batch 2 框架适配器:npm/reactnpm/vuenpm/svelte 全部完成
Batch 3 构建相关:npm/webpack-batteries-included-preprocessornpm/webpack-preprocessornpm/webpack-dev-servernpm/vite-plugin-cypress-esmnpm/vite-dev-server 进行中
Batch 4a 核心包(一):packages/frontend-sharedpackages/iconspackages/launcherpackages/https-proxypackages/proxypackages/net-stubbingpackages/driverpackages/reporterpackages/server packages/server 已完成
Batch 4b 核心包(二):packages/runnerpackages/extensionpackages/networkpackages/socketpackages/telemetrypackages/launchpadpackages/errorspackages/data-contextpackages/app packages/errors 已完成
Batch 4c 核心包(三):clipackages/configpackages/rootpackages/resolve-distpackages/packherd-requirepackages/v8-snapshot-requirepackages/web-configpackages/typespackages/example cli 已完成
Batch 5 测试与脚本目录:system-testsscripts 未开始

批次划分的原则是按目录/类型切分以保持 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 正是这种形态的完整实现,它还演示了三个进阶技巧:

  1. 合并 cliOverridesimport { baseConfig, cliOverrides } from '@packages/eslint-config')来关闭 no-console
  2. 对类型感知检查配置 parserOptions: { allowDefaultProject: true, tsconfigRootDir: __dirname }tsconfigRootDir 显式锚定到包目录,避免 project service 向上误找仓库根的 tsconfig);
  3. cypress/ 测试目录注入 Cypresscywindow 只读 globals,为 src/ 注入 process 只读 global。

4.3 整理依赖

  • 移除现在由共享配置提供的包内 ESLint 插件;
  • 移除 TSLint 相关依赖(新配置已含 @typescript-eslint);
  • 把共享配置加为 devDependency@packages/eslint-config: "0.0.0-development"(未发布到 npm,monorepo 内走相对文件路径解析);
  • 显式加 eslint devDependency:因为它是 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.jsondependencies,而 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.jsonpackages/server/package.jsonscripts 中均为 "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

三种解法,按优先级:

  1. 创建/更新包的 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"]
    }
    
  2. Cypress 测试目录的 cypress/tsconfig.json 需保证 include 覆盖所有测试文件:

    {
      "compilerOptions": { "types": ["cypress"] },
      "include": ["**/*.ts", "**/*.js"]
    }
    
  3. 对确实难以纳入 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-devskip-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-configjiti
  • [ ] 已在 package.json 中配置 lint-staged
  • [ ] 已创建/更新继承基础配置的 tsconfig.json
  • [ ] 已更新 ESLint 脚本(移除 --ext 标志)
  • [ ] yarn lint 成功运行
  • [ ] 已运行测试确认无破坏

指南还附了一个"批次迁移示例"流程:选定批次(如 Batch 1 的 npm/grepnpm/puppeteernpm/mount-utilsnpm/cypress-schematic)→ 对每个包删除 .eslintrc*、添加 eslint.config.ts、移除本地插件依赖、运行 lint 修复错误、运行测试 → 提交并开 PR。

8. 收尾阶段:废弃旧插件与配置简化

当所有包迁移完成后,指南还规划了三步收尾:

  1. 废弃并移除旧插件:从仓库和 CI 中删除 @cypress/eslint-plugin-dev
  2. 简化 lint-staged:根 package.json 收敛为单一全局模式(见第 5 节末尾的 JSON);
  3. 更新 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 统一的大型仓库,这套"先共享配置包、再分批次、用显式钩子配置过渡、最后收敛"的路径都具有直接参考价值。

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