bitbank techblog

Biome.js 新規導入ガイド

こんにちは!リテールチームの kei です。

JS/TS のエコシステムがかなり複雑であるというのはよく知られた事実で、実行環境の細かい差分や周辺ツール類の多さに苦労したことがある方も多いと思います。

とりわけリンターやフォーマッターの設定には常々問題を感じていたのですが、数年ほど前から Rome.js(Biome.js の旧名)を個人で使うようにしてみたところ、この種の問題をほぼ忘れられるくらいに素晴らしいものだったのでとても感動しました。

最近社内で複数リポジトリのモノレポ化というのをやっており、その過程で「ツール類も整備しよう!」ということで、この Biome.js を導入することを決めました。モノレポの対象となるリポジトリはレガシーなものも多かったため、特にリンター周りの設定は慎重に検討する必要があり、細かい調査と実験を行ったことでより理解が深まりました。

本記事ではそれらを材料にしつつ、Biome.js(以下 Biome)を新規導入するためのガイドになれるような内容を目指します!

概要

biomejs.png

https://biomejs.dev/ja/

まさに「ひとつのツールで全部うまくいくようにしよう」という趣旨でスタートしたプロジェクトで、少し前までは Rome.js という名前でした。名前を変えざるを得なかった理由を含む、このツールがたどってきた歴史については公式ブログに説明があるのでぜひどうぞ。OSS の難しさも考えさせられます。

Biomeは Rust 製で実行が速く、Prettier の根底にある「カスタマイズ性は低くてよい」という思想も受け継いでいます。フロントエンドで編集する必要のあるファイル群(html, css, jsx/tsx, vue, svelte, etc...)のサポートはまだ薄いですが、バックエンドリポジトリのような純粋な JS/TS プロジェクトでは存分に力を発揮します。もちろん現バージョンは Production Ready です。

これまでの環境設定では、ESLint と Prettier、そしてこれらを併用するための多種のプラグインなどなど、結局 package.json のうち何行も占有することになってしまいましたが、Biome を使う場合は本当にたったの 1 行で済みます。これこそまさに「ツールチェーンをどうすべきか」という問題を見事に解決している証拠といえるでしょう。

Biome を導入すべきか?

基本的に手放しでおすすめしたいツールではありますが、それだけでは根拠に欠けますし、私は昔からこのプロジェクトを支援していて少し公平性が損なわれている自覚もあるので、改めて Biome を導入すべきかを客観的にまとめてみたいと思います。

  • 新規プロジェクトで導入する場合、基本的に ESLint/Prettier の環境を再現することはかなり簡単なので特に問題はないはず(後述)
  • プラグインという仕組みはまだ存在しないため、リポジトリ固有で独自のフォーマット機構などを組んでいた場合は別で検討する必要があります
  • 既存リポジトリで ESLint/Prettier からの乗り換えを考える場合、特にリントルールの適用についてはテストや実験をおすすめします
    • リポジトリの役割や"年齢"によって慎重度は変わりますが、レガシーリポジトリでユニットテストも揃っていないような状況だとかなり厳しいかもしれません(これも実例を後述します)
  • 「本当は開発中に細かくリント/フォーマットをかけたいが、遅すぎてコミット前にまとめて手動でやるしかない」などの実害を伴う課題がある場合、積極的に導入を検討する価値があります
  • 別に Biome を導入しなくてもよさそうなケース:
    • 社内のリポジトリ数が多くない、今後の増加も見込まれない
    • リント/フォーマットのルール設定や導入手順が整備されていて、環境設定に関する課題がほぼない

結局のところただの開発用ツールでしかないので、どのような状況でも誰か一人が少し気合いを入れればたいていの場合は(その後の継続的な使用含めて)導入可能だと思います。個人的な意見にはなりますが、苦労の程度差はあれど、色々すっきりするという精神的な側面も含め導入はぜひおすすめしたいところです。

マイグレーションについてですが、実はちょうど最近になって公式の「ESLint/Prettier からのマイグレーション機能」が日本語の説明記事とともに登場したところです。

https://biomejs.dev/ja/guides/migrate-eslint-prettier/

もともと Biome は Prettier との高い互換性を持っていることを明示していましたし、移行が比較的簡単でありそうなのも頷けます。ESLint についても同様で、Biome 内にあるほとんどのリントルールは ESLint からインスパイアされたものです。つまり多くの開発者が持つ従来の「JS/TS に対するリントやフォーマットのイメージ」というものは Biome 移行後もほとんど損なわれることがなく、コードの意味でも意識の意味でもマイグレーションは難しくないと思われます。

本記事後半で既存リポジトリに導入した際の記録を書いていますが、一旦この次の章はインストールからスタートします。

インストール

使い始めるのは非常に簡単です。

  1. リポジトリの devDependencies に @biomejs/biome をインストールする
  2. (VS Code, IntelliJ を使っている場合)公式の拡張機能をインストールする
  3. Biome のコンフィグとエディタのコンフィグをセットする

ツール本体のインストールは、他の同種のものがそうであるように、基本的にはリポジトリの devDependencies に含めておくことが推奨されています。バージョンについても範囲演算子を使用せず固定するのが望ましいとあるので素直に従っておきましょう。

次の手順はエディタとの統合で、VS Code を使っている場合は拡張機能をひとつインストールするだけです。

拡張機能が増えることに抵抗があるのは非常に共感できます。が、ESLint と Prettier も同様の条件である(しかも 2 つ!)ことを考えると、そこまで一大事ではありません。また、拡張機能は使わずコマンドベースでリントやフォーマットを実施している方がたまに「コードを書いているときにいちいち実行が走ると遅くて実用に向かない」と言っているのを聞きますが、これは Biome においては気にする必要のない問題です。何しろ速いので!

というわけで拡張機能のインストールを行い、その後下記のようにリポジトリに設定を含めておくのがおすすめです(VS Code の場合):

root/
└── .vscode/
    ├── extensions.json
    └── settings.json

extensions.json

{
    "recommendations": [
        "biomejs.biome",
    ],
}

settings.json

{
    // 開発者のエディタで独自に動く可能性のある ESLint と Prettire を抑制する
    "eslint.enable": false,
    "prettier.enable": false,

    // コマンドをいちいち実行しなくても常に lint/format が素早く実行される状態にする
    "[javascript]": {
        "editor.tabSize": 2,
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "biomejs.biome",
    },
    "[typescript]": {
        "editor.tabSize": 2,
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "biomejs.biome",
    },
    "[json]": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "biomejs.biome",
    },
    "[jsonc]": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "biomejs.biome",
    },
    "editor.codeActionsOnSave": {
        "source.organizeImports.biome": "explicit",
    },
}

これにより、新しくそのリポジトリに参加する開発者が現れたときに下記のようなメリットがあります:

  • 拡張機能が存在していることがわかり、かつその使用が推奨されている
  • エディタの設定については各開発者は何もしなくていい

ぜひ実施しておきましょう!

Biome 本体のコンフィグについては次の章で個別に取り扱います。

コンフィグ類

Biome の設定は biome.json というファイルによって機能します。よくある設定ファイルの挙動と同じように、コマンドが実行された場所からプロジェクトルートまで探索が遡っていき、見つかり次第最も近いファイルが反映されます。

biome.json は存在しなくても問題ありません。すべてがデフォルト設定のままでいいならこのファイルは不要です。また、拡張子は .jsonc でも有効なので基本的にはこちらを使用することをおすすめします。

他の類似ツールと少し違う点として、拡張機能側に直接コンフィグをセットする機能がないことが挙げられます。つまり Biome 自体の制御は必ずリポジトリ内にある biome.json を使うことになるということですね。これは混乱を避けられますし、シンプルで私も非常に好きな仕組みです。

コンフィグの構成

Biome の機能は大きく 3 つにわけられます:

  • lint
  • format
  • organizeImports(インポート文の整理)

設定ファイルも大まかにこの構造に沿っているということをあらかじめ知っておくと理解しやすいかと思います(lint だけは内容が多岐にわたるためさらにもう一段階カテゴリで区分けされています)。

すべてのルールについての詳細は下記リファレンスをご覧ください。

基本的にはまっさらなデフォルト状態でスタートし、自分のプロジェクトが依存するフレームワークの性質上どうしてもルール制御が必要なものなどのみを少しずつ足していくのがいいと思います。特に明確な根拠がないままスタイルに関する設定などを追加するのは激しくおすすめしません。 おそらく三大議論(「' なのか " なのか」「セミコロンのありなし」「インデントサイズ」)についてくらいは社内でおおよその共通見解があると思うので、最初にセットするのはそれくらいにしておいたほうがいいでしょう。オプションの追加によって新たな議論が発生することこそ、Biome はおろか Prettier が古来から最も防ぎたいと考えていることなので。

設定の参考例

設定の実例もご紹介してみます!
比較的ミニマムな構成かと思いますので、これをベースにカスタマイズしていただくのもよいかもしれません。

(説明のためだけにインラインでコメントを入れています)

{
    // JSON スキーマの参照
    "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
    "formatter": {
        // Biome のインデントはデフォルトで tab になっています
        "indentStyle": "space",
    },
    "linter": {
        "rules": {
            // リントルールは数を絞った上でお好みで(自分は過剰すぎるものだけ無効化しています)
            "style": {
                "noNonNullAssertion": "off",
                "useTemplate": "off",
                "noUselessElse": "off",
                "useImportType": "off"
            },
            "complexity": {
                "noForEach": "off"
            }
        }
    },
    "organizeImports": {
        // インポートソートを有効にするかどうか
        "enabled": true
    },
    "javascript": {
        "formatter": {
            // セミコロン、シングルクォート/ダブルクォート、末尾カンマ、行の文字数などはお好みで(前述した注意点をよく理解したうえで!)
            "semicolons": "asNeeded"
        },
        "parser": {
            // フレームワークが用意するデコレータを使うために必要
            "unsafeParameterDecoratorsEnabled": true
        }
    }
}

開発中および CI での使用方法

前述したエディタ統合を行っている場合、普段の開発での使用法というものは特にありません。コードを保存するたびに lint, format, organizeImports が常に働くのでわざわざどこかでコマンドを実行したり Git フック をセットしたりする必要もないです。

最低限 package.json にセットしておくべきコマンドがあるとすれば下記のような感じになるでしょうか。

{
  "scripts": {
    "lint": "biome lint --write --diagnostic-level=error src/*",
    "format": "biome check --write --linter-enabled=false src/*",
    "lint-and-format": "biome check --write --diagnostic-level=error src/*",
    "lint-and-format:ci": "biome ci --verbose --diagnostic-level=error src/*"
  }
}

Biome は初見だとおそらく CLI コマンドだけ少しわかりにくくて苦労するかもしれません。ざっくり説明すると以下のようになります:

  • リントだけ実行したい:biome lint
  • フォーマットだけ実行したい:biome format
  • リント/フォーマットどちらも実行したい:biome check
    • 例えばインポート整理だけ実行したいような場合は biome check からリントとフォーマットを引き算するような形になる
  • 書き換え(いわゆる fix や write といわれるようなもの)をするかどうかなどは上記各コマンド内でオプションとして付加する

そのうえで、CI では biome ci という専用のコマンドを使うことが想定されています。できることはほとんど biome check と同じなのですが、読み取り専用で絶対に変更が行われないであるとか、GitHub の PR 上で使うと見やすい実行結果が表示されるであるとかの些細な違いがあります(後者は公式サイトにも書かれていなくて、ほとんど知られていないと思います…)。いずれのコマンドも、検査に引っかかれば exit code: 1 で終了します。

VCS で GitHub を利用している場合、公式の GitHub Actions もあるのであわせてどうぞ!

なお、コードの中で局所的に Biome を無効化したい場合は以下のように書くことができます。

// biome-ignore lint:
let unknownVar1: any

// biome-ignore lint/suspicious:
let unknownVar2: any

// biome-ignore lint/suspicious/noExplicitAny:
let unknownVar3: any

class MyClass {
    // biome-ignore format:
    constructor(
        private initialized: boolean,
    ) {}
}

次の行で無効化するスコープ(設定ファイルの JSON 構造がそのままスコープの区切りになっていて、Biome では「Diagnostic Category」とよばれています)を指定するイメージですね。当然ですがスコープは常に絞った上で無効化するのがいいと思います。

既存リポジトリに途中から導入する場合

新規プロジェクトに新しく導入する場合はここまでの手順で既にスタートできる状態になっているはずですが、既存のプロジェクトでは特にリントにおいて注意が必要です。

まず Biome のリントの構造を把握し、それをどのような順番で適用していくべきかを理解していきましょう。

Biome のリントは

  • Safe fixes
  • Unsafe fixes

という 2 つの種類があり、それぞれ

  • コードの動作が変わることはないので何も考えず適用して OK なもの
  • コードの動作が変わることもあり得るので適用に注意すべきもの

という区分けになっています。

biome lint --write などのコマンドを実行したとき、デフォルトでは Safe fixes だけが適用されるようになっており、安全は保証されています。Unsafe fixes も適用の対象にしたい場合は --unsafe オプションを追加してください。

当然ですが、修正対象があるひとつの選択肢に定まらない場合は --write などのオプションをつけても修正は行われずエラーの報告がされ続けるだけなので注意してください。基本的にはそれらファイルをひとつずつ開いて手動で編集するのが作業のメインになっていきます(コマンドで修正が反映されるものはあとで差分を見直すだけでよい)。

ここで注意しなければならないのは、「--unsafe をつけていなくても Unsafe fixes の内容は検査対象にはなる」という事実です。つまり --unsafe というオプションは「--write の適用先に Unsafe fixes を含めるかどうかの指定にすぎない」ことになります。これは一見するとややこしく思えますが、Unsafe とはいっても基本的にはすべて対応したほうがいいルールばかりが並んでおり(例はこの次の章でいくつか挙げています)、これらの対応をリポジトリとして正式に決めるというフェーズは必ず経るべきだと考えられます。

検討した結果、コードの修正はせず除外をしたいということになった場合、

  • コメントで無効化する
  • ファイル単位で無効化する
  • 設定ファイルでルールごとに Severity を調整する

などのような対応パターンがあるでしょう。上から順により局所度合いが高い対応になるので、該当エラーの報告数やメンバーの実装方針などを鑑みて決定していくとよいです。

幸いなことに、今回対象となったレガシーリポジトリたちもユニットテストはしっかり用意されているものが大半であったため、上記でいうところのなるべく根本的なアプローチ(下側)から実施してみる→テストを動かしてみるというサイクルが機能しました。最後に、それぞれのルールに対してどういう根拠でどの対応を取ったのかをドキュメントなどに残しておけると完璧です!

Unsafe fixes の中でも特に適用を気をつけるべきものの例

実際に既存リポジトリにリントルールを適用した中でいくつかの問題に遭遇したので、最後にそれらのうち数個をご紹介して終わりにしたいと思います。

noDelete
- delete obj.property
+ obj.property = undefined

オブジェクトのプロパティを削除したい場合、delete 演算子がよく使われていると思います。
このルールの説明を読む限り、基本的にはこれを使うメリットはほとんどないように思えますが、だからといって既存コードにそのまま適用していいかは別問題のようです。

「もともと予期せぬ動作をすることがある」であるとか「配列の要素に使っても配列の長さは変わらない」であるとかかなり危険そうなことが書いてあるので、既存コードでいままでうまく動いていたものがある場合はむやみに変更しないほうがいいと思われます。今回の弊社の例でもこの delete 演算子は基本的にノータッチとしました。

noDoubleEquals
- expect(recieved == expected).toBe(true)
+ expect(recieved === expected).toBe(true)

これはどういう変化があるのか多くの開発者が理解していると思われるため、実害がないのであれば問答無用で適用したいルールですが、これによっていたるところでコードの結果が変わるであろうこともまた明らかです。

この緩い比較は弊社のテストコードで多く見られたので、一度すべての変更を適用してみたあとテストが通らないものだけひとつひとつ個別でチェックし、部分的に除外するのかコード自体を直すのかの検討をしていきました。アプリケーションコード側は安全を優先し基本的に変更なしとしましたが、これはそのリポジトリを今後拡張し続ける予定があるのか、もしくはリプレイスが意識されているのか、などによって決めていくといいと思います。

ちなみにイディオムとしてよく使う == null の存在は許容されていて、Biome でもこの noDoubleEquals ルールからは除外されています。

noExplicitAny
- let unknownVar: any
+ let unknownVar: MyCustomType

よく話題に上がる any の使用についてですが、今回は「warn で警告は出すが修正対象からは外す」という方針にしました。

理由としては、やはり既に存在する多くの any の対応方法を開発者ひとりがすぐに決めるというのは現実的に無理そうであるということと、他のルールをなるべく無効化しないようにしている関係上、この any がよい避難先になるというケースが多々あるからです。毎度 // biome-ignore lint: をするよりはマシな方針なのだろうというのはすぐに周りから理解してもらえると思います。

また、別の重要なポイントとして、オブジェクト(の特に「シェイプ」といわれるもの)であることを表したいがために {}object などの型が記載されていることがかなりよくありますが、これは今回すべて禁止とし修正対象としました(ルール的にはもちろんデフォルトでエラー扱いです)。理由についてはルールの説明ページなどを読めばわかりますが、例えば {} は簡単にいえば「ほぼすべての JS 要素はオブジェクトであり、この記法は any とほとんど変わらない」などのような感じです。

前述したように any の使用は警告は出るものの今回は許容することにしたため、{} などを見かけた場合は一旦 any に書き換えることで作業を進めることができました。つまり「避難先」の一例ですね。とはいえ警告自体は出続けるため、これから新規でコードを書く開発者には「なるべくやめてくれよ」という圧力をかけることもできます。

ちなみに、{ someKey: someValue } という何らかのオブジェクト形状であることのみを示したい場合、Record<string, any> などが適切な代替手段であると思われます。

おわりに

ESLint/Prettier 系の記事や State of JS などでも少しずつ Biome の名前を見かけることが増えてきました。ひっそりとユーザー数が増え始めてきており、かつ使ったことある開発者は皆ポジティブな感情を示しているのが伺えます。

これから人気プロジェクトになっていくであろうと個人的には確信しているのですが、まだまだ認知度が低く、本記事のような情報がその普及に少しでも貢献できれば嬉しいです。

ぜひみなさんも一度お試しください!
それではお読みいただきありがとうございました。

Author image
About kei
expand_less