RemixでESLint&PrettierからBiomeに乗り換える
はじめに
Typescript のプロジェクトを始める時、リンターに ESLint、フォーマッタに Prettier を選ぶことが多いのではないでしょうか?
Remix のプロジェクトは、始めた時点で ESLint が含まれています。なので、Prettier をインストールして使う場合が多いかと思います。
今回は ESLint&Prettier が設定されている Remix プロジェクトから Biome に乗り換える方法を書いていきます。
Remix のプロジェクトを作成する
公式に書かれている方法でプロジェクトを作成します。(こちら)
npx create-remix@latest
Prettier をインストールする
これまた公式に書かれているとおりの方法でインストールします。(こちら)
npm install --save-dev --save-exact prettier
.prettierrc
を作成します。
node --eval "fs.writeFileSync('.prettierrc','{}\n')"
.prettierignore
を作成します。
node --eval "fs.writeFileSync('.prettierignore','# Ignore artifacts:\nbuild\ncoverage\n')"
フォーマットされるファイルがあるか、確認してみます。4 ファイルフォーマットできるようです。
npx prettier . --check
Checking formatting...
[warn] .devcontainer/devcontainer.json
[warn] .github/dependabot.yml
[warn] app/entry.client.tsx
[warn] app/entry.server.tsx
[warn] Code style issues found in 4 files. Run Prettier with --write to fix.
ファイルをフォーマットしてみます。先ほどの 4 ファイルがフォーマットされました。
npx prettier . --write
.devcontainer/devcontainer.json 24ms
.github/dependabot.yml 12ms
app/entry.client.tsx 34ms
app/entry.server.tsx 33ms
ここでentry.client.tsx
の差分を見てみると、</StrictMode>
の後ろに,
がついています。
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
- </StrictMode>
+ </StrictMode>,
);
個人的に嫌なので、設定を変更します。
{
"trailingComma": "es5"
}
もう一度npx prettier . --write
を実行すると、app/entry.client.tsx
とapp/entry.server.tsx
の末尾の,
が削除されます。
ついでに他の設定も加えておきます。特に意味はないです。
{
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}
ESLint と Prettier のルールが競合しないようにeslint-config-prettier
をインストールします。詳しい設定方法はこちらです。
npm install --save-dev eslint-config-prettier
.eslintrc.cjs
を修正します。
// Base config
- extends: ["eslint:recommended"],
+ extends: ["eslint:recommended", "prettier"],
ここまででようやく Prettier と関連パッケージのインストール、諸々の設定が完了です。
Biome をインストールする
公式に書かれているとおりの方法でインストールします。(こちら)
npm install --save-dev --save-exact @biomejs/biome
Biome の設定ファイルであるbiome.json
を作成します。
npx @biomejs/biome init
lint と format を確認してみます。すると結構差分が発生します。
-
lint と format 両方実行
npx @biomejs/biome check
-
lint のみ実行
npx @biomejs/biome lint
-
format のみ実行
npx @biomejs/biome format
biome に書き換えてもらいます。
-
lint と format 両方実行
npx @biomejs/biome check --write
-
lint のみ実行
npx @biomejs/biome lint --write
-
format のみ実行
npx @biomejs/biome format --write
lint でエラーが出ます。関数の引数であるresponseStatusCode
に 500 を割り当てようとしていて怒られているようです。
一旦無視して先に進めます。
/workspaces/remix-biome/app/entry.server.tsx:77:11 lint/style/noParameterAssign ━━━━━━━━━━━━━━━━━━━━
✖ Reassigning a function parameter is confusing.
75 │ },
76 │ onError(error: unknown) {
> 77 │ responseStatusCode = 500;
│ ^^^^^^^^^^^^^^^^^^
78 │ // Log streaming rendering errors from inside the shell. Don't log
79 │ // errors encountered during initial shell rendering since they'll
ℹ The parameter is declared here:
42 │ function handleBotRequest(
43 │ request: Request,
> 44 │ responseStatusCode: number,
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^
45 │ responseHeaders: Headers,
46 │ remixContext: EntryContext
ℹ Use a local variable instead.
/workspaces/remix-biome/app/entry.server.tsx:127:11 lint/style/noParameterAssign ━━━━━━━━━━━━━━━━━━━
✖ Reassigning a function parameter is confusing.
125 │ },
126 │ onError(error: unknown) {
> 127 │ responseStatusCode = 500;
│ ^^^^^^^^^^^^^^^^^^
128 │ // Log streaming rendering errors from inside the shell. Don't log
129 │ // errors encountered during initial shell rendering since they'll
ℹ The parameter is declared here:
92 │ function handleBrowserRequest(
93 │ request: Request,
> 94 │ responseStatusCode: number,
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^
95 │ responseHeaders: Headers,
96 │ remixContext: EntryContext
ℹ Use a local variable instead.
Checked 12 files in 4ms. No fixes applied.
Found 2 errors.
lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Some errors were emitted while running checks.
ESLint からの移行
こちらに書かれている手順に従って、移行を進めます。
まずは ESLint から移行します。--include-inspired
をつけることで、eslint からヒントを得てルールを追加します。
npx @biomejs/biome migrate eslint --write --include-inspired
ターミナルの最後の 1 文を読むとマイグレーションは成功しているようですが、一部失敗しているようです。
./.eslintrc.cjs:1:173 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Negated patterns are not supported.
> 1 │ {"root":true,"parserOptions":{"ecmaVersion":"latest","sourceType":"module","ecmaFeatures":{"jsx":true}},"env":{"browser":true,"commonjs":true,"es6":true},"ignorePatterns":["!**/.server","!**/.client"],"extends":["eslint:recommended","prettier"],"overrides":[{"files":["**/*.{js,jsx,ts,tsx}"],"plugins":["react","jsx-a11y"],"extends":["plugin:react/recommended","plugin:react/jsx-runtime","plugin:react-hooks/recommended","plugin:jsx-a11y/recommended"],"settings":{"react":{"version":"detect"},"formComponents":["Form"],"linkComponents":[{"name":"Link","linkAttribute":"to"},{"name":"NavLink","linkAttribute":"to"}],"import/resolver":{"typescript":{}}}},{"files":["**/*.{ts,tsx}"],"plugins":["@typescript-eslint","import"],"parser":"@typescript-eslint/parser","settings":{"import/internal-regex":"^~/","import/resolver":{"node":{"extensions":[".ts",".tsx"]},"typescript":{"alwaysTryTypes":true}}},"extends":["plugin:@typescript-eslint/recommended","plugin:import/recommended","plugin:import/typescript"]},{"files":[".eslintrc.cjs"],"env":{"node":true}}]}
│ ^^^^^^^^^^^^^
2 │
./.eslintrc.cjs:1:187 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Negated patterns are not supported.
> 1 │ {"root":true,"parserOptions":{"ecmaVersion":"latest","sourceType":"module","ecmaFeatures":{"jsx":true}},"env":{"browser":true,"commonjs":true,"es6":true},"ignorePatterns":["!**/.server","!**/.client"],"extends":["eslint:recommended","prettier"],"overrides":[{"files":["**/*.{js,jsx,ts,tsx}"],"plugins":["react","jsx-a11y"],"extends":["plugin:react/recommended","plugin:react/jsx-runtime","plugin:react-hooks/recommended","plugin:jsx-a11y/recommended"],"settings":{"react":{"version":"detect"},"formComponents":["Form"],"linkComponents":[{"name":"Link","linkAttribute":"to"},{"name":"NavLink","linkAttribute":"to"}],"import/resolver":{"typescript":{}}}},{"files":["**/*.{ts,tsx}"],"plugins":["@typescript-eslint","import"],"parser":"@typescript-eslint/parser","settings":{"import/internal-regex":"^~/","import/resolver":{"node":{"extensions":[".ts",".tsx"]},"typescript":{"alwaysTryTypes":true}}},"extends":["plugin:@typescript-eslint/recommended","plugin:import/recommended","plugin:import/typescript"]},{"files":[".eslintrc.cjs"],"env":{"node":true}}]}
│ ^^^^^^^^^^^^^
2 │
./.eslintrc.cjs has been successfully migrated.
どうやら、./.eslintrc.cjs
のignorePatterns
で否定!
を使っているのが原因のようです。まるまるコメントアウトすると成功します。
// Base config
- ignorePatterns: ["!**/.server", "!**/.client"],
+ // ignorePatterns: ["!**/.server", "!**/.client"],
コメントアウトした部分はマイグレーションされません。
なので、lint のマイグレーションを行った後、biome.json
に以下の設定を追加します。
{
"linter": {
"enabled": true,
+ "ignore": ["!./.server", "!./.client"]
},
}
Prettier からの移行
続いて、Prettier を移行します。
npx @biomejs/biome migrate prettier --write
ターミナル.prettierignore
と.prettierrc
からの移行が成功しているようです。
.prettierignore has been successfully migrated.
.prettierrc has been successfully migrated.
ただ、実際にフォーマットしてみると、ダブルクォーテーションがシングルクォーテーションになっていたり、最後にセミコロンがつかなかったりとルールの適用がうまくいっていない箇所が見受けられます。なので以下のように変更しました。
{
"javascript": {
- "semicolons": "asNeeded",
+ "semicolons": "always",
- "quoteStyle": "single",
+ "quoteStyle": "double",
},
}
また、Prettier を設定する際に.prettierrc
に書いたルールもbiome.json
に反映されています。
{
"formatter": {
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"javascript": {
"formatter": {
"trailingCommas": "es5"
}
},
}
ルールについては、適宜変更して下さい。
不要なファイルを削除する
.eslintrc.cjs
、.prettierignore
、.prettierrc
を削除します。
また、biome.json
の overrides に.eslintrc.cjs
の記述があるので削除します。
{
"overrides": [
- { "include": [".eslintrc.cjs"] }
]
}
package.json
に lint のスクリプトがあるので修正します。
ここでは format と lint を同時に行うスクリプトを追加しています。
お好みで format だけ、lint だけをそれぞれ行うスクリプトを書いて下さい。
{
"scripts": [
- "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
- "lint-fix": "eslint --fix",
+ "biome-check":"npx @biomejs/biome check",
+ "biome-check-write":"npx @biomejs/biome format --write",
]
}
パッケージも整理しましょう。以下で不要なパッケージを確認します。
npx depcheck
depcheck
をインストールするか聞かれるのでy
と答えます。すると使われていないパッケージが表示されます。
Need to install the following packages:
depcheck@1.4.7
Ok to proceed? (y) y
Unused devDependencies
* @typescript-eslint/eslint-plugin
* @typescript-eslint/parser
* eslint
* eslint-config-prettier
* eslint-import-resolver-typescript
* eslint-plugin-import
* eslint-plugin-jsx-a11y
* eslint-plugin-react
* eslint-plugin-react-hooks
* prettier
eslint や prettier のパッケージが不要なので削除します。
npm uninstall @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks prettier
おまけ
エディタの拡張機能で ESLint と Prettier を入れていたら削除します。
Biome の拡張機能もあるのでインストールしましょう。
また、個人的な設定として、以下を設定しています。デフォルトのフォーマッタを Biome にするのと、ファイル保存時にフォーマッタが動くようにしています。
-
Editor: Default Formatter
: Biome -
Format On Save
: true
こちらの記事で remix の簡単なアップグレード方法も書いているので、よかったら読んでいって下さい!
最後に
Remix のプロジェクトで ESLint&Prettier から Biome に乗り換える方法を書いてきました。
Biome は日本語ドキュメントがあるので移行手順の理解が簡単でした!
こちらに Biome に移行した Remix のリポジトリがありますので、よかったらご参照ください!
devcontainer で立ち上げるとnpm install
や拡張機能のインストール、各種設定もしてある状態で立ち上げられます!
この記事が参考になれば幸いです!🙇
Discussion