---
title: "Code tooling: the actual roles of Biome, ESLint, and formatting"
description: Read ChatCrystal’s scripts and Flat Config, verify ESLint diagnostics offline, and distinguish syntax rules, type checking, formatting, and CI gates.
image: "https://zengliangyi.com/og/blog/en/lint-format-tooling.png"
---

A tool listed in dependencies does not establish that every check is enabled. This article follows ChatCrystal’s actual commands, Biome switches, and client Flat Config, then verifies target diagnostics offline. The goal is a demonstrable scope, rather than adding tools.

## Read scripts, then tool switches

The current [root package.json](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/package.json) runs Biome first for pnpm lint, then client ESLint if it succeeds. These are current script excerpts:



```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) enables the linter, while **formatter and assist are both false**. Current biome check –fix therefore does not establish repository-wide formatting. VCS ignores and ignoreUnknown also affect coverage; a dot in the command does not mean every file type receives identical checks.

Root dependencies do not integrate Prettier, husky, or lint-staged, and there is no .husky commit hook. Editor formatting and local commit checks remain possible additions. Teaching installation instructions from the original draft cannot be presented as existing project features.

## Flat Config controls matching, parsing, and rules separately

[client/eslint.config.js](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/client/eslint.config.js) uses ESLint Flat Config. It globally ignores dist and combines these configurations for `**/*.{ts,tsx}`:

| Configuration | Current role |

| --- | --- |

| @eslint/js recommended | Common JavaScript problem rules |

| typescript-eslint recommended | TypeScript syntax and recommended rules |

| react-hooks flat recommended | React Hooks rules |

| react-refresh vite | Export conventions for Vite refresh |

languageOptions uses ecmaVersion:latest and browser globals, rather than the original draft’s fixed 2020. React’s set-state-in-effect rule is disabled; that override does not disable every Hooks rule. Global ignores differ from matching only particular file types; see the [official configuration guide](https://eslint.org/docs/latest/use/configure/configuration-files).

The typescript-eslint recommended configuration here **does not establish type-aware linting**. Current settings do not wire type information through projectService or equivalent options. ESLint cannot be described as performing all compiler type analysis or enforcing enum naming by default. Rules needing type information have separate [configuration requirements](https://typescript-eslint.io/getting-started/typed-linting/).

## Verify rules through specific diagnostics

Install ESLint 10.10.0, matching the current client, in a new temporary directory. Save lesson.mjs and run with Node 24+. This **independent JavaScript teaching configuration** loads no ChatCrystal configuration and includes no React or TypeScript rules.



```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'  );
```



Change no-unused-vars from error to off and the first error-count assertion should fail; restore it and the program passes. Checking ruleId also avoids confusing a parse error with the intended rule. lintText returns diagnostics; the API call does not automatically set process status from the error count. Assertions determine success or failure in this lesson.

Integration checks should cover a matched file, an ignored file, and a file with a rule override. From client, `pnpm exec eslint --print-config src/App.tsx` shows the merged configuration. Array order, file matching, and override values establish more than the mere presence of plugins.

## Formatting needs its own implementation choice

To add formatting, first choose Biome or Prettier and specify supported file types, commands, and editor settings. Enabling Biome formatter or adding Prettier are **future options; this article changes neither configuration**.

eslint-config-prettier disables a set of potentially conflicting formatting rules. It does not run Prettier or disable every ESLint quality rule. Existing ESLint rules need not conflict with formatting, so the presence of two tools alone does not establish incompatibility.

Save-time fixes and CI checks serve different purposes: the former changes fixable items, while the latter verifies committed content. Automatic fixes cannot guarantee removal of every error; inspect the diff and check again. Commit hooks can be bypassed. Whether CI blocks merging also depends on repository required checks, which cannot be inferred from a workflow file alone.

## Check commands and failure exits

Current npm and Electron publishing jobs in [release.yml](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/.github/workflows/release.yml) run pnpm lint and pnpm test before building. That release workflow triggers from tags or manual dispatch. It does not establish identical gates on every ordinary pull request.

Accept a check only after verifying the target diagnostic, passing repaired input, intended ignores, and a failure that actually stops downstream work. Type and runtime checks have separate roles in [TypeScript monorepo configuration](/en/blog/typescript-monorepo-configuration/); passing lint does not replace a testing strategy either.

Share:

##  Related Posts

[##  Five AI conversation parsers: retained fields and loss boundaries

Build an acceptance matrix from Claude Code, Codex, Cursor, Trae, and Copilot parsers, checking tool flags, thinking, order, and snapshot coverage.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#TypeScript

#Node.js](/en/blog/ai-conversation-parser-boundaries)    [##  AI SDK Provider Abstraction: Capabilities and Protocols

Test 7 ChatCrystal providers and AI SDK factories with offline mocks for Responses, Chat Completions, embeddings, and structured output rejection.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#TypeScript](/en/blog/ai-sdk-provider-abstraction)    [##  SourceAdapter internals: identity, content hashes, and reimport

Trace ChatCrystal’s registry, importer, and remote payload to verify identity, canonical hashes, metadata shortcuts, and parser-version boundaries.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#TypeScript

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

##  From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.

[Visit my 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": "Code tooling: the actual roles of Biome, ESLint, and formatting",
  "description": "Read ChatCrystal’s scripts and Flat Config, verify ESLint diagnostics offline, and distinguish syntax rules, type checking, formatting, and CI gates.",
  "url": "https://zengliangyi.com/en/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/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/lint-format-tooling/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Code tooling: the actual roles of Biome, ESLint, and formatting",
      "item": "https://zengliangyi.com/en/blog/lint-format-tooling/"
    }
  ]
}
```
