深入解析 @cypress/eslint-plugin-dev:Cypress 内部开发级 ESLint 规范、自定义规则与 Git 钩子实践
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.json 的 bin 字段(第 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/parser、react、mocha、json-format 等插件名,若未安装,ESLint 解析配置时会直接报"插件找不到"。
2)在包根目录的 .eslintrc.json 中开启配置
{
"plugins": [
"@cypress/dev"
],
"extends": [
"plugin:@cypress/dev/general"
]
}
- 使用 React/JSX 时追加
"plugin:@cypress/dev/react"; - 存在
test/目录时,在该目录内单独建一份.eslintrc.json并extends: ["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.js 的 configs 对象里,源码可直接查阅,下面逐项拆解其构成与适用场景。
general:默认放在包根目录
general 的组成包括:
parserOptions:ecmaVersion: 2018、sourceType: 'module';env:node: 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-undef、indent、no-unused-vars、no-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-parser(requireConfigFile: false、开启 jsx 与 legacyDecorators),并开启十余条 react/* 规则,例如 react/jsx-no-undef、react/jsx-pascal-case、react/jsx-wrap-multilines、react/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 |
强制 .skip(it/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-composer 的 filterReports,复用 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.js 与 oneline.js 正是用于验证"多行报错 / 单行放行"这两类样本。
2)skip-comment:让每个 .skip 都"有据可查"
测试代码里 it.skip/describe.skip 常被用来临时屏蔽失败用例,若无人解释原因,会成为长期遗留的定时炸弹。skip-comment.js 在 CallExpression: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.js、skip-comment-fail.js、skip-comment-config.js 分别覆盖了"带注释通过、无注释报错、自定义 token"三种情形。
3)no-return-before:拦截测试链上的过早 return
Cypress 测试代码中,return it(...)、return expect(...) 往往是重构残留且语义含糊。no-return-before.js 在 CallExpression:exit 中检测调用名命中默认 tokens(it/describe/context/expect,可用 { tokens: [...] } 覆盖)时,取出其前一个 token,若为 return 关键字即报 Found a 'return' after '{{token}}'。与另外两条规则不同,它声明了 fixable: 'code' 并提供 fixer,用 fixer.replaceTextRange(...) 直接删除多余的 return(fixture no-return-before-fail.js 与 no-return-before-pass.js 即对应修前/修后样本)。
4)no-only:拦截 .only 的"红线"规则
no-only.js 用于阻止 it.only/describe.only/context.only 被提交。它监听 CallExpression:exit,当 callee 为 MemberExpression 且 property.name === 'only'、object.name 命中 it/describe/context 时报错 Found only: ...。规则按单行截取报错位置文本,避免多行链式调用导致报错信息过长。
一个容易被忽略的细节是:该规则默认并未在任何内置配置中开启——lib/index.js 的 baseRules 中该行被注释(// '@cypress/dev/no-only': 'error',),tests 配置也只默认开启 skip-comment。换句话说,no-only 作为"可用弹药"随包分发,需要消费方在各自 .eslintrc 的 rules 里手动点亮(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 钩子 |
把"完全暂存文件"与"部分暂存文件"分流处理,自动 --fix 并 git 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 文件(天然跳过图片、锁文件等),底层以 bluebird 的 Promise.map 做并发控制。
在 package.json 中通过 husky 接入的示例写法为:
"husky": {
"hooks": {
"pre-commit": "lint-pre-commit"
}
}
README 特别提醒:lint-pre-commit 只会对无未暂存改动的文件执行 --fix 后 git 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.ts(arrow-body-multiline-braces.spec.ts、no-only.spec.ts、no-return-before.spec.ts、skip-comment.spec.ts),这为"规则行为可回归验证"提供了直接保障。
八、注意事项(Gotchas)与演进观察
AGENTS.md 明确的三点注意事项,在实际使用中应始终牢记:
- 内部专用,勿向终端用户推荐:Cypress 用户应使用面向用户的
eslint-plugin-cypress(独立仓库);本包是 Cypress 工程团队维护仓库内部代码质量的工具。 - 两条二进制贯穿 git 钩子与 CI:
lint-changed与lint-pre-commit被 monorepo 各包的 git 钩子、CI 任务广泛调用,其行为改动会影响整仓提交流程,务必通过test/下的 vitest 用例回归。 - 只兼容 ESLint 8.x:peer dependency 为
eslint: "^= 8.0.0"(另含@typescript-eslint/* >= 7、@babel/eslint-parser ^7、eslint-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-console、padding-line空行策略……)集中成general/tests/react三个语义化预置; - 规范即规则:用
skip-comment、no-only、no-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.js 与 lib/scripts/utils.js 的源码逐层阅读,是一份难得的、可直接落地的参考实现。
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 StartedRust0627
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