🪄

PrettierだけBiomeに移行してみた

に公開

はじめに

株式会社INFLUでサブプロダクトのPOをやっている、Nakano as a Serviceです。

最近、フォーマッターをPrettierからBiomeに移行しました。ついでにインポートソートもESLintからBiomeに移行しました。この記事では、移行の動機から具体的な設定方法までを紹介します。同じように移行を検討している方の参考になればと思います。

移行の動機

開発速度の向上

BiomeはRustで実装されており、フォーマット処理が高速です。開発中に保存のたびに実行されるフォーマットが速くなることで、ストレスを若干軽減できると考えました。

将来のESLint移行への布石

将来的にESLintもBiomeに移行しようかなと考えています。まずはフォーマッターとインポートソートから段階的に移行することで、リスクを抑えながら移行を進めたいと考えました。

興味本位

実際のところ単純に新しいツールを試してみたかったというのが一番大きいです。もしこの選択が間違いだったとしても簡単に後戻りできるだろうと考え気軽に決断しました。

移行内容

Biome設定ファイルの作成

まず、Biomeの設定ファイルを作成します。ルートディレクトリにbiome.jsoncを作成しました。

{
  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
  // ... 設定内容 ...
}

設定ファイルを作成する際のポイント:

  1. 拡張子をjsoncにする:JSON with Comments形式を使用することで、設定ファイル内にコメントを記述できます。設定の意図をコメントで説明できるため、可読性が向上します。

  2. $schemaを追加する$schemaプロパティにBiomeの設定スキーマのパスを指定することで、エディタでの補完や型チェックが効くようになります。これにより、設定項目のタイプミスを防ぎ、利用可能な設定をエディタが提案してくれます。

Prettier設定のBiomeへの移行

既存のPrettier設定に合わせて、Biomeのフォーマット設定を調整しました。主な設定項目は以下の通りです。

移行前のPrettier設定

const config = {
  endOfLine: "lf",
  tabWidth: 2,
  printWidth: 80,
  useTabs: false,
  singleQuote: false,
  semi: false,
  trailingComma: "all",
}

移行後のBiome設定

{
  "formatter": {
    "enabled": true,
    "indentStyle": "space"
  },
  "javascript": {
    "formatter": {
      "semicolons": "asNeeded",
      "quoteStyle": "double"
    }
  }
}

主な対応関係:

  • semi: falsesemicolons: "asNeeded"(セミコロンは必要な場合のみ)
  • useTabs: falseindentStyle: "space"(スペースでインデント)
  • singleQuote: falsequoteStyle: "double"(ダブルクォートを使用)
  • tabWidth: 2はBiomeのデフォルトが2のため、明示的な設定は不要

リンターの無効化

現時点ではBiomeのフォーマッターとインポートソートのみを使用しており、リンター機能は使用していません。そのため、リンターを無効化しています。

{
  "linter": {
    "enabled": false
  }
}

将来的にESLintからBiomeのリンターに移行する場合は、この設定をenabled: trueに変更します。

CSS設定

Tailwind CSSを使用しているプロジェクトでは、CSSパーサーにTailwindディレクティブのサポートを有効化します。

{
  "css": {
    "parser": {
      "tailwindDirectives": true
    }
  }
}

この設定により、@tailwind@apply@layerなどのTailwind CSSディレクティブが正しくパースされ、フォーマット時にエラーが発生しなくなります。

Gitとの連携設定

BiomeでGitリポジトリと統合するため、vcs設定を追加しました。この設定により、BiomeがGitリポジトリを認識し、適切に動作します。

{
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true,
    "defaultBranch": "main"
  }
}

各設定項目の説明:

  • enabled: true:VCS統合を有効化します
  • clientKind: "git":Gitを使用することを明示します
  • useIgnoreFile: true.gitignoreファイルを参照して、チェック対象から除外するファイルを決定します
  • defaultBranch: "main":デフォルトブランチをmainに設定します。これにより、マージコンフリクトマーカーの検出などが適切に動作します

チェック対象ファイルの設定

Biomeがチェック対象とするファイルをfiles設定で指定します。

{
  "files": {
    "includes": ["**", "!**/.next", "!**/.turbo"]
  }
}

この設定により、すべてのファイル(**)をチェック対象に含めつつ、/.nextディレクトリと/.turboディレクトリを除外します。Next.jsのビルド成果物やTurborepoのキャッシュディレクトリはチェック対象外とするため、パフォーマンスが向上します。

インポートのソート・グルーピング設定

実現したいこと

インポートのソート・グルーピングを設定すると、Biomeが自動的にインポート文を整理します。具体的には、インポートの種類(標準ライブラリ、外部パッケージ、エイリアス、相対パスなど)によってグループ化し、各グループ内でアルファベット順にソートします。グループの間には空行が自動的に挿入されます。

整理後の例は以下の通りです:

import path from "node:path"

import { format } from "date-fns"
import { useState } from "react"
import { z } from "zod"

import { getUser } from "~/lib/api"

import { Button } from "./components/button"

これは保存時に自動的に実行されるため、開発速度に大きく影響します。Biomeのフォーマットがいくら高速でも、ESLintのインポートソートが毎回実行されると、結局ESLintに律速されてしまいます。そのため、インポートソートもBiomeに移行しました。

Biomeの設定

{
  "assist": {
    "enabled": true,
    "actions": {
      "source": {
        "organizeImports": {
          "level": "on",
          "options": {
            "groups": [
              // 標準ライブラリ・DenoのURLインポート('node:path' など)
              [":NODE:", ":BUN:", ":URL:"],
              ":BLANK_LINE:",
              // 外部のパッケージ('react' など)
              [":PACKAGE:", ":PACKAGE_WITH_PROTOCOL:", "!@repo/**"],
              ":BLANK_LINE:",
              // モノレポのパッケージ('@repo/ui' など)
              "@repo/**",
              ":BLANK_LINE:",
              // エイリアス('~/lib/firebase' など)
              ":ALIAS:",
              ":BLANK_LINE:",
              // パス('./components/nav-bar' など)
              ":PATH:"
            ]
          }
        }
      }
    }
  }
}

ESLintのインポートソート設定の無効化

Biomeでインポートソートを管理するため、ESLintのインポートソート関連のルールを無効化しました。

社内の共通ルールセットを読み込んだ上で、以下の2つのルールを無効化します:

  • sort-imports(ESLint標準インポートのソート)
  • import/order(eslint-plugin-importのグルーピング)

設定例

import myCompanyRule from "@my-company/rule"

const eslintConfig = [
  ...myCompanyRule,
  {
    ignores: ["**/*.config.mjs", "node_modules/", "dist/", ".next/"],
  },
  {
    rules: {
      // ... 他のルール ...
      
      // Biomeでインポートソートを管理するため無効化
      "import/order": "off",
      "sort-imports": "off",
    }
  }
]

export default eslintConfig

無効化の理由は、BiomeとESLintで重複してインポートソートを実行すると、競合やパフォーマンスの低下が発生する可能性があるためです。

VSCode設定の変更

開発体験を向上させるため、VSCodeの設定も変更しました。

.vscode/settings.json

デフォルトフォーマッターをBiomeに変更し、保存時に自動フォーマットとインポート整理が実行されるように設定しました:

{
  "biome.enabled": true,
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.organizeImports.biome": "explicit"
  },
  "extensions.ignoreRecommendations": false
}

.vscode/extensions.json

推奨エクステンションを更新し、Biomeエクステンションを追加しました:

{
  "recommendations": [
    "biomejs.biome",
    "streetsidesoftware.code-spell-checker",
    "dbaeumer.vscode-eslint"
  ]
}

チェックコマンドの統一

Biomeでは、インポートソートはlintでもformatでもなくactionとして扱われます。biome checkコマンドを実行すると、以下の3つがまとめて実行されます:

  • lint:コードの品質チェック
  • format:コードのフォーマット
  • action:インポートの整理など

我々もこれにならい、package.jsoncheckスクリプトを以下のように設定しました:

{
  "scripts": {
    "check": "biome check . && pnpm run lint",
    "check:fix": "biome check --write . && pnpm run lint --fix"
  }
}

biome checkでフォーマットとインポートソート(action)を実行し、続けてpnpm run lintでESLintの他のルールチェックを実行します。これにより、すべてのチェックがpnpm run checkで一括実行できます。将来的にESLintもBiomeに移行する場合は、&& pnpm run lintの部分を削除すればよいでしょう。

GitHub Actionsの変更

上の章で作成したpnpm run checkコマンドをCIで実行するように設定を変更しました。

- name: Run checks
  run: pnpm run check

移行後の感想

パフォーマンス

体感で少しだけ速くなったと感じています。

安定性

今のところ何の問題もなく使えています。既存のPrettier設定とほぼ同等のフォーマット結果が得られており、インポートソートも期待通りに動作しています。

まとめ

PrettierとESLintのインポートソートからBiomeへの移行は成功し、期待通りの効果が得られました。設定ファイルもシンプルになり、開発体験が向上しました。

今後はtypescript-eslint相当のルールが充実し、プラグインも豊富になってきたらESLintも段階的にBiomeに移行するかもしれません。同じように移行を検討している方は、この記事の設定スニペットを参考にしていただければと思います。

参考:biome.jsoncの全体

最後に、今回作成したbiome.jsoncの全体を掲載します:

{
  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true,
    "defaultBranch": "main"
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space"
  },
  "linter": {
    "enabled": false
  },
  "javascript": {
    "formatter": {
      "semicolons": "asNeeded",
      "quoteStyle": "double"
    }
  },
  "html": {
    "formatter": {
      "selfCloseVoidElements": "always"
    }
  },
  "css": {
    "parser": {
      "tailwindDirectives": true
    }
  },
  "assist": {
    "enabled": true,
    "actions": {
      "source": {
        "organizeImports": {
          "level": "on",
          "options": {
            "groups": [
              // 標準ライブラリ・DenoのURLインポート('node:path' など)
              [":NODE:", ":BUN:", ":URL:"],
              ":BLANK_LINE:",
              // 外部のパッケージ('react' など)
              [":PACKAGE:", ":PACKAGE_WITH_PROTOCOL:", "!@repo/**"],
              ":BLANK_LINE:",
              // モノレポのパッケージ('@repo/ui' など)
              "@repo/**",
              ":BLANK_LINE:",
              // エイリアス('~/lib/firebase' など)
              ":ALIAS:",
              ":BLANK_LINE:",
              // パス('./components/nav-bar' など)
              ":PATH:"
            ]
          }
        }
      }
    }
  }
}

Discussion