Biome v2がリリースされ、ついに我々が待ち望んだ本当のimport
の自動整列が使えるようになりました!!!!やったぜ!
Biome最大の不満の一つがこのimport
周りで、お世辞にも快適なものとは言えませんでした。
例えば
- 最上位はnpmパッケージ群
- 空行を挟んで自作のモジュール群、
/lib
ディレクトリを優先的に上位に表示 - 空行を挟んで型定義ファイル
という設定はESLintでもよく行われる自動修正です。
import { foo } from './foo'
import { lib1 } from './lib/lib1';
import { lib2 } from './lib/lib2';
import type { type1 } from './types/type1';
import { useState } from 'react';
これがv1までだと
import { useState } from 'react';
import { foo } from './foo';
import { lib1 } from './lib/lib1';
import { lib2 } from './lib/lib2';
import type { type1 } from './types/type1';
こんな感じに自動修正するのが限界でした。
つまり空行を任意の場所で自動挿入してグルーピングするということができず、基本的にアルファベット順にソートされてしまうので/lib
ディレクトリ配下を優先的に上位にしたいというソートもできなかったわけです。
これがv2ならこうなります。
import { useState } from 'react';
import { lib1 } from './lib/lib1';
import { lib2 } from './lib/lib2';
import { foo } from './foo';
import type { type1 } from './types/type1';
自動でnpmパッケージと自作モジュールの間に空行挿入、foo
よりも/lib
が優先的に上位表示、import type
の型定義ファイルとの間にも空行挿入、これこそ俺たちが求めていたものです!!!!!!!!!
ちなみにv1までは手動で空行を挿入することでグルーピング自体は可能でした。
が、どうしても手動挿入が必要だったのに加えて、まだimport
を書いていない状態で関数名等を書いてIDEが自動補完した場合基本的にimport
の最後に追加されます。
import { foo } from './foo';
import type { fooType } from './types/fooType';
import { bar } from './bar'; //新たにIDE補完で自動追加
ここで自動修正が走ると空行によってfooType
とグルーピングされてしまいます。
// 本当はfooとグルーピングされてほしいのに、fooとfooTypeの間に空行があるため
// barとfooTypeが一つのグループと見なされて修正されてしまう
import { foo } from './foo';
import { bar } from './bar';
import type { fooType } from './types/fooType';
なのでいちいちfoo
とbar
の空行を削除しbar
とfooType
の間に空行を足さなければならないという極めて不便な仕様でした。
Biome v2のImport設定
BiomeではorganizeImports
という設定項目でimport
の設定を行います。
organizeImports
は今まで独立した設定項目でした。
{
"organizeImports": {
"enabled": true
}
}
これが今後はassist
ブロックの配下に変更されます。
{
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
// ここに設定を書いていく
}
}
}
}
}
}
options
内に色々設定を書いていくことになります。
ちなみに単純に"organizeImports": "on"
と書くこともできますが、以下の順でソートする挙動になります。
- npmパッケージ群
- ユーザー定義モジュール
type-only import
が区別なく名前順で並び替えられるのはv1と同じ挙動ですが、なんと空行によるグルーピングが無くなってv1以下の性能になっています。
なおここでは詳細に触れませんが、assist
にはorganizeImports
以外にも項目があります。
キー | 内容 | 公式マニュアル |
---|---|---|
organizeImports |
import の自動ソート(今回の内容) |
organizeImports |
useSortedKeys |
オブジェクト/jsonのキーを自動ソート | useSortedKeys |
useSortedAttributes |
props を自動ソート |
useSortedAttributes |
useSortedProperties |
CSSのプロパティを自動ソート | useSortedProperties |
当然organizeImports
と併用可能です。
Biomeの設定ファイルはjson
とjsonc
が使用できますが、ここでは統一してjsonc
を採用しています。
一部json
ではエラーになる記述(コメント等)があるのでコピペの際は注意してください。
最終的な完成形
最終的に以下のような形で自動整形できるようにします。
// React関連パッケージを優先しnpmパッケージ群をグルーピング、その後空行
import { useState } from 'react';
import axios from 'axios';
// /libディレクトリのモジュールを優先しユーザー定義モジュールをグルーピング、その後空行
import { lib1 } from './lib/lib1';
import { constant } from './constant';
import { utils } from './utils';
// type-only importはまとめてグルーピング
import type { propsType } from '../types/props';
biome.jsoncの設定
上記を満たすbiome.jsonc
は以下の通りです。
{
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
// ===== react packages =====
// react関連パッケージを優先してimport
{ "type": false, "source": ["react*", "react/**"] },
// ===== npm packages =====
// react*,react/**以外のnpmパッケージを全てキャッチ
{ "type": false, "source": ":PACKAGE:" },
":BLANK_LINE:",
// ===== lib modules =====
// ./libディレクトリを優先してimport
{ "type": false, "source": "./lib/**" },
// ===== user modules =====
// ./libディレクトリ以外のユーザー定義モジュールをまとめてキャッチ
{ "type": false, "source": ":PATH:" },
":BLANK_LINE:",
// ===== other imports =====
// 上記に引っかからない全てのimportをここでキャッチ
{ "type": false },
":BLANK_LINE:",
// ===== import types =====
// 最後にtype-only importをキャッチ
{ "type": true }
]
}
}
}
}
}
}
VSCodeで自動修正させる
{
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit"
}
}
source.organizeImports.biome
をexplicit
にすることで、BiomeのorganizeImports
が走るようになります。
解説
groups
配列の中にマッチ条件を記述します。
マッチ条件には以下の種類があります。
マッチ条件 | 意味 | 種類 |
---|---|---|
定義済みキー |
:PACKAGE: や:PATH: 等、Biomeが定義済みのキー |
文字列 or 配列 |
globパターン |
* 、** 、!(否定) 、\(エスケープ用)
|
文字列 or 配列 |
オブジェクトマッチャー | { "type": boolean, "source": string|string[] } |
オブジェクト |
例えば"react"
のような指定は完全一致のglobパターンとなります。
定義済みのキー
途中に:PACKAGE:
や:PATH:
という特殊な文字列が出てきますが、これはBiomeが元々用意してくれている定義済みのキーです。
キー | 意味 |
---|---|
:ALIAS: |
# 、@/ 、~ 、$ 、% で始まるエイリアス |
:BUN: |
Bunの組み込みモジュール |
:NODE: |
Node.jsの組み込みモジュール |
:PACKAGE: |
npmパッケージ |
:PACKAGE_WITH_PROTOCOL: |
npm: やjsr: のようなプロトコル付きパッケージ |
:PATH: |
相対・絶対パス |
:URL: |
URLでimportするパッケージ |
:BLANK_LINE: |
空行 |
これらの定義済みのキーを使えばほとんどのimport
を自動整理することが可能です。
例えばnpmパッケージの後空行、その後自作モジュールをimport
したい場合は以下のように指定すればOK。
{
"options": {
"groups": [
// まずは:PACKAGE:を指定してnpmパッケージをimport
":PACKAGE:",
// :BLANK_LINE:を指定してnpmパッケージの後に必ず空行を挿入する
":BLANK_LINE:",
// 次に:PATH:を指定して自作モジュール群をimport
":PATH:",
]
}
}
これだけでかなり見やすいimport
の自動整理が実現します(ただし上記の場合、import type(type-only import)
は区別されません)。
:ALIAS:
は非常に便利で、よくあるコンポーネントをまとめた@/components
やモノレポでshared
ディレクトリを切った時の@/shared
などに使えます。
が、:ALIAS:
がカバーするのはあくまで以下のようなpathの場合です。
#
@/
~
$
%
見て分かる通り@/
だけアットマーク+スラッシュで一つの塊で、@
はヒットしません。
{
"paths": {
"@/shared/*": ["../shared/*"]
}
}
こんな感じでエイリアスを@/
としていれば:ALIAS:
が使用可能ですが、以下のように/
をつけ忘れると機能しません。
{
"paths": {
"@shared/*": ["../shared/*"]
}
}
@
単体だとnpmパッケージの@scope
なのか判別ができないからこういう形になっているんだと思います。
僕は最初この仕様に気づかず30分ぐらい格闘しました。
globパターン
残念ながら正規表現は使用できませんが、代わりにglobパターンである程度柔軟にimport
の並び替えが可能です。
先述した通り"react"
、"hono"
のように単純にパッケージ名だけを指定して完全一致のglobパターンを使う機会は多いと思います。
また、reactから始まるパッケージを一括指定したい場合は"react*"
のように指定します。(react
、react-dom
、react-router
など)
ディレクトリ階層を超えて指定したい場合は"react/**"
です(react/server
、react/jsx-runtime
など)。
否定!
は例えばReact Nativeプロジェクト等頻繁にreact
というワードが出てくるプロジェクトで便利です。
最後に残念ながら今のところ使用できませんがBiomeで予約されているため使用できないメタ文字として?
、[
、]
、{
、 }
があります。
あまりないとは思いますが、これらのメタ文字がファイル名等に含まれている場合バックスラッシュ\
を使ってエスケープが必要です。
これらのglobパターンは配列マッチャーを使って複数指定可能です。
例えばreact
で始まるパッケージを優先的に上位表示したいが、react-native
だけはその後にグループ化したい場合は以下のように記述します。
{
"options": {
"groups": [
// まずは配列マッチャーでreact、もしくはreact/かつreact-native以外のパッケージをimport
["react*", "react/**", "!react-native*"],
// 次にreact-nativeで始まるパッケージをimport
["react-native*", "react-native/**"]
]
}
}
オブジェクトマッチャー
オブジェクトマッチャーは先述した定義済みキーやglobパターンをimport
かimport type
(type-only import
)も組み合わせてオブジェクト形式で指定する方法です。
キーはtype
とsource
の2つです。
type
もsource
も省略可能です、つまり先程の定義済みキーやglobパターンを使用した指定は「type
を省略した同じ意味の文字列マッチャー」です。
ちなみにsource
だけの場合は先程のように定義済みキーやglobパターンを文字列マッチャーとして指定可能ですが、type
だけの場合は必ずオブジェクト形式である必要があります。
{
"options": {
"groups": [
// typeとsourceを両方指定する場合
{ "type": true, "source": "react" },
// sourceだけを指定する場合
{ "source": ":PACKAGE:" },
// これでもOK
":PACKAGE:",
// typeだけを指定する場合
{ "type": false },
// これはダメ
false
]
}
}
type
import
が通常のimport
か、import type
かを指定します。
現状TypeScriptのimport
は以下のimport
もしくはtype-only import
のいずれかです。
// よく使うimport
import { OpenAPIHono } from '@hono/zod-openapi';
// 型ファイルをimportで書く場合
import { type Handler } from 'hono';
// 型ファイルをimport type(type-only import)で書く場合
import type { Handler } from 'hono';
僕は型のimportはimport type
で書いた方が視認性が高いので全てimport type
で統一しています。
{
"options": {
"groups": [
// 通常のimportを指定したい場合
{ "type": false },
// import type(type-only import)を指定したい場合
{ "type": true }
]
}
}
typeを省略した場合import type
→import
の順番になり、例えばhono
のように関数と型定義を両方提供しているライブラリを使用して以下のように指定します。
{
"options": {
"groups": [
["hono", "hono/*", "@hono/**"],
":BLANK_LINE:",
":PACKAGE:"
]
}
}
この場合、以下のように並び替えられます。
// import typeが先にimportされる
import type { Handler } from 'hono';
import { Hono } from 'hono';
// その他のnpmパッケージは:PACKAGE:に分類
import { z } from 'zod';
先に提示した完成形のように、「import type
はとにかく一番最後にまとめてグルーピングすればOK」の場合はoptions
の一番最後に以下のように書いておけば良いです。
{
"options": {
"groups": [
// 色々な設定を書いた一番最後にimport typesをまとめてグルーピング
// ===== import types =====
{ "type": true }
]
}
}
もちろん"type": true
を指定した場合もsource
を組み合わせて自由に並び替え可能です。
例えばtypes/foo
とtypes/bar
があった場合通常アルファベット順でbar
が先に並びますが以下のように指定すればfoo
が先に並びます。
{
"options": {
"groups": [
// この場合barが最上位、ついでnpmパッケージ、自作型定義と続く
{ "type": true, "source": "./types/bar" },
{ "type": true }
]
}
}
単に"type": true
とだけ書く場合全てのimport type
がここに吸収されるので、「npmパッケージは最上位、その次にbar
、次にそれ以外」の場合だともう少し記述が必要になります。
{
"options": {
"groups": [
// この場合はnpmパッケージが最上位、次いでbar、その他の自作型定義の順
// npmパッケージとbarの間に空行を挿入したい場合は先程と同じく:BLANK_LINE:を指定する
{ "type": true, "source": ":PACKAGE:" },
{ "type": true, "source": "./types/bar" },
{ "type": true }
]
}
}
source
sourceは具体的にどのパターンを指すかを定義します。
先述した定義済みキー、globパターン、配列マッチャーがそのまま利用できます。
{
"options": {
"groups": [
// 配列マッチャーをsourceに指定する例
// この場合は「react-nativeから始まるものを除外した以外のreactから始まる」という意味
{ "type": false, "source": ["react*", "!react-native*"] }
]
}
}
type
を省略した場合、オブジェクトマッチャーではなく文字列マッチャーとして指定しても良いです。
{
"options": {
"groups": [
// この2つは同じ意味
{ "source": ":PACKAGE:" },
":PACKAGE:"
]
}
}
とりあえずのテンプレート
ここまでつらつら書いてきましたが、多くの方にとって
-
type-only import
を除いたnpmパッケージ+空行 -
type-only import
を除いたユーザー定義モジュール+空行 type-only import
で並び替えされれば十分だと思います。
その場合は以下のように指定しておいてください。
{
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
// ===== npm packages =====
{ "type": false, "source": ":PACKAGE:" },
":BLANK_LINE:",
// ===== user modules =====
{ "type": false, "source": ":PATH:" },
":BLANK_LINE:",
// ===== import types =====
{ "type": true }
]
}
}
}
}
}
}
ここに「Nextjs
プロジェクトなので関連パッケージが上位だと嬉しいな」とか、「モノレポプロジェクトでshared
ディレクトリに定義した型定義は分かりやすくグルーピングしたいな」みたいな要望が出てきた時に追加すればいいかなあと思います。
BiomeがESLintを本当に置き換える日も近いかも
今回のimport
の改善で個人的にはBiomeの最大の不満がようやく解消された感があります。
とはいえまだTailwindのclass自動ソートはβ版、細かい調整はエコシステムの充実度から言ってもまだまだESLintに軍配が上がります。
RubyにおけるRails的な意味合いで「Biomeさえ入れとけば後はそのルールに従ってれば良い」というならともかく、ここからはBiomeそのものももちろんコミュニティがいかに成長できるかが鍵かなあという感想です。
僕のような小〜中規模プロジェクトを作る個人開発者は十分Biomeを採用しても良いかなと思っています。
現に現在新規プロジェクトでv2を採用していますが全く不満なく開発を進められています。
逆に大規模プロジェクト、特にプラグインを入れまくってたり独自ルールを書きまくってるプロジェクトはまだまだ厳しいでしょう。
ですがフラットコンフィグへの移行やそもそもESLint自体の設定が複雑すぎる点などで脱ESLintが求められているのも事実で、プラグインがESLintのアップデートに追いつかずその影響でESLint自体をアップデートできないなんて声もちょくちょく目にします。
ここでは触れていませんが特にv2になりモノレポサポート、プラグインシステム導入などその下地は出来つつあるので今後もBiomeの動きはぜひ注目しておきたいところです。