🐿️

ESLintでガッツリ縛ったプロダクトへの段階的Biome導入の記録(前篇)

2024/12/26に公開

この記事はなに?

Linter周りの開発体験が悪いから、Biomeに載せ替えたら快適になるんじゃない?の取り組み
年末年始のカイゼン活動をしたい発作の記録

😰 課題: 待ち時間が長いと、集中の寸断が発生して効率を落とす

  • ESLintをローカルで実行すると1分30秒くらい掛かっていた
  • エディタのESLint拡張が重たくなる時がある
  • pre-commitが長い

CIは...早いに越したことは無いがテスト系がもっと長いので一旦課題感は小さい

環境

  • yarn workspaceを用いたmonorepo(立ち上げ約5年の大きめのプロジェクト)
  • React.js, TypeScriptを用いたSPA
  • 導入Biomeのバージョンは 1.9.4(2系のLTSが待ち遠しい)

自己紹介(あなたは誰?)

株式会社グロービスのデジタルプラットフォーム部門のGLOPLA事業開発室のエンジニアです。主にフロントエンドを強みとしていますが、毎日の様にバックエンドも書いています。React.js, TypeScript, GraphQL, Ruby on Railsで開発しています。

毎年年末年始に大きめのカイゼン活動をしたい衝動にかられるため、2023年は webpack から vite に載せ替えを行うなどしています。

グロービス - GLOPLA事業開発室の紹介はこちら

目指す状態

ESLintとBiomeの併用で良いとこ取り

  • 基本のルールはBiomeで縛る(1秒未満を目指す)
  • 独自ルールやLibrary推奨のルールはESLintで縛る(5秒以内を目指す)

完全置き換えから併用に至るまでの思考過程

インストールして recommended のルールでエラーは800件くらいでている状態で
実行時間 約0.3秒 で実行できた。確かに早い!

課題: BiomeはESLintのルールを完全に置き換えるものではない

  • 独自ルールが作れない
  • Library推奨の npm package として公開されているrulesが無い(testing-libraryやvitestなど)

対策: 併用で良いとこ取りをしよう

  • 完全置き換えは現実的にできない
    • Biomeで実現できないルールを捨てるか?
      • 一部はできるが、有用なルールは捨てられない
    • Biomeをやめるか?
      • 早いは正義!
      • webpackからviteに載せ替えた時のアレに近い効果を得られるならやりたい
  • ESLintとBiomeを併用しよう
    • Biomeに基本ルールを移せばトータルの実行時間は削減出来る
      • ESLintのどのルールが実行時間を長くしているのかは別途調査が必要
    • 複数のライブラリを入れて管理が煩雑にならないか?
      • むしろ上手く行けば prettier や eslint rules library を減らすことが出来る
    • 独自ルールやLibrary推奨のルールはESLintで縛るのは継続出来る

Biomeの導入

Biome公式 Getting StartedBiome公式 Migrate from ESLint & Prettierを参考にしていく。

1. Biomeはrootに1つだけ導入する(monorepoの話)

最初にyarn workspace で、それぞれのworkspaceごとにbiomeを導入しようとした。背景としては、ESLintはworkspaceごとに .eslintrc.js で設定を個別で書いていたからそれに合わせた形だった。

# ⭕️ rootにだけ導入で良さそう
yarn add --dev --exact @biomejs/biome

# ❌️ 当初はworkspaceごとに導入しようとしていた
yarn workspace packageA add --dev --exact @biomejs/biome
yarn workspace packageB add --dev --exact @biomejs/biome
yarn workspace packageC add --dev --exact @biomejs/biome

Q. workspaceごとにルールを変えたかったら?

A. workspaceごとに biome.json を作成する

  • biomeは実行された場所を起点に biome.json を探す
  • 実行階層に biome.json が有ればその設定を見てくれる
  • 無ければ上に遡ってrootの biome.json を見に行く

※ 噂によると VSCode などの拡張では root だけを読みに行くらしい?また workspace 拡張で開いた場合はどうなるのだろう?

Biome公式 Big Projects
Biome の導入と設定方法まとめ - 設定と振る舞い

Q. ルールを継承したければ?

A. workspace の biome.json にrootの biome.jsonextends する

root/packages/packageA/biome.json
{
  "extends": ["../biome.json"]
}

2. migrateコマンドで導入

Biome公式 Migrate from ESLint & Prettier
メインのパッケージのルールを元にmigrateするために、特定配下で biome migrate コマンドを実行する。

cd root/packages/packageA # 任意の.eslintrc.jsがある階層で
yarn biome migrate eslint --write
yarn biome migrate prettier --write

biome.jsonが作成されるが、初動を楽にしてくれるくらいの気持ちでどんどん編集して落とし所を探そう。

3. 小さく段階的に導入するために、差分の出るルールをoffにする

段階的に導入するため、まずほ基本ルールは全て off で導入する。
ESLintと完全に同じ部分はそのまま ON で recommended を受け入れる。

{
  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
  "vcs": {
    "enabled": false,
    "clientKind": "git",
    "useIgnoreFile": true // おそらく root の .gitignore しか見れていない
  },
  "files": {
    "ignoreUnknown": false,
    // NOTE: "packages/**/*.[ts,tsx,js,jsx,json]" の様な指定はできないらしい。。。
    "include": [
      "packages/**/*.ts",
      "packages/**/*.tsx",
      "packages/**/*.js",
      "packages/**/*.jsx",
      "packages/**/*.json"
    ],
    "ignore": [
      "**/node_modules/**",
    ]
  },
  "formatter": {
    "enabled": false, // prettierを取り除くまで OFF
  },
  "organizeImports": {
    "enabled": false // 差分が多すぎるので一旦 OFF
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true, // 推奨ルールは ON にしていきたい
      // 省略するが、一旦 recommended の差分の多いルールは OFF にして導入だけ果たす
      // 段階的に1ルールずつPRを作成して適用する
      "style": {
        "useImportType": "error", // これだけ ESLint とほぼ互換があったので ON にしてみた例
        "useNumberNamespace": "off",
      },
    }
  },
  "javascript": {
    "jsxRuntime": "reactClassic" // Reactのimportを一旦許す v18移行では必要無いが差分が大きかった
  }
}

4. CIで動く様にする

ESLintが動いているCIに Biome を追加するだけでOKなので、特筆することはなかった。
GitHub Actions や CircleCI などに用意してある script を動かせば良い

余談: workspaceトップのscriptsイメージ

workspaceごとにeslintを回していれば、biome + 並列でeslintを回すのがおすすめかも

root/package.json
"scripts": {
  "lint": "yarn biome check && yarn eslint-parallel",
  "lint:ci": "yarn biome ci && yarn eslint-parallel",
  "eslint-parallel": "yarn workspaces foreach -Api --exclude $(basename $PWD) run lint",
}

また、biomeは lint, check, ci, format 等があるため、用途によって使い分ける。
(biome checkはlint, format, import sortingを実行する)
Biome CLI公式コマンド一覧

余談: Localではエラー0なのに、CIでだけエラーが大量に出る!!?

CI実行時に生成しているファイルのignoreを忘れていただけだった。
Localには無いファイルだったので、てっきり biome.json が CI上でだけ読み込まれていないかと思って慌ててしまった。

5. pre-commitでESLintとBiomeを併用する

現状: prettier & husky & lint-staged でpre-commitを実行している
=> ESLint & Biome & prettier の併用が必要になる?

Lefthookを導入する

husky & lint-staged の効果を1パッケージで実現してくれる。
Biome公式でも紹介されていて、記述が楽だったので乗り換え検討

root/.lefthook.yml
pre-commit:
  commands:
    check:
      glob: "packages/**/*.{js,ts,jsx,tsx,json}"
      run: npx @biomejs/biome check --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}

今回は biome 分だけ
yarn workspaceでgit差分だけをチェックするときに、staged_filesで上手く調整するのが難しかったので後日挑戦する。

ESLintの実行時間の長いルールを削る or Biomeに移行する

Biome導入前にやっておけばよかった...と思いつつ

1. ルールごとに実行時間を確認する

想像以上に顕著に実行時間が長い物が出た。
本当に必要なルールなのか?を問いかける必要がある。

$ TIMING=1 yarn eslint
Rule                                    | Time (ms) | Relative
:---------------------------------------|----------:|--------:
@typescript-eslint/no-unsafe-assignment | 11810.625 |    23.3%
import/no-cycle                         |  6642.921 |    13.1%
import/no-extraneous-dependencies       |  3880.751 |     7.7%
@typescript-eslint/no-unsafe-argument   |  3038.600 |     6.0%
import/no-relative-packages             |  2969.991 |     5.9%
@typescript-eslint/no-redeclare         |  1824.819 |     3.6%
@typescript-eslint/naming-convention    |  1530.939 |     3.0%
import/no-duplicates                    |  1180.910 |     2.3%
import/order                            |  1064.651 |     2.1%
react/no-array-index-key                |   872.323 |     1.7%

TS系のASTを必要とするルールの実行時間が長い

試しに off にしてみて何秒短縮されるか検証すると TS系のASTを必要とするルールが目立った。

@typescript-eslint/parser には型情報をキャッシュする機構が無いらくし、毎回生成してしまうらしい。入り組んだ型が eslint --cache で変更の有るファイルだけのLintでも、結果的に型情報は遠くの型までコンパイルする必要があるのだろうか?

import/no-cycle を biome で置き換えられないか?

循環参照のチェックなので時間が掛かるのは理解出来る。
Biomeで完全互換のルールは無いらしいが、useImportRestrictionsというルールである程度循環参照を防げるらしい。

完全互換は無くとも、1から考えて「達成したい状態」を満たせるのであれば近いルールを適用するのはありだと思う。

2. どんなルールが実際に適用されているか確認する

大量に適用されているルールを見直すことで純粋に削減できないか?
Biome公式のRules Sourcesで互換性はあるか?

yarn eslint --print-config
{
  # 大量に出力されるESLintの設定(記事中の値はAIが出力したダミー)
  "rules": {
    "import/no-cycle": "error",
    "import/no-extraneous-dependencies": "error",
    "import/no-relative-packages": "error",
    "import/no-duplicates": "error",
    "import/order": "error",
  },
}

3. 不要なルール or 互換性のあるルールを削除する

ルールの削除や互換性のあるBiomeルールへの置き換えは、1ルール1PRがおすすめ
PRレベルで合意形成と周知が行える。
また、差分が大きすぎるとConflictも発生しやすいし、レビュアーに負担が大きい。

削減結果(前編まで)

今回は、キャッシュありで、ESLintのルールを「1分30秒」から「5秒」程度に削減した。
ただ、キャッシュ無しでは、1分以上掛かっているので、さらなる削減が必要

Biomeは相変わらず 0.3秒程度で実行される。

後編に期待される内容

  • VSCodeなどでBiome拡張を入れる導線とREADMEの更新
  • Formatterの置き換え(prettierの削除)
  • import sortingの導入
  • ESLintのルール削減 & Biomeへの移行で、キャッシュ無しで 5秒以内を目指す
    • 1PRで1ルールずつ段階的に行う
    • ルールごとに互換先と意思決定の過程などを記事にできたら良いかも?
GitHubで編集を提案
GLOBIS Tech

Discussion