工具安装在依赖里,不代表所有检查都已启用。本文对照 ChatCrystal 的实际命令、Biome 开关和客户端 Flat Config,再用离线诊断证明规则真的识别目标问题。重点是可验证的覆盖范围,而不是增加工具数量。
先读脚本,再读工具开关
当前 根 package.json的 pnpm lint 先运行 Biome,成功后再运行 client 的 ESLint。下面是当前脚本摘录:
{
"lint": "pnpm exec biome check . --diagnostic-level=error && pnpm --filter ./client lint",
"lint:fix": "pnpm exec biome check --fix . && pnpm --filter ./client lint -- --fix"
}
biome.json启用 linter,formatter 与 assist 均为 false。因此不能把当前 biome check –fix 描述成已经提供全仓库格式化。VCS 忽略规则和 ignoreUnknown 也会影响覆盖范围;命令里的点号不意味着每种文件都经过同样检查。
根依赖没有接入 Prettier、husky 或 lint-staged,仓库也没有 .husky 提交钩子。编辑器保存时自动格式化和本地提交检查可以是后续选择,不能把原稿中的教学安装方案写成已经存在的项目功能。
Flat Config 分别决定匹配、解析与规则
client/eslint.config.js使用 ESLint Flat Config。它全局忽略 dist,对 **/*.{ts,tsx} 组合以下规则集:
| 配置组 | 当前作用 |
|---|---|
| @eslint/js recommended | 常见 JavaScript 问题规则 |
| typescript-eslint recommended | TypeScript 语法与推荐规则 |
| react-hooks flat recommended | React Hooks 相关规则 |
| react-refresh vite | Vite 刷新场景的导出约定 |
languageOptions 使用 ecmaVersion:latest 和 browser globals;不是原稿中的固定 2020。React 的 set-state-in-effect 规则被关闭,这是一条实际覆盖,不代表所有 Hooks 规则关闭。全局忽略与只匹配某类文件也要区分,见官方配置说明。
这里使用的 typescript-eslint recommended 不等于启用了依赖类型信息的检查。当前配置没有接入 projectService 等类型信息设置;不能据此声称 ESLint 完成了 TypeScript 编译器的全部类型分析,或默认强制枚举命名风格。需要类型信息的规则有单独的配置条件。
用字面诊断检查规则是否生效
下面安装与当前客户端相同的 ESLint 10.10.0。在新临时目录保存 lesson.mjs,用 Node 24+ 运行。它是独立 JavaScript 教学配置,不加载 ChatCrystal 的配置,也没有 React 或 TypeScript 规则。
npm init -y
npm install --ignore-scripts --no-audit --no-fund eslint@10.10.0
import assert from 'node:assert/strict';
import { ESLint } from 'eslint';
const lint = new ESLint({
overrideConfigFile: true,
overrideConfig: [
{
files: ['**/*.js'],
languageOptions: { globals: { console: 'readonly' } },
rules: { 'no-unused-vars': 'error', 'no-undef': 'error' },
},
],
});
const [bad] = await lint.lintText('const unused = 1;', { filePath: 'fixture.js' });
assert.equal(bad.errorCount, 1);
assert.equal(bad.messages[0].ruleId, 'no-unused-vars');
const [good] = await lint.lintText("console.log('ready');", { filePath: 'fixture.js' });
assert.equal(good.errorCount, 0);
const [missing] = await lint.lintText('missing();', { filePath: 'fixture.js' });
assert.equal(missing.messages[0].ruleId, 'no-undef');
console.log('specific diagnostics and repaired input pass');
把 no-unused-vars 从 error 改为 off,第一条错误数断言应失败;恢复后通过。断言还检查具体 ruleId,避免把解析失败误认成目标规则生效。lintText 返回诊断结果,这个 API 调用本身不会自动按错误数设置进程退出码;教学进程通过 assert 决定成败。
真正接入时应检查一个匹配文件、一个被忽略文件和一个被覆盖规则的文件。也可以在 client 目录运行 pnpm exec eslint --print-config src/App.tsx 查看实际合并配置。配置数组顺序、文件匹配和覆盖值,比“已经安装插件”更能说明实际行为。
格式化选择需要单独落地
如果这个项目要加入格式化,先决定由 Biome 还是 Prettier 负责,并明确支持的文件类型、命令与编辑器设置。启用 Biome formatter 或新增 Prettier 都属于后续方案,本文没有修改配置。
选择 Prettier 时,eslint-config-prettier 可以关闭一组可能冲突的格式规则,但它不替你运行 Prettier,也不是关闭所有 ESLint 质量规则的开关。既有 ESLint 规则未必都与格式化冲突,不能只根据有两个工具就宣布它们互斥。
保存时修复与 CI 检查也有不同任务:前者修改可修复项,后者验证提交内容。自动修复不能保证所有错误消失,应查看 diff 并再次运行检查。提交钩子可以被绕过;CI 是否阻止合并还取决于仓库 required checks,不能从一个 workflow 文件推断保护规则已经设置。
检查命令和失败出口
当前 release.yml的 npm 与 Electron 发布 job 都在构建前运行 pnpm lint、pnpm test。它是标签或手动触发的发布流程,不能概括为每个普通 PR 都自动执行同样的门禁。
一条检查的验收应包含:目标错误是否产生目标诊断,修复后是否通过,忽略是否符合预期,命令失败是否真的让下游停止。类型检查和运行验证另见多包 TypeScript 配置,测试策略也不能由 lint 的绿灯代替。