sunabox

eslintをflat configで書き換える

本記事は「つながる勉強会 Advent Calendar 2022」の 21 日目の記事です。

前日も自分の記事で、「eslintのpluginsとextendsの違いを理解する」です。
前回の記事でeslintの設定について勘違いしやすい場所を解説したのでよかったら見てみてください。

この記事ではその内容を理解した上で、いよいよflat configに置き換えていきます。

ESLint's new config system, Part 2: Introduction to flat config - ESLint - Pluggable JavaScript LinterA pluggable and configurable linter tool for identifying and reporting on patterns in JavaScript. Maintain your code quality with ease.
faviconeslint.org

書き換え前

書き換え前のeslintの設定を見た後に、実際にそれを置き換えていくというステップで進めていきます。

今回はこんな設定を考えてみます。

.eslintrc.json
{
  "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-importno-duplicatesとeslint標準のno-restricted-importsのみです。
no-restricted-importsではNextのLinkのimportをsrc/components/Link.tsx以外で禁止しています。
(Linkを拡張した独自コンポーネントを定義して、それしか使えなくするためのルール)

ちなみにpluginsimportを記述していませんが、それでもimportのルールが適用されるのはnext/core-web-vitalsの中のpluginsimportが記載されているからです。

flat configで書き換える

flat configの特徴はその名の通り、flatな設定の書き方であり、以下のように配列の中に適用させたい設定を順にオブジェクトの形で書いていきます。
この時前から順に適用されていき、それぞれの対象ファイルはfilesに該当するファイルに制限されます。

JavaScript Icon
eslint.config.js
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に展開してあげればそれだけで良さそうです。

JavaScript Icon
eslint.config.js
const prettier = require("eslint-config-prettier")
 
module.exports = [
  {
    rules: {
      ...prettier.rules,
    },
  },
]

next/core-web-vitals

これはeslint-config-nextというパッケージの中のcore-web-vitals.jsの内容を読み込んでいます。
それを見てみると以下のような記述がありました。

JavaScript Icon
eslint-config-next/core-web-vitals.js
module.exports = {
  extends: [require.resolve('.'), 'plugin:@next/next/core-web-vitals'],
}

つまり、@next/eslint-plugin-nextcore-web-vitals.jsを読み込んでいるということなのですが、この内部実装がどうなってるのかは探せませんでした。

従って、ここは諦めて先述したFlatCompatを使ってextendsすることにしました。

JavaScript Icon
eslint.config.js
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のrecommendedextendsしていました。
この部分の実装を見てみると下記のようになっていました。

extends: ['./configs/base', './configs/eslint-recommended'],
typescript-eslint/packages/eslint-plugin/src/configs/recommended.ts at 9e35ef9af3ec51ab2dd49336699f3a94528bb4b1 · typescript-eslint/typescript-eslint:sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript - typescript-eslint/typescript-eslint
favicongithub.com

baseの方はただのparserの設定です。
recommendedの中でeslint-recommendedextendsしています。
この2つはどちらもrulesの設定のみなので、parserの設定とこの2つのrulesを展開してあげれば良さそうです。

JavaScript Icon
eslint.config.js
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ファイルを対象にしてrecommendedeslint-recommendedの2つのrulesを展開しています。
また、以前までとは違い、parserpluginsの設定も明示的に行う必要があります。

残ったルールの設定

あと記載していないルールはimport/no-duplicatesno-restricted-importsのみです。

一気に2つ追記します。

JavaScript Icon
eslint.config.js
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-prettierextendsの時は一番最後に書くのが推奨されていたので、それに倣い最後に記載する形にしました。
競合した場合にこちらのルールを優先させたかったからです。

逆に言えば、競合しない全体で適用させたいような設定は一番最初に記載しておくことで、これまでのextendsによるshareable configsとまでは行かないまでもある程度全体のベース設定として使えるのではないでしょうか。

ただ、今回書き換えるに当たって、どのrulesを適用させればいいかなどはライブラリの内部実装を見ながら地道に対応していきました。
正直適用しているpluginが多い場合は面倒です。
この辺はエコシステムが成熟してもうちょっと楽に設定できる未来が来ることを祈ってます。笑

まとめ

従来のeslintの設定ファイルを実際にflat configで書き換えていきました。

記述量は長くなるかもしれないけど、明示的でわかりやすくなって個人的には積極的に使っていきたいなと思いました。

まだexperimentalな機能で情報も少ないので参考になったら嬉しいです。
間違ってるところなどあったら指摘いただけると嬉しいです。

参考

Configuration Files - ESLint - Pluggable JavaScript LinterA pluggable and configurable linter tool for identifying and reporting on patterns in JavaScript. Maintain your code quality with ease.
faviconeslint.org
Flat Config導入完了! 新しいESLintの設定フォーマットを使ってみた
faviconzenn.dev
ESLintのconfigがどのように変わり得るか(flat configとは何か)
faviconzenn.dev
Buy Me A Coffeeのbutton

目次