---
title: 代码规范工具：核对 Biome、ESLint 与格式化的真实分工
description: 阅读 ChatCrystal 的实际脚本与 Flat Config，用离线 ESLint 诊断验证规则，再区分语法检查、类型检查、格式化和 CI 门禁。
image: "https://zengliangyi.com/og/blog/lint-format-tooling.png"
---

工具安装在依赖里，不代表所有检查都已启用。本文对照 ChatCrystal 的实际命令、Biome 开关和客户端 Flat Config，再用离线诊断证明规则真的识别目标问题。重点是可验证的覆盖范围，而不是增加工具数量。

## 先读脚本，再读工具开关

当前 [根 package.json](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/package.json)的 pnpm lint 先运行 Biome，成功后再运行 client 的 ESLint。下面是当前脚本摘录：



```json
{  
    "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](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/biome.json)启用 linter，**formatter 与 assist 均为 false**。因此不能把当前 biome check –fix 描述成已经提供全仓库格式化。VCS 忽略规则和 ignoreUnknown 也会影响覆盖范围；命令里的点号不意味着每种文件都经过同样检查。

根依赖没有接入 Prettier、husky 或 lint-staged，仓库也没有 .husky 提交钩子。编辑器保存时自动格式化和本地提交检查可以是后续选择，不能把原稿中的教学安装方案写成已经存在的项目功能。

## Flat Config 分别决定匹配、解析与规则

[client/eslint.config.js](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/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 规则关闭。全局忽略与只匹配某类文件也要区分，见[官方配置说明](https://eslint.org/docs/latest/use/configure/configuration-files)。

这里使用的 typescript-eslint recommended **不等于启用了依赖类型信息的检查**。当前配置没有接入 projectService 等类型信息设置；不能据此声称 ESLint 完成了 TypeScript 编译器的全部类型分析，或默认强制枚举命名风格。需要类型信息的规则有单独的[配置条件](https://typescript-eslint.io/getting-started/typed-linting/)。

## 用字面诊断检查规则是否生效

下面安装与当前客户端相同的 ESLint 10.10.0。在新临时目录保存 lesson.mjs，用 Node 24+ 运行。它是**独立 JavaScript 教学配置**，不加载 ChatCrystal 的配置，也没有 React 或 TypeScript 规则。



```sh
npm   init   -y  
  npm   install   --ignore-scripts   --no-audit   --no-fund   eslint@10.10.0
```





```js
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](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/.github/workflows/release.yml)的 npm 与 Electron 发布 job 都在构建前运行 pnpm lint、pnpm test。它是标签或手动触发的发布流程，不能概括为每个普通 PR 都自动执行同样的门禁。

一条检查的验收应包含：目标错误是否产生目标诊断，修复后是否通过，忽略是否符合预期，命令失败是否真的让下游停止。类型检查和运行验证另见[多包 TypeScript 配置](/blog/typescript-monorepo-configuration/)，测试策略也不能由 lint 的绿灯代替。

分享：

##  相关文章

[##  五种 AI 对话解析器：字段保留与损失边界

从 Claude Code、Codex、Cursor、Trae 和 Copilot 的实际解析器建立验收矩阵，检查工具标记、思考文本、顺序与快照覆盖范围。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#TypeScript

#Node.js](/blog/ai-conversation-parser-boundaries)    [##  AI SDK Provider 抽象：能力表与协议边界

核对 ChatCrystal 的七项 Provider 注册、AI SDK 7 模型工厂和结构化输出，用无网络 mock 验证 Responses、Chat Completions、Embedding 与 schema 失败。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#TypeScript](/blog/ai-sdk-provider-abstraction)    [##  SourceAdapter 内部链路：身份、内容哈希与重新导入

对照 ChatCrystal 的注册表、导入器和远端载荷，验证会话身份、内容哈希、元数据捷径与 parserVersion 的实际边界。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#TypeScript

#Node.js](/blog/source-adapter-internals)

##  从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录，欢迎交流实现细节。

[访问我的 GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "代码规范工具：核对 Biome、ESLint 与格式化的真实分工",
  "description": "阅读 ChatCrystal 的实际脚本与 Flat Config，用离线 ESLint 诊断验证规则，再区分语法检查、类型检查、格式化和 CI 门禁。",
  "url": "https://zengliangyi.com/blog/lint-format-tooling/",
  "image": "https://zengliangyi.com/images/blog/lint-format-tooling.svg",
  "datePublished": "2026-10-06T00:00:00.000Z",
  "dateModified": "2026-10-06T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/blog/lint-format-tooling/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首页",
      "item": "https://zengliangyi.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "https://zengliangyi.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "代码规范工具：核对 Biome、ESLint 与格式化的真实分工",
      "item": "https://zengliangyi.com/blog/lint-format-tooling/"
    }
  ]
}
```
