eslintをflat configで書き換える
本記事は「つながる勉強会 Advent Calendar 2022」の 21 日目の記事です。
前日も自分の記事で、「eslintのpluginsとextendsの違いを理解する」です。
前回の記事でeslintの設定について勘違いしやすい場所を解説したのでよかったら見てみてください。
この記事ではその内容を理解した上で、いよいよflat configに置き換えていきます。
書き換え前
書き換え前のeslintの設定を見た後に、実際にそれを置き換えていくというステップで進めていきます。
今回はこんな設定を考えてみます。
{
"extends": ["plugin:@typescript-eslint/recommended", "next/core-web-vitals", "prettier"],
"rules": {
"import/no-duplicates": "error",
"no-restricted-imports": [
"error",
{
"paths": ["next/link"],
}
]
},
"overrides": [
{
"files": ["src/components/Link.tsx"],
"rules": {
"no-restricted-imports": "off"
}
}
]
}
shareable confisとして読み込んでいるのは3つです。
- plugin:@typescript-eslint/recommended
- next/core-web-vitals
- prettier
ルールとして自分で設定しているのはeslint-config-import
のno-duplicates
とeslint標準のno-restricted-imports
のみです。
no-restricted-imports
ではNextのLinkのimportをsrc/components/Link.tsx
以外で禁止しています。
(Linkを拡張した独自コンポーネントを定義して、それしか使えなくするためのルール)
ちなみにplugins
にimport
を記述していませんが、それでもimport
のルールが適用されるのはnext/core-web-vitals
の中のplugins
にimport
が記載されているからです。
flat configで書き換える
flat configの特徴はその名の通り、flatな設定の書き方であり、以下のように配列の中に適用させたい設定を順にオブジェクトの形で書いていきます。
この時前から順に適用されていき、それぞれの対象ファイルはfiles
に該当するファイルに制限されます。
module.exports = [
{
...
rules: { ... }
},
{
...
files: ["*.ts", "*.tsx"],
rules: { ... }
}
]
新しいflat configではextends
の項目はありません。
そのため、どのルールを適用するかは明示的に書く必要があります。
一応後方互換性のためのFlatCompat
なるClassがeslint/eslintrc
からexportされているのでそれを使えばextends
を使うこともできますが、今回はせっかくflat configを使うのでその使用は最小限にしました。
一気に置き換えると何が何だかわからなくなりそうなので部分ごとに置き換えていきます。
以前までは設定ファイルはjson
だったりyaml
だったりと色んな書き方ができましたが、flat configで読み込めるのはeslint.config.js
のみです。
ちなみに諸事情により今回はCommonJSで記載していきます。
flat config自体はESMで記載できます。
eslint-config-prettier
まずeslint-config-prettier
ですが、内部実装を見てみるとこれがextends
で読み込んでいたのは膨大な量のrulesのみです。
従って以下のようにrules
に展開してあげればそれだけで良さそうです。
const prettier = require("eslint-config-prettier")
module.exports = [
{
rules: {
...prettier.rules,
},
},
]
next/core-web-vitals
これはeslint-config-next
というパッケージの中のcore-web-vitals.js
の内容を読み込んでいます。
それを見てみると以下のような記述がありました。
module.exports = {
extends: [require.resolve('.'), 'plugin:@next/next/core-web-vitals'],
}
つまり、@next/eslint-plugin-next
のcore-web-vitals.js
を読み込んでいるということなのですが、この内部実装がどうなってるのかは探せませんでした。
従って、ここは諦めて先述したFlatCompat
を使ってextends
することにしました。
const { FlatCompat } = require("@eslint/eslintrc")
const prettier = require("eslint-config-prettier")
const compat = new FlatCompat()
module.exports = [
...compat.extends("next/core-web-vitals"),
{
rules: {
...prettier.rules,
},
},
]
@typescript-eslint/eslint-plugin
元の設定ではconfigsのrecommended
をextends
していました。
この部分の実装を見てみると下記のようになっていました。
extends: ['./configs/base', './configs/eslint-recommended'],
baseの方はただのparserの設定です。
recommended
の中でeslint-recommended
をextends
しています。
この2つはどちらもrulesの設定のみなので、parserの設定とこの2つのrulesを展開してあげれば良さそうです。
const { FlatCompat } = require("@eslint/eslintrc")
const prettier = require("eslint-config-prettier")
const ts = require("@typescript-eslint/eslint-plugin")
const tsParser = require("@typescript-eslint/parser")
const compat = new FlatCompat()
module.exports = [
...compat.extends("next/core-web-vitals"),
{
files: ["src/**/*.ts", "src/**/*.tsx"],
languageOptions: {
parser: tsParser,
},
plugins: {
"@typescript-eslint": ts,
},
rules: {
...ts.configs["recommended"].rules,
...ts.configs["eslint-recommended"].rules,
},
},
{
rules: {
...prettier.rules,
},
},
]
こんな感じでts
, tsx
ファイルを対象にしてrecommended
とeslint-recommended
の2つのrulesを展開しています。
また、以前までとは違い、parser
やplugins
の設定も明示的に行う必要があります。
残ったルールの設定
あと記載していないルールはimport/no-duplicates
とno-restricted-imports
のみです。
一気に2つ追記します。
const { FlatCompat } = require("@eslint/eslintrc")
const prettier = require("eslint-config-prettier")
const ts = require("@typescript-eslint/eslint-plugin")
const tsParser = require("@typescript-eslint/parser")
const compat = new FlatCompat()
module.exports = [
...compat.extends("next/core-web-vitals"),
{
files: ["src/**/*.ts", "src/**/*.tsx"],
languageOptions: {
parser: tsParser,
},
plugins: {
"@typescript-eslint": ts,
},
rules: {
...ts.configs["recommended"].rules,
...ts.configs["eslint-recommended"].rules,
},
},
{
files: ["src/**/*.tsx"],
ignores: ["src/components/Link.tsx"],
rules: {
"no-restricted-imports": [
"error",
{
paths: ["next/link"],
},
],
},
},
{
rules: {
"import/no-duplicates": "error",
...prettier.rules,
},
},
]
import/no-duplicates
は単に追記しただけです。
no-restricted-imports
の方はignores
に適用しないファイルを指定しています。
このおかげでルールを一括で適用してoverrides
で例外を上書きするという構文を避けることができています。
こんな感じで適用したいファイルとルールの組み合わせごとにオブジェクトで定義して、それらが配列として順に適用されるというような感じでしょうか。
shareable configsをextendsする構文が無くなったことで、全体の記述量は多くなりましたが、どのルールがどんなふうに適用されているのかが明示的になってかなり読みやすくなったのではないかと思います。
以前までの書き方だと、どのpluginがどのタイミングで適用されているのかが暗黙的でよくわからない上にルールの上書きが起こっていると非常に読み解きにくかったので、記述が長くなったとしても個人的にはflat configの方が断然好きです。
ところでeslint-config-prettier
はextends
の時は一番最後に書くのが推奨されていたので、それに倣い最後に記載する形にしました。
競合した場合にこちらのルールを優先させたかったからです。
逆に言えば、競合しない全体で適用させたいような設定は一番最初に記載しておくことで、これまでのextends
によるshareable configsとまでは行かないまでもある程度全体のベース設定として使えるのではないでしょうか。
ただ、今回書き換えるに当たって、どのrulesを適用させればいいかなどはライブラリの内部実装を見ながら地道に対応していきました。
正直適用しているpluginが多い場合は面倒です。
この辺はエコシステムが成熟してもうちょっと楽に設定できる未来が来ることを祈ってます。笑
まとめ
従来のeslintの設定ファイルを実際にflat configで書き換えていきました。
記述量は長くなるかもしれないけど、明示的でわかりやすくなって個人的には積極的に使っていきたいなと思いました。
まだexperimentalな機能で情報も少ないので参考になったら嬉しいです。
間違ってるところなどあったら指摘いただけると嬉しいです。