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な機能で情報も少ないので参考になったら嬉しいです。
間違ってるところなどあったら指摘いただけると嬉しいです。


