首页
/ 深入解析 @cypress/eslint-plugin-dev:Cypress 内部开发级 ESLint 规范、自定义规则与 Git 钩子实践

深入解析 @cypress/eslint-plugin-dev:Cypress 内部开发级 ESLint 规范、自定义规则与 Git 钩子实践

2026-09-07 22:45:03作者:翟江哲Frasier

Cypress 是一个体量庞大的 monorepo,内含 30+ 个开发包、数千个源文件,靠一套统一的 ESLint 规范才能维持代码风格与质量。@cypress/eslint-plugin-dev 正是这套"内部开发工具链"的载体:它既提供内置代码风格配置与预置(presets),也提供专门针对 Cypress 测试代码的自定义规则,还封装了只 lint 改动文件的 CLI 脚本,供 Git 钩子与 CI 使用。本文以 npm/eslint-plugin-dev/AGENTS.md 为骨架,结合其 README 与全部源码、测试,完整讲解这个包的定位、安装配置、预置项、自定义规则实现与 git 钩子脚本的工作原理。

一、先明确边界:这是"开发者用"而非"用户用"的插件

@cypress/eslint-plugin-dev 是一个发布在 npm 上的包,用于在 Cypress monorepo 各内部开发包之间共享 ESLint 规则与配置。文档在开头就划定了明确边界:

  • 仅供 Cypress monorepo 内部及内部工具链使用
  • 不是面向 Cypress 终端用户的插件——用户侧对应的是独立的 eslint-plugin-cypress(位于另一个仓库),不要在面向用户的教程中推荐本包;
  • 它的脚本会在 monorepo 根目录以整仓视角运行 lint,本质上是"仓库治理工具"。

这个定位决定了它的所有设计取向:general 预置里是风格与正确性并重的规则全集;tests 预置里是**面向 Cypress 测试文件(Mocha 风格)**的规则;lib/scripts/ 里的脚本则深度依赖 git 命令。

二、仓库结构与架构总览

根据 AGENTS.md 的架构说明,并结合 npm/eslint-plugin-dev 的实际目录,包的代码布局如下:

npm/eslint-plugin-dev/
├── lib/
│   ├── index.js                    # 主入口:导出 configs(general/tests/react)与自定义 rules
│   ├── custom-rules/               # Cypress 内部专用的自定义 ESLint 规则
│   │   ├── index.js                # 自动读取目录内除 index.js 外的 *.js 并合并导出
│   │   ├── arrow-body-multiline-braces.js
│   │   ├── no-only.js
│   │   ├── no-return-before.js
│   │   └── skip-comment.js
│   └── scripts/                    # CLI 脚本(同时注册为 npm bin)
│       ├── utils.js                # 供各脚本复用的 lint 引擎
│       ├── lint-changed.js         # 只 lint 改动过的文件(--fix 可选)
│       ├── lint-pre-commit.js      # pre-commit 钩子入口
│       ├── lint-pre-push.js        # pre-push 钩子入口
│       └── lint-staged.js          # lint-staged 场景入口
├── test/
│   ├── *.spec.ts                   # 四个自定义规则的 vitest 规格测试
│   └── fixtures/                   # 规则测试用正/反例源码
├── package.json                    # bin 注册、脚本、peerDependencies
├── README.md / AGENTS.md / CHANGELOG.md / LICENSE.md
└── vitest.config.ts

主入口 lib/index.js 的逻辑非常直白:先定义一份约 80 条的 baseRules 风格规则基线,再组合出三个命名配置(general / tests / react),最后通过 rules: { ...customRules } 把四个自定义规则全部暴露为 @cypress/dev/<rule-name>。而 lib/custom-rules/index.js 采用"目录自动发现"模式:遍历目录内所有以 .js 结尾(排除自身)的文件并逐个 require,新增规则时无需改动任何注册代码。

package.jsonbin 字段(第 37–40 行)可以看到,包对外暴露两个可执行命令:

"bin": {
  "lint-changed": "./lib/scripts/lint-changed.js",
  "lint-pre-commit": "./lib/scripts/lint-pre-commit.js"
}

三、快速上手:安装、接入与 .eslintignore 细节

README 提取的接入流程如下。

版本兼容约束:本包不支持 ESLint 9;AGENTS.md 也特别提示 peer dependency 只覆盖 ESLint 8.x(仓库内 package.json 中写的是 "eslint": "^= 8.0.0")。按 README 的版本指引:ESLint 8 请用本包 6.x.x,ESLint 7 及以下请用 5.x.x。实际接入前应先确认自身 ESLint 主版本。

1)安装所需的 devDependencies

@cypress/eslint-plugin-dev
eslint-plugin-json-format
@typescript-eslint/parser
@typescript-eslint/eslint-plugin
eslint-plugin-mocha
eslint-plugin-import

# 若含 react / jsx 文件,还需要:
eslint-plugin-react
@babel/eslint-parser

这些依赖并非可有可无——本包的三个预置配置在源码里会直接引用 @typescript-eslint/parserreactmochajson-format 等插件名,若未安装,ESLint 解析配置时会直接报"插件找不到"。

2)在包根目录的 .eslintrc.json 中开启配置

{
  "plugins": [
    "@cypress/dev"
  ],
  "extends": [
    "plugin:@cypress/dev/general"
  ]
}
  • 使用 React/JSX 时追加 "plugin:@cypress/dev/react"
  • 存在 test/ 目录时,在该目录内单独建一份 .eslintrc.jsonextends: ["plugin:@cypress/dev/tests"](因为测试文件的 Mocha 全局、.skip 场景与业务代码差异很大)。

3)配置 .eslintignore

# don't ignore hidden files, useful for formatting json config files
!.*

这是本包一个非常实用的细节:默认 ESLint 会忽略以 . 开头的隐藏文件,而这会导致如 .eslintrc.json 之类的 JSON 配置无法被 eslint-plugin-json-format 自动格式化,所以 README 要求显式反忽略。

4)叠加自己的覆盖规则

extends 之上可按需覆盖/关闭具体规则:

// .eslintrc.json
{
  "extends": ["plugin:@cypress/dev/general"],
  "rules": {
    "comma-dangle": "off",
    "no-debugger": "warn"
  }
}

不想让 package.json 被自动排序格式化时,关闭该设置即可:

{
  "settings": {
    "json/sort-package-json": false
  }
}

四、三大预置配置(presets)的源码级拆解

三个预置全部定义在 lib/index.jsconfigs 对象里,源码可直接查阅,下面逐项拆解其构成与适用场景。

general:默认放在包根目录

general 的组成包括:

  • parserOptionsecmaVersion: 2018sourceType: 'module'
  • envnode: true + es6: true(内置代码默认就是 Node 环境);
  • plugins/settings:注册 json-format,并把 settings.json['sort-package-json'] 设为 'pro'——这是让 package.json 被自动按键排序的开关;
  • rules:完整展开 baseRules。这是全文最大的一块,摘录代表性条目及取值:
规则 配置 意图
indent ['error', 2, { ignoredNodes: ['TemplateLiteral'], SwitchCase: 1, MemberExpression: 0 }] 2 空格缩进,模板字符串暂不校验(源码注释标注 TODO)
semi ['error', 'never'] 禁止分号
quotes ['error', 'single', { allowTemplateLiterals: true }] 单引号,模板字符串除外
comma-dangle ['error', 'always-multiline'] 多行结构尾逗号
brace-style ['error', '1tbs', { allowSingleLine: false }] 1TBS 大括号风格
arrow-parens ['error', 'always'] 箭头函数参数恒加括号
no-console / no-debugger 'error' 禁止生产代码残留调试输出
no-unused-vars ['error', { args: 'none', ignoreRestSiblings: true }] 不检查函数参数
padding-line-between-statements 三段式 语句组之间/return 前强制空行
eqeqeq ['error', 'allow-null'] 强制 ===,仅 null 比较放行
  • overrides 分支
    • *.jsx / *.tsx:关闭 arrow-body-multiline-braces(JSX 场景下该规则误报率高);
    • *.ts / *.tsx / *.vue:切换 @typescript-eslint/parser,把 no-undefindentno-unused-varsno-duplicate-imports 等交给 TS 系规则接管,并打开 @typescript-eslint/type-annotation-spacing@typescript-eslint/indent@typescript-eslint/member-delimiter-style(多行分隔符为 none)等。

值得注意的是,这份 general 基线同时承担了 "JSON 配置格式化" 的职责——仓库内形形色色的 .json/.eslintrc 都能被统一排序,保证配置文件 diff 干净。

tests:放在 test/ 目录

tests 针对 Mocha/Cypress 测试代码:

tests: {
  env: { mocha: true },
  globals: { expect: true },
  plugins: ['mocha'],
  rules: {
    'mocha/handle-done-callback': 'error',
    'mocha/no-exclusive-tests': 'error',
    'mocha/no-global-tests': 'error',
    '@cypress/dev/skip-comment': 'error',
  },
  overrides: [{ files: '*.spec.tsx', /* TS parser + react 插件 */ }],
}

语义很清晰:测试代码不允许 .only、不允许全局测试、必须处理 done 回调,且任何 .skip 都必须给出说明注释(由 @cypress/dev/skip-comment 强制,见下文)。

react:面向 React / JSX

react 配置使用 @babel/eslint-parserrequireConfigFile: false、开启 jsxlegacyDecorators),并开启十余条 react/* 规则,例如 react/jsx-no-undefreact/jsx-pascal-casereact/jsx-wrap-multilinesreact/no-unknown-property 等,用于约束 JSX 的拼写与结构规范。

五、四条自定义规则的实现细节(源码 + 测试)

自定义规则位于 lib/custom-rules,每条都有独立的 vitest 规格测试与正反例 fixture(见 npm/eslint-plugin-dev/test)。规则总览如下表:

规则名 作用 可配置项 示例
@cypress/dev/arrow-body-multiline-braces 仅当箭头函数体为多行时强制使用 {} ['always' | 'never'],文档要求恒为 'always' '@cypress/dev/arrow-body-multiline-braces': ['error', 'always']
@cypress/dev/skip-comment 强制 .skipit/describe/context)前必须带解释注释 commentTokens 数组,默认 ['NOTE:', 'TODO:', 'FIXME:'] '@cypress/dev/skip-comment': ['error', { commentTokens: ['TODO:'] }]
@cypress/dev/no-return-before 禁止在指定调用(默认 it/describe/context/expect)前写 return tokens 数组 '@cypress/dev/no-return-before': ['error', { tokens: ['myfn'] }]
@cypress/dev/no-only 禁止 spec 文件里出现 it.only 需在配置中手动开启

1)arrow-body-multiline-braces:基于 eslint-rule-composer 的精修

该规则是"站在巨人肩膀上"的典型实现:arrow-body-multiline-braces.js 通过 eslint-rule-composerfilterReports复用 ESLint 内置 arrow-body-style 规则,只保留"函数体跨越多行"的报告,从而实现对单行箭头函数的放行:

const arrowBodyStyle = new eslint.Linter().getRules().get('arrow-body-style')

module.exports = ruleComposer.filterReports(
  arrowBodyStyle,
  (problem, metadata) => {
    if (problem.node.loc.start.line === problem.node.loc.end.line) return // 单行定义直接放行
    // …再排除多行注释符(----)等误报情形
  },
)

测试目录中的 multiline.jsoneline.js 正是用于验证"多行报错 / 单行放行"这两类样本。

2)skip-comment:让每个 .skip 都"有据可查"

测试代码里 it.skip/describe.skip 常被用来临时屏蔽失败用例,若无人解释原因,会成为长期遗留的定时炸弹。skip-comment.jsCallExpression:exit 上监听 callee.property.name === 'skip' 且调用者为 it/describe/context 的节点,用 sourceCode.getCommentsBefore(node) 检查节点之前的注释内容是否以 commentTokens 之一开头(默认 NOTE:TODO:FIXME:,同时兼容 # NOTE: 形式的注释):

const defaultCommentTokens = ['NOTE:', 'TODO:', 'FIXME:']
// 未命中任何 token → context.report(...)
// 报错信息会给出示例:"// NOTE: <reason test was skipped>"

它通过 schema 声明可选的 commentTokens 数组,代码通过 context.options[0].commentTokens 读取覆盖值。配套测试 skip-comment.spec.ts 及 fixture skip-comment-pass.jsskip-comment-fail.jsskip-comment-config.js 分别覆盖了"带注释通过、无注释报错、自定义 token"三种情形。

3)no-return-before:拦截测试链上的过早 return

Cypress 测试代码中,return it(...)return expect(...) 往往是重构残留且语义含糊。no-return-before.jsCallExpression:exit 中检测调用名命中默认 tokensit/describe/context/expect,可用 { tokens: [...] } 覆盖)时,取出其前一个 token,若为 return 关键字即报 Found a 'return' after '{{token}}'。与另外两条规则不同,它声明了 fixable: 'code' 并提供 fixer,用 fixer.replaceTextRange(...) 直接删除多余的 return(fixture no-return-before-fail.jsno-return-before-pass.js 即对应修前/修后样本)。

4)no-only:拦截 .only 的"红线"规则

no-only.js 用于阻止 it.only/describe.only/context.only 被提交。它监听 CallExpression:exit,当 calleeMemberExpressionproperty.name === 'only'object.name 命中 it/describe/context 时报错 Found only: ...。规则按单行截取报错位置文本,避免多行链式调用导致报错信息过长。

一个容易被忽略的细节是:该规则默认并未在任何内置配置中开启——lib/index.jsbaseRules 中该行被注释(// '@cypress/dev/no-only': 'error',),tests 配置也只默认开启 skip-comment。换句话说,no-only 作为"可用弹药"随包分发,需要消费方在各自 .eslintrcrules 里手动点亮(Cypress 历史代码中曾大量使用 .only,一次性强制开启会导致海量报错,这与源码注释中被整体注释的现状互相印证)。它的功能正确性由 no-only.spec.ts 与 fixture with-only.js 保证,而 .only 拦截更多交给 mocha/no-exclusive-tests(已在 tests 配置中默认开启)兜底。

六、CLI 脚本:只 lint "该 lint 的"文件

整仓 lint 的开销随代码量线性增长。为此包内实现了多套"增量 lint"脚本,全部集中在 lib/scripts

脚本 入口场景 核心逻辑
lint-changed.js lint-changed 命令(可手动跑,也常在 CI 使用) 汇总 git diff --name-only --diff-filter=M(已修改)与 git diff --name-only --diff-filter=MA --staged(已暂存)的并集,支持 --fix
lint-pre-commit.js husky 的 pre-commit 钩子 把"完全暂存文件"与"部分暂存文件"分流处理,自动 --fixgit add 完全暂存文件;对部分暂存文件走 stdin 临时内容校验,避免把未暂存改动混进提交
lint-pre-push.js pre-push 钩子 读取 HUSKY_GIT_PARAMS(默认 origin),用 git diff HEAD <remote>/<branch> --name-only 找出将要推送的改动并 lint
lint-staged.js lint-staged 工具链 面向 lint-staged 的适配入口

其公共引擎在 lib/scripts/utils.js,两个核心函数分工明确:

  • lintFilesByName(options):用 shelljs 拼接出文件列表后执行 npx eslint --color=true [--fix] <files>且强制在 monorepo 根目录运行cwd: path.resolve(__dirname, '../../../../')),保证复用仓库根目录的 .eslintrc 与本地 eslint 二进制;
  • lintFilesByText(options):不落盘 lint,通过 git show :<file>暂存区内容再以 eslint --stdin --stdin-filename <file> 校验——这正是保护"部分暂存"文件的关键手段。

两个函数内部都用正则 filesRegex = /\.(js|jsx|ts|tsx|json|eslintrc)$/ 过滤出可 lint 文件(天然跳过图片、锁文件等),底层以 bluebirdPromise.map 做并发控制。

package.json 中通过 husky 接入的示例写法为:

"husky": {
  "hooks": {
    "pre-commit": "lint-pre-commit"
  }
}

README 特别提醒:lint-pre-commit 只会对无未暂存改动的文件执行 --fixgit add,从而保护部分暂存文件不被整个覆盖;若要强制修复所有暂存与非暂存文件,可手动执行 ./node_modules/.bin/lint-changed --fix

七、包自身的开发命令(Key Commands)

AGENTS.md 与 package.json 中的 scripts 一一对应,包的自检与测试命令为:

yarn lint           # 对包自身执行 ESLint(--ext .js,json,.eslintrc)
yarn lint-changed   # 只 lint 自上次提交以来改动的文件
yarn lint-fix       # ESLint 附加 --fix 自动修复
yarn test -- <path-to-spec>      # 运行指定 vitest 规格文件
yarn test -- "<glob-pattern>"    # 用 glob 匹配 vitest 规格

测试基于 vitest.config.ts,四个自定义规则各配一个 *.spec.tsarrow-body-multiline-braces.spec.tsno-only.spec.tsno-return-before.spec.tsskip-comment.spec.ts),这为"规则行为可回归验证"提供了直接保障。

八、注意事项(Gotchas)与演进观察

AGENTS.md 明确的三点注意事项,在实际使用中应始终牢记:

  1. 内部专用,勿向终端用户推荐:Cypress 用户应使用面向用户的 eslint-plugin-cypress(独立仓库);本包是 Cypress 工程团队维护仓库内部代码质量的工具。
  2. 两条二进制贯穿 git 钩子与 CIlint-changedlint-pre-commit 被 monorepo 各包的 git 钩子、CI 任务广泛调用,其行为改动会影响整仓提交流程,务必通过 test/ 下的 vitest 用例回归。
  3. 只兼容 ESLint 8.x:peer dependency 为 eslint: "^= 8.0.0"(另含 @typescript-eslint/* >= 7@babel/eslint-parser ^7eslint-plugin-import >= 2 等),在消费端不支持 ESLint 9 的 flat config

从仓库现状可以观察到一个趋势:monorepo 的诸多包正在向 ESLint 9 flat config 迁移——例如 cli/eslint.config.ts 已改由 @packages/eslint-config 导出 baseConfig,而 packages/eslint-config/src/baseConfig.ts 中可见大量与本包 general 如出一辙的规则值(如相同的 padding-line-between-statements 三段式、no-console: 'error')。可以推断:本包代表的是 monorepo 基于 .eslintrc(eslintrc 格式)的既有规范基线,而仓库 guides/eslint-migration.md 描述的迁移过程正在逐步将同类规则平移至 flat config 体系。因此,在为本仓库新增或修改代码时,判断"该用哪套 lint 基线",取决于目标包当前采用的是 .eslintrc(对接本包)还是新式 eslint.config.ts

九、总结:一份可复用的"内部仓库代码治理"范本

纵观整个包,@cypress/eslint-plugin-dev 的价值不在于它解决了多么复杂的算法问题,而在于把三类工程实践固化成了可发布、可测试、可接入 git 钩子的 npm 包:

  • 风格即配置:把整仓约定(2 空格、无分号、单引号、1TBS、no-consolepadding-line 空行策略……)集中成 general/tests/react 三个语义化预置;
  • 规范即规则:用 skip-commentno-onlyno-return-before 等自定义规则把 Cypress 特有的测试纪律(.skip 必须有解释、禁止 .only、不要在测试链上过早 return)变成 CI 自动拦截的检查项;
  • 效率即脚本:用 git diff 驱动的 lint-changed / lint-pre-commit 实现毫秒级增量 lint,用 stdin 机制保护部分暂存文件,让大仓 lint 不致拖垮开发与提交体验。

对于想了解 Cypress 内部工程规范、或计划为自己的大型 monorepo 搭建"共享 ESLint + git 钩子"体系的开发者,本包从 AGENTS.md 的概览出发,顺着 lib/index.jslib/scripts/utils.js 的源码逐层阅读,是一份难得的、可直接落地的参考实现。

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