ESLint を eslintrc から Flat Config に移行する、ハマりポイントを添えて。

ESLint の Flat Config は v9 でデフォルトとなり、 v10 で eslintrc は完全に削除されるため、徐々に移行していく方が増えてくる頃合いでしょうか。

https://eslint.org/blog/2023/10/flat-config-rollout-plans/

私が移行する上でわからなかったこと、ハマったポイントなど残しておこうと思います。
これが誰かの助けになれば幸いです。

:::note warn
この記事で扱っている ESLint のバージョンは `8.52.0` です。
:::

## ESLint の Flat Config とは

ESLint の新しい設定システムです。

今まで ESLint の設定は eslintrc に記述していましたが、Flat Config では `eslint.config.js` というファイルに記述していく形になります。

eslintrc で書いていたものとの違いは主に以下のものがあります。

- plugin や perser の読み込み方が変わった
- extends が廃止された
- glob pattern で対象のファイルを毎に設定を書くことができるようになった
    - .eslintignore で除外ファイルを指定する必要がなくなった
    - overrides プロパティを使う必要がなくなった

詳しくは公式の Flat Config のページや移行ガイドを見ていただくのが良いでしょう。

https://eslint.org/docs/latest/use/configure/configuration-files-new

https://eslint.org/docs/latest/use/configure/migration-guide


## 基本的な Flat Config の書き方

Flat Config は `eslint.config.js` をプロジェクトのルートに配置するか、環境変数`ESLINT_USE_FLAT_CONFIG` を `true` に設定することで有効化されます。
早速 eslint.config.js をプロジェクトルートに作成しましょう！

### 各オプションについて

eslintrc から eslint.config.js に変わるにあたって、 rule などは同じような書き方で書くことができますが env や plugin の書き方が大きく変わっていると感じました。

Flat Config の書き方で見ると eslintrc の項目は以下のように対応しています。

```js:eslint.config.js
import globals from "globals"
import jsdoc from "eslint-plugin-jsdoc"

export default [
	{
		// 1. ルールの対象を glob pattern で指定できるようになりました。
		files: ["**/*.js"],
		languageOptions: {
			// 2. env オプションは無くなり、代わりに globals を使用するようになりました。
			globals: {
				...globals.browser,
				...globals.node
			},
			// eslintrc の parserOptions と同じです。
			parserOptions: {
				ecmaVersion: "latest"
			},
		},
  		// 3. plugin は名称を指定できるようになりましたが、注意があります。
		plugins: {
			jsdoc
		},
		// rule の書き方は基本的に同じです。
		rules: {
			"jsdoc/require-description": "error"
		}
	}
]
```

#### 1. ルールの対象を glob pattern で指定できるようになりました。

上記の場合拡張子が js のファイルのみオブジェクト内のルールが適用され、それ以外のファイルではルールは適用されません。

#### 2. env オプションは無くなり、代わりに globals を使用するようになりました。
globals を別途インストールする必要がありますのでその点は注意しましょう。
    
```bash
npm i -D globals
```
    
#### 3. plugin は名称を指定できるようになりましたが、注意があります。
ここで指定した名称は `rules` に影響します。

上記の場合は eslint-plugin-jsdoc を `jsdoc` という名前を指定しているため rule も `jsdoc/xxx` と指定しています。もし `doc` という名前を指定した場合 rule の指定方法は以下のようになります。
    
```js:eslint.config.js
{
    {
        ...
        plugins: {
            doc: jsdoc
        },
        rules: {
            "doc/require-description": "error"
        }
    }
}
```
ただしこの指定方法には注意が必要です。
plugin に含まれている recommended を使用したい場合 plugin 側で既に書かれている key と plugin 名が一致しないとエラーになってしまいます。
そのため plugin 制作元が指定している名称を指定するのが無難でしょう。
    
```js:eslint.config.js
{
    plugins: {
        // s が抜けている、正しくは react-hooks
        "react-hook": reactHooks,
    },
    rules: {
        // react-hooks で指定されていないので以下のようなエラーが発生する
        // TypeError: Key "rules": Key "react-hooks/rules-of-hooks": Could not find plugin "react-hooks".
        ...reactHooks.configs.recommended.rules,
    },
}
```
    

### config を分割する

files で対象にするファイルを絞り込めるようになったので、TypeScript 用の Config や Test 用の Config などを別々に書くことができるようになりました。

個人的には全ての設定を1つのファイルに書くと長くなりすぎて辛くなってくるので、ファイル分割などすることである程度見通しが良くなるかと思います。

以下のような感じで必要なものだけファイル毎の config を用意しておいて、最後に必要なものだけマージできるのがいいですね（そのままでは動かないのでご注意ください）。

```js:eslint.config.js
const commonConfig = {
	//　files なしで設定を書く
}

const tsConfig = {
    // ts 用の設定を書く
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
        parser: tsParser,
        parserOptions: {
          project: ["tsconfig.json"],
        }
    },
    ...
}

const testConfig = {
    //　test 用の設定を書く
    files: ["**/*.test.{ts,tsx}"],
    languageOptions: {
        globals: {
            // test では jest を global object として扱いたい
            ...globals.jest
        },
    },
    ...
}

const configFileConfig = {
    //　config file 用の設定を書く
    files: [".storybook/*.{ts,tsx}", "next.config.js"],
    languageOptions: {
        // config ファイルにだけ globals.node を指定したり、別の tsconfig.json を読み込ませたりもできます。
        globals: {
            ...globals.node,
        },
        parserOptions: {
            project: "tsconfig.eslint.json",
        },
    },
}

// 最後にマージ、順番には気をつけましょう
export module [
	commonConfig,
	tsConfig,
	testConfig,
	configFileConfig
]
```

### recommended ルールを適用する

FlatConfig に対応した plugin であれば export している配列に追加するだけで使用することができます。
※ 対応されていない plugin は同じように書いても反映されないので注意してくだいさい
※ plugin によっては書き方が異なる場合があるので plugin のドキュメントを参照することをお勧めします。

例えば esllintrc で `extends: ["eslint:recommended"]` と書いていたものは以下のように書き換えることができます。

```js:eslint.config.js
import js from "@eslint/js"

const someConfig = { ... }

export default [
	js.configs.recommended, // eslint:recommended を適用する
	someConfig
]
```

js.configs.recommended で files が指定されている場合は該当のファイルのみに適応されます。
もし個別に files を指定したい場合は `rules` で recommended  を読み込むようにしましょう。

## その他ハマりポイント

### config に型を当てたい

FlatConfig にももちろん型ファイルが存在します！ types をインストールすることで型安全に config を書くことができます！

```bash
npm i -D @types/eslint
```

以下のように指定することで型を当てることができます。

```js:eslint.config.js
/** @type {import("eslint").Linter.FlatConfig[]} */
export default [
	someConfig
]

// 個別に config を書く場合
/** @type {import("eslint").Linter.FlatConfig} */
const someConfig = {
	...
}
```
（これで typo に怯える日々から解放される・・・！）


### Flat Config に対応していない Plugin を使いたい

ESLint の plugin の中にはまだ Flat Config に対応されていないものもあります。
そんな場合は公式が用意してくれている FlatCompat を使用しましょう。

FlatCompat の準備は公式サイトを参照してもらうとして・・・

https://eslint.org/docs/latest/use/configure/migration-guide#using-eslintrc-configs-in-flat-config

例えば `eslint-plugin-storybook` の recommended を追加したい場合、以下のようにすることで追加することができます。

```js:eslint.config.js
export default [
	...compat.extends("plugin:storybook/recommended"),
]
```

recommended を追加するだけの場合 storybook の plugin を import する必要はありません。
個別に rule を書く場合は Flat Config の書き方に習い plugin を指定して書くことができます。
使用したい plugin が Flat Config に対応されるまではこれで乗り切りましょう。

:::note warn
eslint-plugin-storybook のバージョンは`0.6.15` です。
:::

### CommonJS なプロジェクトでも ES module 形式で書きたい（SyntaxError: Cannot use import statement outside a module が出る）

Flat Config はデフォルトでは拡張子 `.mjs` がサポートされていません。
package.json で `type: "module"` を指定しているプロジェクトでは問題になりませんが、それ以外のプロジェクトでは拡張子が `.js` だと以下のようなエラーが発生します。

```
import js from "@eslint/js"
^^^^^^

SyntaxError: Cannot use import statement outside a module
```

import の部分を `const js = require("@eslint/js)` のように `require` に書き換えればいい話なのですが、基本的に設定ファイルは使い回したく、プロジェクトごとに書き方を変えるのは面倒です。また全てのプロジェクトで ES module を指定することも難しいと思います。

その場合 config file を `.mjs` 形式で作成し cli のオプションに該当のファイルを指定してあげることで 動作させることができます。 `eslint.config.js` 以外のファイルを指定する場合環境変数を渡す必要があることに注意しましょう。

```json:package.json
{
	"scripts" {
		"lint": "ESLINT_USE_FLAT_CONFIG=true eslint -c eslint.config.mjs 'src/**/*.{js,jsx,ts,tsx}'"
	}
}
```

## VSCode の ESLint Extension で Flat Config を有効化する

現状設定ファイルで Flat Config を有効化してあげる必要があります。

`settings.json` に以下を追記すると自動で eslint.config.js を読み込んでくれるようになります。

```json:.vscode/settings.json
{
	"eslint.experimental.useFlatConfig": true
}
```

もし eslint.config.js 以外のファイル名を使用している場合はファイルのパスを設定してあげる必要がありますので注意しましょう。

```json:.vscode/settings.json
{
	"eslint.options": {
    "overrideConfigFile": "eslint.config.mjs"
  }
}
```

:::note warn
ESLint Extension のバージョンは `2.4.2` です。
:::

---

以上になります！他にも気づいた点があればアップデートしていこうと思います。
おかしな点などありましたらお気軽にコメントください！
それでは良い Flat Config ライフをー:muscle: