Open22

Biomeを触ってみる

きむそんきむそん

eslint + prettier で困ってはないけど最近良く名前聞くようになってきたので触ってみる

きむそんきむそん

Setup

$ pnpx create-vite --template react-ts
✔ Project name: … biome-sample

Scaffolding project in /Users/kaito/Apps/biome-sample...

Done. Now run:

  cd biome-sample
  pnpm install
  pnpm run dev

これでお試し用のプロジェクトが作れた

$ cd biome-sample
$ pnpm i
$ pnpm add -D @biomejs/biome
きむそんきむそん
$ pnpm biome init

をすると設定ファイルが作成された

biome.json
{
	"$schema": "https://biomejs.dev/schemas/1.9.3/schema.json",
	"vcs": {
		"enabled": false,
		"clientKind": "git",
		"useIgnoreFile": false
	},
	"files": {
		"ignoreUnknown": false,
		"ignore": []
	},
	"formatter": {
		"enabled": true,
		"indentStyle": "tab"
	},
	"organizeImports": {
		"enabled": true
	},
	"linter": {
		"enabled": true,
		"rules": {
			"recommended": true
		}
	},
	"javascript": {
		"formatter": {
			"quoteStyle": "double"
		}
	}
}

中身はあとでみるが、linter, formatter がそれぞれ分かれているので例えば linter は無効にして eslint は使いつつ、prettier だけ biome に置き換えるみたいなこともできそう

きむそんきむそん

lint, fomat, check をそれぞれ実行してみる
ぱっと見でチェックするコマンドと、自動修正するコマンドが別れているのかなと思ったけどそうではなく、自動修正するかどうかは --write で指定する形

check は lint, format をまとめて実行する(つまり check = lint + format)という関係性らしい

format

pnpm biome format .                   
./package.json format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Formatter would have printed the following content:
  
     1  1{
     2    │ - ··"name""biome-sample",
     3    │ - ··"private":·true,
     4    │ - ··"version""0.0.0",
     5    │ - ··"type""module",
     6    │ - ··"scripts"{
     7    │ - ····"dev""vite",
     8    │ - ····"build""tsc·-b·&&·vite·build",
     9    │ - ····"lint""eslint·.",
    10    │ - ····"preview""vite·preview"
    11    │ - ··},
    12    │ - ··"dependencies"{
    13    │ - ····"react""^18.3.1",
    14    │ - ····"react-dom""^18.3.1"
    15    │ - ··},
    16    │ - ··"devDependencies"{
    17    │ - ····"@biomejs/biome""^1.9.3",
    18    │ - ····"@eslint/js""^9.11.1",
    19    │ - ····"@types/react""^18.3.10",
    20    │ - ····"@types/react-dom""^18.3.0",
    21    │ - ····"@vitejs/plugin-react""^4.3.2",
    22    │ - ····"eslint""^9.11.1",
    23    │ - ····"eslint-plugin-react-hooks""^5.1.0-rc.0",
    24    │ - ····"eslint-plugin-react-refresh""^0.4.12",
    25    │ - ····"globals""^15.9.0",
    26    │ - ····"typescript""^5.5.3",
    27    │ - ····"typescript-eslint""^8.7.0",
    28    │ - ····"vite""^5.4.8"
    29    │ - ··},
    30    │ - ··"packageManager""pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903"
        2 │ + → "name""biome-sample",
        3 │ + → "private":·true,
        4 │ + → "version""0.0.0",
        5 │ + → "type""module",
        6 │ + → "scripts"{
        7 │ + → → "dev""vite",
        8 │ + → → "build""tsc·-b·&&·vite·build",
        9 │ + → → "lint""eslint·.",
       10 │ + → → "preview""vite·preview"
       11 │ + → },
       12 │ + → "dependencies"{
       13 │ + → → "react""^18.3.1",
       14 │ + → → "react-dom""^18.3.1"
       15 │ + → },
       16 │ + → "devDependencies"{
       17 │ + → → "@biomejs/biome""^1.9.3",
       18 │ + → → "@eslint/js""^9.11.1",
       19 │ + → → "@types/react""^18.3.10",
       20 │ + → → "@types/react-dom""^18.3.0",
       21 │ + → → "@vitejs/plugin-react""^4.3.2",
       22 │ + → → "eslint""^9.11.1",
       23 │ + → → "eslint-plugin-react-hooks""^5.1.0-rc.0",
       24 │ + → → "eslint-plugin-react-refresh""^0.4.12",
       25 │ + → → "globals""^15.9.0",
       26 │ + → → "typescript""^5.5.3",
       27 │ + → → "typescript-eslint""^8.7.0",
       28 │ + → → "vite""^5.4.8"
       29 │ + → },
       30 │ + → "packageManager""pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903"
    31 31}
    32 32

修正してほしい内容がわかりやすい

--write してみる

pnpm biome format --write .
./tsconfig.app.json:9:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ JSON standard does not allow comments.
  
     7"skipLibCheck": true,
     8> 9 │     /* Bundler mode */
       │     ^^^^^^^^^^^^^^^^^^
    10"moduleResolution": "bundler",
    11"allowImportingTsExtensions": true,

これは lint では?という気もするが、JSON にはコメントかけないよというエラーがでた。
tsconfig に関しては jsonc なので無効にしたい

https://biomejs.dev/ja/internals/language-support/#jsoncサポート

を参考にファイル名を指定して設定を上書きできるので

❯ git diff biome.json
diff --git a/biome.json b/biome.json
index beb54d0..bc3e2b3 100644
--- a/biome.json
+++ b/biome.json
@@ -26,5 +26,15 @@
                "formatter": {
                        "quoteStyle": "double"
                }
-       }
+       },
+       "overrides": [
+               {
+                       "include": ["tsconfig.*.json", "tsconfig.json"],
+                       "json": {
+                               "parser": {
+                                       "allowComments": true
+                               }
+                       }
+               }
+       ]
 }

これで format が通るようになった。

lint

pnpm biome lint .
./src/main.tsx:6:12 lint/style/noNonNullAssertion ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Forbidden non-null assertion.
  
    4import "./index.css";
    5> 6 │ createRoot(document.getElementById("root")!).render(
      │            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    7<StrictMode>
    8<App />
  

./src/App.tsx:12:34 lint/a11y/noBlankTarget  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Avoid using target="_blank" without rel="noreferrer".
  
    10<>
    11<div>
  > 12<a href="https://vitejs.dev" target="_blank">
       │                                                             ^^^^^^^^^^^^^^^
    13<img src={viteLogo} className="logo" alt="Vite logo" />
    14</a>
  
  ℹ Opening external links in new tabs without rel="noreferrer" is a security risk. See the explanation for more details.
  
  ℹ Safe fix: Add the rel="noreferrer" attribute.
  
    12 │ → → → → <a·href="https://vitejs.dev"·target="_blank"·rel="noreferrer">
       │                                                     +++++++++++++++++ 

./src/App.tsx:15:33 lint/a11y/noBlankTarget  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Avoid using target="_blank" without rel="noreferrer".
  
    13<img src={viteLogo} className="logo" alt="Vite logo" />
    14</a>
  > 15<a href="https://react.dev" target="_blank">
       │                                                            ^^^^^^^^^^^^^^^
    16<img src={reactLogo} className="logo react" alt="React logo" />
    17</a>
  
  ℹ Opening external links in new tabs without rel="noreferrer" is a security risk. See the explanation for more details.
  
  ℹ Safe fix: Add the rel="noreferrer" attribute.
  
    15 │ → → → → <a·href="https://react.dev"·target="_blank"·rel="noreferrer">
       │                                                    +++++++++++++++++ 

./src/App.tsx:21:5 lint/a11y/useButtonType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Provide an explicit type prop for the button element.
  
    19<h1>Vite + React</h1>
    20<div className="card">
  > 21<button onClick={() => setCount((count) => count + 1)}>
       │                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    22 │                                        count is {count}
    23</button>
  
  ℹ The default type of a button is submit, which causes the submission of a form when placed inside a `form` element. This is likely not the behaviour that you want inside a React application.
  
  ℹ Allowed button types are: submit, button or reset
  

Checked 12 files in 3ms. No fixes applied.
Found 4 errors.
lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Some errors were emitted while running checks.

特にルール追加したりしていないが、nonNullAssertion の禁止だったり、noreferrer をつけない externalLink の禁止だったりのルールが適用されているらしい

自動修正してみる

$ pnpm biome lint --write .
./src/main.tsx:6:12 lint/style/noNonNullAssertion ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Forbidden non-null assertion.
  
    4import "./index.css";
    5> 6 │ createRoot(document.getElementById("root")!).render(
      │            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    7<StrictMode>
    8<App />
  

./src/App.tsx:21:5 lint/a11y/useButtonType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Provide an explicit type prop for the button element.
  
    19<h1>Vite + React</h1>
    20<div className="card">
  > 21<button onClick={() => setCount((count) => count + 1)}>
       │                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    22 │                                        count is {count}
    23</button>
  
  ℹ The default type of a button is submit, which causes the submission of a form when placed inside a `form` element. This is likely not the behaviour that you want inside a React application.
  
  ℹ Allowed button types are: submit, button or reset
  

Checked 12 files in 2ms. Fixed 1 file.
Found 2 errors.
lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Some errors were emitted while running checks.

noreferrer をつけるだけのところは自動修正されたが、nonNullAssertion は自動修正できないので一部エラーが残った。4件→2件

手動で直して lint が通った

pnpm biome lint --write .
Checked 12 files in 19ms. No fixes applied.

check

$ pnpm biome check .         
./vite.config.ts organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Import statements could be sorted:
  
    1   │ - import·{·defineConfig·}·from·"vite";
    2   │ - import·react·from·"@vitejs/plugin-react";
      1 │ + import·react·from·"@vitejs/plugin-react";
      2 │ + import·{·defineConfig·}·from·"vite";
    3 34 4 │   // https://vitejs.dev/config/
  

./eslint.config.js organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Import statements could be sorted:
  
     1  1import js from "@eslint/js";
     2    │ - import·globals·from·"globals";
     3    │ - import·reactHooks·from·"eslint-plugin-react-hooks";
        2 │ + import·reactHooks·from·"eslint-plugin-react-hooks";
     4  3import reactRefresh from "eslint-plugin-react-refresh";
     5    │ - import·tseslint·from·"typescript-eslint";
        4 │ + import·globals·from·"globals";
        5 │ + import·tseslint·from·"typescript-eslint";
     6  67  7export default tseslint.config(
  

./src/App.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Import statements could be sorted:
  
     1  1import { useState } from "react";
     2    │ - import·reactLogo·from·"./assets/react.svg";
     3    │ - import·viteLogo·from·"/vite.svg";
        2 │ + import·viteLogo·from·"/vite.svg";
        3 │ + import·reactLogo·from·"./assets/react.svg";
     4  4import "./App.css";
     5  5 │   
  

Skipped 3 suggested fixes.
If you wish to apply the suggested (unsafe) fixes, use the command biome check --fix --unsafe

Checked 12 files in 2ms. No fixes applied.
Found 3 errors.
check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Some errors were emitted while running checks.

check = lint + format と書いたけど、それ以外に Import 文の整理もやってくれるらしい。eslint にも同様のルールが有り lint ではなくまた別なのが少し直感に反するが、そういうものらしい。

自動修正してみる

$ pnpm biome check --write .
Checked 12 files in 2ms. Fixed 3 files.

自動修正できた。
この辺 eslint だと設定が結構面倒だったのでデフォルトで入っているのはありがたい

きむそんきむそん

Linter の Introduction を読む

ref: https://biomejs.dev/ja/linter/

きむそんきむそん

eslint との違いで面白いのが unsafe な変更もサポートされていること

安全ではない修正(Unsafe fixes)Section titled 安全ではない修正(Unsafe fixes)
安全ではない修正は、プログラムのセマンティクスを変更する可能性があります。そのため、変更を手動でレビューすることをおすすめします。

安全ではない修正 を適用するには、--write --unsafeを使用します:

安全に修正できない問題も、一応そのルールが通るように修正するオプションが提供されている。
ルールが通るだけでそれによってセマンティックが変わりうるので、レビューは一応してねという立ち位置。

きむそんきむそん

ignore の仕方

// biome-ignore lint: <explanation>
// biome-ignore lint/suspicious/noDebugger: <explanation>
きむそんきむそん

eslint のルールを移行してみる

https://biomejs.dev/ja/guides/migrate-eslint-prettier/
公式のマイグレーションツールがあるようなので試してみる

元の eslint ルールは以下の通り:

import js from "@eslint/js";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
import tseslint from "typescript-eslint";

export default tseslint.config(
  { ignores: ["dist"] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      "react-hooks": reactHooks,
      "react-refresh": reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      "react-refresh/only-export-components": [
        "warn",
        { allowConstantExport: true },
      ],
    },
  },
);
きむそんきむそん
$ pnpm biome migrate eslint --write

を実行すると、biome.json にルールが書き出された

❯ git diff ./biome.json
diff --git a/biome.json b/biome.json
index bb3524a..3dfb016 100644
--- a/biome.json
+++ b/biome.json
@@ -1,39 +1,273 @@
 {
   "$schema": "https://biomejs.dev/schemas/1.9.3/schema.json",
-  "vcs": {
-    "enabled": false,
-    "clientKind": "git",
-    "useIgnoreFile": false
-  },
-  "files": {
-    "ignoreUnknown": false,
-    "ignore": []
-  },
-  "formatter": {
-    "enabled": true,
-    "indentStyle": "space",
-    "indentWidth": 2
-  },
-  "organizeImports": {
-    "enabled": true
-  },
+  "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
+  "files": { "ignoreUnknown": false, "ignore": [] },
+  "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 },
+  "organizeImports": { "enabled": true },
   "linter": {
     "enabled": true,
-    "rules": {
-      "recommended": true
-    }
-  },
-  "javascript": {
-    "formatter": {
-      "quoteStyle": "double"
-    }
+    "rules": { "recommended": false },
+    "ignore": ["dist"]
   },
+  "javascript": { "formatter": { "quoteStyle": "double" } },
   "overrides": [
     {
       "include": ["tsconfig.*.json", "tsconfig.json"],
-      "json": {
-        "parser": {
-          "allowComments": true
+      "json": { "parser": { "allowComments": true } }
+    },
+    {
+      "include": ["**/*.{ts,tsx}"],
+      "linter": {
+        "rules": {
+          "complexity": {
+            "noExtraBooleanCast": "error",
+            "noMultipleSpacesInRegularExpressionLiterals": "error",
+            "noUselessCatch": "error",
+            "noWith": "error"
+          },
+          "correctness": {
+            "noConstAssign": "error",
+            "noConstantCondition": "error",
+            "noEmptyCharacterClassInRegex": "error",
+            "noEmptyPattern": "error",
+            "noGlobalObjectCalls": "error",
+            "noInvalidBuiltinInstantiation": "error",
+            "noInvalidConstructorSuper": "error",
+            "noNonoctalDecimalEscape": "error",
+            "noPrecisionLoss": "error",
+            "noSelfAssign": "error",
+            "noSetterReturn": "error",
+            "noSwitchDeclarations": "error",
+            "noUndeclaredVariables": "error",
+            "noUnreachable": "error",
+            "noUnreachableSuper": "error",
+            "noUnsafeFinally": "error",
+            "noUnsafeOptionalChaining": "error",
+            "noUnusedLabels": "error",
+            "noUnusedPrivateClassMembers": "error",
+            "noUnusedVariables": "error",
+            "useIsNan": "error",
+            "useValidForDirection": "error",
+            "useYield": "error"
+          },
# 長くなってしまうので割愛

ignore だったりルールの適用範囲に関する設定は良い感じに移行されていそう。

Biomeがいくつかのルールオプションを実装しないか、元の実装からわずかに逸脱することを選択したため、ESLintとまったく同じ動作を得る可能性は低いことに注意してください。

とあるように、当然 ESLint のほうが表現できるルールは広いので移行できなかったルールが分かると嬉しいなと思ったが出力にそういった情報はなかった(オプションとしても見当たらなかった)

きむそんきむそん

CLI オプションを見る

きむそんきむそん

個人的によく使っていたESLintのオプションと対応関係がありそうなものや、気になったもの

  • --no-errors-on-unmatched:
    • Silence errors that would be emitted in case no files were processed during the execution of the command.

    • 対象ファイルが0件のときに異常終了させないオプション
    • lefthook 等 commit hook と組み合わせるときに指定しがち
  • --staged
    • ステージされているファイルのみに check をかけられる模様
    • 例えば husky を使うときに lint-staged を使う必要がなくなりそうなのと、ローカル開発で雑に実行するときは全ファイル実行すると遅いのでこのオプションで実行するとかも良さげ

ESLint や prettier には --cache オプションがあるが同様のものはないか探したけど見当たらなかった

きむそんきむそん
きむそんきむそん

やりたいこと:

  • 特定の拡張子ファイルは onSave で自動フォーマット、safe な自動修正
  • Lint ルールに違反するときエディタ上でエラー表示が出る
きむそんきむそん

結論:

.vscode/settings.json
{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[javascript][json][jsonc]": {
    "editor.defaultFormatter": "biomejs.biome",
    "editor.formatOnSave": true
  },
  "editor.codeActionsOnSave": {
    "quickfix.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  }
}

ESLint, prettier と同じように設定するだけでいけた

モノレポは試してない

きむそんきむそん

所感

  • ドキュメントもわかりやすく特に困らずにセットアップできた
  • 設定が煩雑にならなくてとても良い!
  • --staged オプションが biome 自体に組み込まれていたり、unsafe な autofix も行えたりと使いやすそう
  • 現時点だとルールや format できる対象のファイルが ESLint, prettier には及ばない
    • 重要度の高いところはサポートされている印象で、ルールをガチガチに組むつもりがないなら設定ファイルも少ないし依存も少ないので biome だけ入れて、もありかなと思った。ライト層向け
    • 逆に自分もそうだけど暗黙知をできるだけ ESLint Rule で縛りたいみたいな思想の人はまだ不足を感じそう
    • prettier だけ置き換える、とかもありだが、markdown や HTML のフォーマットをしなくて良いならという制約付き。自分はこの辺も opinionated にしたいのでまだかなという気持ち