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
Getting Started 通りにとりあえず進める
$ pnpm biome init
をすると設定ファイルが作成された
{
"$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 なので無効にしたい
を参考にファイル名を指定して設定を上書きできるので
❯ 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.
4 │ import "./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.
4 │ import "./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 3 │
4 4 │ // https://vitejs.dev/config/
./eslint.config.js organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements could be sorted:
1 1 │ import 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 3 │ import reactRefresh from "eslint-plugin-react-refresh";
5 │ - import·tseslint·from·"typescript-eslint";
4 │ + import·globals·from·"globals";
5 │ + import·tseslint·from·"typescript-eslint";
6 6 │
7 7 │ export default tseslint.config(
./src/App.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements could be sorted:
1 1 │ import { 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 4 │ import "./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 だと設定が結構面倒だったのでデフォルトで入っているのはありがたい
Formatter の Introdcution を読む
ref: https://biomejs.dev/ja/formatter/
prettier に強く影響を受けていて同じく opinionated であり、細かい挙動でより良い点があると説明されていた。
ignore については
// biome-ignore format: <説明文>
で ignore できるらしい。
Linter の Introduction を読む
eslint との違いで面白いのが unsafe な変更もサポートされていること
安全ではない修正(Unsafe fixes)Section titled 安全ではない修正(Unsafe fixes)
安全ではない修正は、プログラムのセマンティクスを変更する可能性があります。そのため、変更を手動でレビューすることをおすすめします。安全ではない修正 を適用するには、--write --unsafeを使用します:
安全に修正できない問題も、一応そのルールが通るように修正するオプションが提供されている。
ルールが通るだけでそれによってセマンティックが変わりうるので、レビューは一応してねという立ち位置。
ignore の仕方
// biome-ignore lint: <explanation>
// biome-ignore lint/suspicious/noDebugger: <explanation>
eslint のルールを移行してみる
公式のマイグレーションツールがあるようなので試してみる
元の 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
オプションがあるが同様のものはないか探したけど見当たらなかった
サポート範囲
現時点では format 対象が (prettier と比べちゃうと)まだ不足がある印象
個人的には
- HTML
- CSS
- YAML
- Markdown
この辺のフォーマットも prettier に任せることが多かったので入ってくると嬉しい
VSCode への統合
やりたいこと:
- 特定の拡張子ファイルは onSave で自動フォーマット、safe な自動修正
- Lint ルールに違反するときエディタ上でエラー表示が出る
結論:
{
"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 にしたいのでまだかなという気持ち