remixのESLintの設定をFlat Configで記述し直す

## はじめに
ESLintのバージョン9.0.0から[Flat Config](https://eslint.org/docs/latest/use/configure/configuration-files-new)という設定ファイルの新たな記法がデフォルトで有効になり、元の記法が非推奨となります。そしてバージョン10.0.0では元の記法がサポートされなくなります。

https://eslint.org/blog/2023/10/flat-config-rollout-plans/#flat-config-by-default-in-eslint-v9.0.0

サポートが切れるため元の記法で記述していた場合はFlat Configへ書き直す必要があります。しかし、同じ設定を記述するには記法が大きく変化したため慣れや工夫が必要となります。

この記事ではFlat Configに移行することへ慣れるための練習として、`create-remix`で生成されるremixのESLintの設定をFlat Configに書き直します。
remixの設定はReactを開発する上で一般的なルールが記載されているので[`eslint-config-next`](https://github.com/vercel/next.js/tree/canary/packages/eslint-config-next)のような設定がまとまったパッケージを使っていない多くの環境にとって参考になると考えています。

成果物はGitHubで公開しています。

https://github.com/k35o/remix-to-flat-config

:::note
Flat Configについての背景や設定について正確に知りたい場合はこれらのサイトを閲覧することがおすすめです。
- [Flat ConfigについてのRFC](https://github.com/eslint/rfcs/pull/9)
- [blog1](https://eslint.org/blog/2022/08/new-config-system-part-1/)
- [blog2](https://eslint.org/blog/2022/08/new-config-system-part-2/)
- [blog3](https://eslint.org/blog/2022/08/new-config-system-part-3/)
- [ドキュメント](https://eslint.org/docs/latest/use/configure/configuration-files-new)
:::


## 移行前の環境準備
まずは`create-remix`でremixの環境を作成します。
```sh
npx create-remix@latest
```
以下がremixのバージョン2.8.1におけるESLintの設定です。
```js:.eslintrc.cjs
/**
 * This is intended to be a basic starting point for linting in your app.
 * It relies on recommended configs out of the box for simplicity, but you can
 * and should modify this configuration to best suit your team's needs.
 */

/** @type {import('eslint').Linter.Config} */
module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module",
    ecmaFeatures: {
      jsx: true,
    },
  },
  env: {
    browser: true,
    commonjs: true,
    es6: true,
  },

  // Base config
  extends: ["eslint:recommended"],

  overrides: [
    // React
    {
      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: {},
        },
      },
    },

    // 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",
      ],
    },

    // Node
    {
      files: [".eslintrc.cjs"],
      env: {
        node: true,
      },
    },
  ],
};
```
ESLintの実行は`npm run lint`で実行できます。`package.json`の`scripts`を確認すると`npx eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .`で実行されていることがわかります。

## Flat Configへの移行
`create-remix`で生成された`.eslintrc.cjs`を元に`eslint.config.js`を記述します。remixのパッケージではデフォルトがESMとなっているためESM形式を用います(`package.json`で`type`が`module`になっていない場合は拡張子を`mjs`にした方が良いかもしれません)。
### テストファイルを作る
Flat Configへ移行した後に設定が有効になっていることを確かめるため以下のようなファイルを作成します。
```tsx
import { Link, NavLink } from "@remix-run/react";
import { useEffect } from "react";

// violate eslint recommendation
for (let i = 0; i < 10; i--) {
  console.log(i);
}

// violate react recommendation
() => {
  return <>{
    [1, 2, 3].map((i) => <div>{i}</div>)
  }</>;
};

// violate react-hooks recommendation
const _ = () => {
  const flag = true;
  if (flag) {
    useEffect(() => {
      console.log('effect');
    }, []);
  }
  return <div />;
};
_;

// violate jsx-a11y recommendation
const App = () => {
  return (
    <>
      <a>Link</a>
      <Link to="/about">About</Link>
      <NavLink to="/contact">Contact</NavLink>
    </>
  );
};

// violate typescript-eslint recommendation
Array(0, 1, 2);
```
`eslint.config.js`を作成する前に`npm run lint`を実行して、それぞれ異なるパッケージからのエラーが5つ出ることを確認していください。

このファイルはエラーが発生することを確認するために作っただけなので、他のファイルを適当に準備しても問題ありません。

:::note warn
このファイルは移行前後で設定が同一であることを保証するものではないです。それぞれの代表的なルールが有効であることを確かめるために記述していますので、厳密にやりたい場合は他の方法を検討してください。
ESLint Config Inspectorでは有効なルールを可視化してくれるのでおすすめです([記事](https://qiita.com/KokiSakano/items/55fc416cbaa6edb2bf20))。
:::

### 設定ファイルを作る
`eslint.config.js`ファイルを作ります。中身は以下のように記述してください。
```js:eslint.config.js
/**
 * @type {import('eslint').Linter.FlatConfig[]}
 */
export default [];
```
作成したらESLintは`.eslintrc.cjs`ではなく`eslint.config.js`を設定ファイルとして読みにいきます。`npx eslint --debug`を実行して`Loading config from /xxx/eslint.config.js`と表示されることを確認してください。

Flat Configでは設定を有効するファイル単位ごとや、複数の設定を同時に記述するために配列で設定を記述します。

### 無視するファイル
前の記法では無視するファイルをcliで`--ignore-path .gitignore`のように渡していました。Flat Configでは無視するファイルを設定ファイルに記述します。
以下のように`ignores`に無視するファイルの一覧を詰め込んで配列に流し込むことで無効化されます。
```js:eslint.config.js
/**
 * @type {import('eslint').Linter.FlatConfig[]}
 */
export default [
  {
    ignores: ['node_modules', '.cache', 'build', 'public/build', '.env'],
  },
];
```
配列に記述された設定は最終的にマージされて1つの設定ファイルとなるので、これによって`ignores`に記述したファイルに対して実行されなくなります。

`package.json`の`scripts`からも`--ignore-path`が不要になったので消します。
```json:package.json
  "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
```

#### Base(プロジェクト全体の設定)
元ファイルのコメントに合わせて`Base`、`React`、`TypeScript`、`Node`に分けて移行します。
まずは`Base`です。`Base`は特別にコメントの範囲を超えて`overrides`以外の全ての設定を記述します。

##### root
```js:.eslintrc.cjs
  root: true,
```
前の記法は実行されるファイルからルートのまでのすべての設定ファイルを読み込んでいましたが、Flat Configは最も近い祖先のファイルだけを読み込むのでルートファイルという概念がなくなり`root: true`の宣言自体が不要になりました。

##### parserOptions
```js:.eslintrc.cjs
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module",
    ecmaFeatures: {
      jsx: true,
    },
  },
```
`ecmaVersion`と`sourceType`は`parser`だけではなく構成等にも反映するため`languageOptions`へ書かれるようになり、`parserOptions`自体も`languageOptions`へ記述されるようになりました。
```js:eslint.config.js
{
  languageOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    parserOptions: {
      ecmaFeatures: {
        jsx: true
      },
    },
  },
},
```

##### env
```js:.eslintrc.cjs
  env: {
    browser: true,
    commonjs: true,
    es6: true,
  },
```
Flat Configでは`env`を削除して、`languageOptions`の`globals`に記述するようになりました。そして、`globals`パッケージを利用して環境の情報をよりわかりやすく提供するようになりました([globals.json](https://github.com/sindresorhus/globals/blob/main/globals.json)に環境ごとの設定が列挙されており、それを展開しています)。
```js:eslint.config.js
import globals from 'globals';
...
...
{
  languageOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    globals: {
      ...globals.browser,
      ...globals.commonjs,
      ...globals.es6,
    },
    parserOptions: {
      ecmaFeatures: {
        jsx: true
      },
    },
  },
},
```
`globals`パッケージは`eslint`をインストールした時の依存パッケージとしてインストールされているのでそのままでも利用出来ますが、直接インポートするので`devDependencies`に追加した方が良いです。
```sh
npm install --save-dev globals
```
筆者の環境では`@remix-run/dev`に依存する`@babel/traverse`が古い`globals`を利用しているのでインストールしなければ動きませんでした。
![スクリーンショット 2024-03-18 17.10.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/cb4c90c9-dfd1-76b3-791d-96702e47ffae.png)

##### eslint recommended
```js:eslintrc.cjs
  // Base config
  extends: ["eslint:recommended"],
```
`eslint`は当然のことながらFlat Configに対応しているので[ドキュメント](https://eslint.org/docs/latest/use/configure/configuration-files-new#using-predefined-configurations)通りに実装すれば良いです。
`@eslint/js`からすでに定義済みの設定を持ってきて先ほどまで記述していた箇所に展開します。定義された内容は[eslintのGitHub](https://github.com/eslint/eslint/blob/main/packages/js/src/configs/eslint-recommended.js)から確認できます。`rules`を記述しているだけだったので、`languageOptions`は上書きするような形で配置しました。
```js:eslint.config.js
import globals from 'globals';
import eslint from '@eslint/js';
...
...
  {
    ...eslint.configs.recommended,
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: {
        ...globals.browser,
        ...globals.commonjs,
        ...globals.es6,
      },
      parserOptions: {
        ecmaFeatures: {
          jsx: true
        },
      },
    },
  },
```
##### まとめ
ここまでの移行で設定ファイルは以下のようになりました。
```js:eslint.config.js
import globals from 'globals';
import eslint from '@eslint/js';

/**
 * @type {import('eslint').Linter.FlatConfig[]}
 */
export default tseslint.config(
  {
    ignores: ['node_modules', '.cache', 'build', 'public/build', '.env'],
  },
  {
    ...eslint.configs.recommended,
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: {
        ...globals.browser,
        ...globals.commonjs,
        ...globals.es6,
      },
      parserOptions: {
        ecmaFeatures: {
          jsx: true
        },
      },
    },
  },
);

```
`npx eslint . --debug`で関係ないファイルが実行対象になっていないこと、エラー確認用のコードから期待したエラーが返ることを確認できます。

#### React
次に`React`です。[`eslint-plugin-react`](https://github.com/jsx-eslint/eslint-plugin-react)、[`eslint-plugin-react-hooks`](https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks)、[`eslint-plugin-jsx-a11y`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y)等の設定をします。

`.eslintrc.cjs`では`overrides`を用いて特定のファイルに対するルールを記述していましたが、Flat Configでは他のルールと同じように配列に追加して、設定内の`files`によって実行対象のファイルを制限します。

```js:eslint.config.js
/**
 * @type {import('eslint').Linter.FlatConfig[]}
 */
export default tseslint.config(
  {
    ...
  },
  {
    ...
  },
  {
    files: ["**/*.{js,jsx,ts,tsx}"],
    ...
  },
);

```

##### eslint-plugin-react
`eslint-plugin-react`は[READMEのFalt Configのサポートについて](https://github.com/jsx-eslint/eslint-plugin-react?tab=readme-ov-file#configuration-new-eslintconfigjs)を参考に実装します。
`eslint-plugin-react`に関連する部分を`.eslintrc.cjs`から抜き出すと以下のようになります(今後部分ごとに参照する場合は対象の箇所を抜き出した状態で紹介します)。
```js:.eslintrc.cjs
    // React
    {
      files: ["**/*.{js,jsx,ts,tsx}"],
      plugins: ["react"],
      extends: [
        "plugin:react/recommended",
        "plugin:react/jsx-runtime",
      ],
      settings: {
        react: {
          version: "detect",
        },
        formComponents: ["Form"],
        linkComponents: [
          { name: "Link", linkAttribute: "to" },
          { name: "NavLink", linkAttribute: "to" },
        ],
      },
    },
```
`settings`はこれまで通り記述できるので、`recommended`と`jsx-runtime`の箇所を書き換えるだけです。
```js:eslint.config.js
import reactRecommended from 'eslint-plugin-react/configs/recommended.js';
import reactJSXRuntime from 'eslint-plugin-react/configs/jsx-runtime.js';
...
... 
  {
    files: ['**/*.{js,jsx,ts,tsx}'],
    ...reactRecommended,
    ...reactJSXRuntime,
    rules: {
      ...reactRecommended.rules,
      ...reactJSXRuntime.rules,
    },
    languageOptions: {
      ...reactRecommended.languageOptions,
      ...reactJSXRuntime.languageOptions,
    },
    settings: {
      react: {
        version: "detect",
      },
      formComponents: ["Form"],
      linkComponents: [
        { name: "Link", linkAttribute: "to" },
        { name: "NavLink", linkAttribute: "to" },
      ],
    },
  },
```
[`recommended`と`jsx-runtime`に実装された内容](https://github.com/jsx-eslint/eslint-plugin-react/tree/master/configs)を元にしてそれぞれ展開しています。[React17以降のJSXの変換](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html)へ対応するために`recommended`のルールを`jsx-runtime`のルールで上書きして欲しいので`jsx-runtime`の方を後から宣言しました。
`rules`の内容をそれぞれ確認すると`recommended`の方は`'react/react-in-jsx-scope': 2`に対して`jsx-runtime`では`'react/react-in-jsx-scope': 0`となっていてこの記述では`0`が有効になります。

##### eslint-plugin-react-hooks
`eslint-plugin-react`とは異なり、パッケージ自体でFlat Confingの対応が行われていないのでFlat Configで扱えるように変換する必要があります。
このように対応されていないパッケージとの互換性のために`FlatCompat`というクラスが準備されています([使い方についてのブログ](https://eslint.org/blog/2022/08/new-config-system-part-2/#backwards-compatibility-utility))。
```js:.eslintrc.cjs
    // React
    {
      files: ["**/*.{js,jsx,ts,tsx}"],
      extends: [
        "plugin:react-hooks/recommended",
      ],
    },
```
`new FlatCompat()`で生成した`compat`を元に`eslint-plugin-react-hooks`の`recommended`をFlat Configで利用できるように最適化します。
```js:eslint.config.js
import { FlatCompat } from '@eslint/eslintrc';
import reactRecommended from 'eslint-plugin-react/configs/recommended.js';
import reactJSXRuntime from 'eslint-plugin-react/configs/jsx-runtime.js';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
...
const compat = new FlatCompat();
...
  {
    files: ["**/*.{js,jsx,ts,tsx}"],
    ...reactRecommended,
    ...reactJSXRuntime,
    rules: {
      ...reactRecommended.rules,
      ...reactJSXRuntime.rules,
    },
    languageOptions: {
      ...reactRecommended.languageOptions,
      ...reactJSXRuntime.languageOptions,
    },
    extends: [
      ...compat.config(reactHooksPlugin.configs.recommended),
    ],
    settings: {
      react: {
        version: "detect",
      },
      formComponents: ["Form"],
      linkComponents: [
        { name: "Link", linkAttribute: "to" },
        { name: "NavLink", linkAttribute: "to" },
      ],
    },
  },
```

##### eslint-plugin-jsx-a11y
このライブラリも`eslint-plugin-react-hooks`と同じくFlat Configの対応が完了しておりません。
```js:.eslintrc.cjs
    // React
    {
      files: ["**/*.{js,jsx,ts,tsx}"],
      plugins: ["jsx-a11y"],
      extends: [
        "plugin:jsx-a11y/recommended",
      ],
    },
```
先ほどと異なる点は`plugins`があることです。`plugins`は対応するパッケージに対応させるように宣言します。
```js:eslint.config.js

import { FlatCompat } from '@eslint/eslintrc';
import reactRecommended from 'eslint-plugin-react/configs/recommended.js';
import reactJSXRuntime from 'eslint-plugin-react/configs/jsx-runtime.js';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
...
const compat = new FlatCompat();
...
  {
    files: ["**/*.{js,jsx,ts,tsx}"],
    ...reactRecommended,
    ...reactJSXRuntime,
    rules: {
      ...reactRecommended.rules,
      ...reactJSXRuntime.rules,
    },
    languageOptions: {
      ...reactRecommended.languageOptions,
      ...reactJSXRuntime.languageOptions,
    },
    plugins: {
      ['jsx-a11y']: jsxA11yPlugin,
    },
    extends: [
      ...compat.config(reactHooksPlugin.configs.recommended),
      ...compat.config(jsxA11yPlugin.configs.recommended),
    ],
    settings: {
      react: {
        version: "detect",
      },
      formComponents: ["Form"],
      linkComponents: [
        { name: "Link", linkAttribute: "to" },
        { name: "NavLink", linkAttribute: "to" },
      ],
    },
  },
```

##### まとめ
対応していない`settings`の`"import/resolver"`を新しいファイルに移した段階でのファイルは以下のようになっています。
```js:eslint.config.js
import globals from 'globals';
import eslint from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
import reactRecommended from 'eslint-plugin-react/configs/recommended.js';
import reactJSXRuntime from 'eslint-plugin-react/configs/jsx-runtime.js';
import reactHooksPlugin from 'eslint-plugin-react-hooks';

const compat = new FlatCompat();

/**
 * @type {import('eslint').Linter.FlatConfig[]}
 */
export default tseslint.config(
  {
    ignores: ['node_modules', '.cache', 'build', 'public/build', '.env'],
  },
  {
    ...eslint.configs.recommended,
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: {
        ...globals.browser,
        ...globals.commonjs,
        ...globals.es6,
      },
      parserOptions: {
        ecmaFeatures: {
          jsx: true
        },
      },
    },
  },
  {
    files: ["**/*.{js,jsx,ts,tsx}"],
    ...reactRecommended,
    ...reactJSXRuntime,
    rules: {
      ...reactRecommended.rules,
      ...reactJSXRuntime.rules,
    },
    languageOptions: {
      ...reactRecommended.languageOptions,
      ...reactJSXRuntime.languageOptions,
    },
    plugins: {
      ['jsx-a11y']: jsxA11yPlugin,
    },
    extends: [
      ...compat.config(reactHooksPlugin.configs.recommended),
      ...compat.config(jsxA11yPlugin.configs.recommended),
    ],
    settings: {
      react: {
        version: "detect",
      },
      formComponents: ["Form"],
      linkComponents: [
        { name: "Link", linkAttribute: "to" },
        { name: "NavLink", linkAttribute: "to" },
      ],
      "import/resolver": {
        typescript: {},
      },
    },
  },
);

```

#### Typescript
続いて`TypeScript`です。[`typescript-eslint`](https://typescript-eslint.io/)と[`eslint-plugin-import`](https://github.com/import-js/eslint-plugin-import)の対応です。
まずはTypeScriptのファイルに対してルールを記述する場所を作ります。
```js:eslint.config.js
/**
 * @type {import('eslint').Linter.FlatConfig[]}
 */
export default tseslint.config(
  {
    ...
  },
  {
    ...
  },
  {
    ...
  },
  {
    files: ["**/*.{ts,tsx}"],
    ...
  },
);
```

##### eslint-plugin-import
`eslint-plugin-react-hooks`と同じように`FlatCompat`を利用して移行します。
```js:.eslintrc.js
    {
      files: ["**/*.{ts,tsx}"],
      plugins: ["import"],
      settings: {
        "import/internal-regex": "^~/",
        "import/resolver": {
          node: {
            extensions: [".ts", ".tsx"],
          },
          typescript: {
            alwaysTryTypes: true,
          },
        },
      },
      extends: [
        "plugin:import/recommended",
        "plugin:import/typescript",
      ],
    },
```
`settings`はそのまま移行して、`plguins`と`extends`はパッケージをインポートして記述します。
```js:eslint.config.js
import importPlugin from 'eslint-plugin-import';
...
...
  {
    files: ["**/*.{ts,tsx}"],
    plugins: {
      import: importPlugin,
    },
    extends: [
      ...compat.config(importPlugin.configs.recommended),
      ...compat.config(importPlugin.configs.typescript),
    ],
    settings: {
      "import/internal-regex": "^~/",
      "import/resolver": {
        node: {
          extensions: [".ts", ".tsx"],
        },
        typescript: {
          alwaysTryTypes: true,
        },
      },
    },
  },
```

##### typescript-eslint
`typescript-eslint`はFlat Configのサポートがされており、[リリースブログ](https://typescript-eslint.io/blog/announcing-typescript-eslint-v7#new-features---flat-config-support)を元に移行します。
まずは、利用するパッケージが変更されたのでこれまでの`@typescript-eslint/parser`と`@typescript-eslint/eslint-plugin`をアンインストールして`typescript-eslint`を導入します。
```sh
npm uninstall @typescript-eslint/parser @typescript-eslint/eslint-plugin
npm install --save-dev typescript-eslint
```
そして、これまで記述してきたESLintの設定全体を`tseslint.config`で囲みます。配列は展開して関数の引数に渡します。
```js:eslint.config.js
import tseslint from 'typescript-eslint';
...
...
export default tseslint.config(
  {
    ...
  },
  {
    ...
  },
  {
    ...
  },
  {
    ...
  },
);
```
これによって設定ファイル内の型やエディターの補完を強化してくれます。記述しなくても動きますが、`typescript-eslint`では利用することを強く推奨しています。

`typescript-eslint`を利用する準備ができたので移行します。
```js:.eslintrc.cjs

    {
      files: ["**/*.{ts,tsx}"],
      plugins: ["@typescript-eslint"],
      parser: "@typescript-eslint/parser",
      extends: [
        "plugin:@typescript-eslint/recommended",
      ],
    },
```
プラグインやパーサー等は自動で解決してくれるため不要です。`extends`に`import`と合わせて記述します。
```js:eslint.config.js
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
...
...
  {
    files: ["**/*.{ts,tsx}"],
    plugins: {
      import: importPlugin,
    },
    extends: [
      ...tseslint.configs.recommended,
      ...compat.config(importPlugin.configs.recommended),
      ...compat.config(importPlugin.configs.typescript),
    ],
    settings: {
      "import/internal-regex": "^~/",
      "import/resolver": {
        node: {
          extensions: [".ts", ".tsx"],
        },
        typescript: {
          alwaysTryTypes: true,
        },
      },
    },
  },
```
##### まとめ
`tslint.config`が加わって全体的な変化が加わりました。
```js:eslint.config.js
import globals from 'globals';
import eslint from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactRecommended from 'eslint-plugin-react/configs/recommended.js';
import reactJSXRuntime from 'eslint-plugin-react/configs/jsx-runtime.js';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
import importPlugin from 'eslint-plugin-import';

const compat = new FlatCompat();

export default tseslint.config(
  {
    ignores: ['node_modules', '.cache', 'build', 'public/build', '.env'],
  },
  {
    ...eslint.configs.recommended,
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: {
        ...globals.browser,
        ...globals.commonjs,
        ...globals.es6,
      },
      parserOptions: {
        ecmaFeatures: {
          jsx: true
        },
      },
    },
  },
  {
    files: ["**/*.{js,jsx,ts,tsx}"],
    ...reactRecommended,
    ...reactJSXRuntime,
    rules: {
      ...reactRecommended.rules,
      ...reactJSXRuntime.rules,
    },
    languageOptions: {
      ...reactRecommended.languageOptions,
      ...reactJSXRuntime.languageOptions,
    },
    plugins: {
      react: reactPlugin,
      ['jsx-a11y']: jsxA11yPlugin,
    },
    extends: [
      ...compat.config(reactHooksPlugin.configs.recommended),
      ...compat.config(jsxA11yPlugin.configs.recommended),
    ],
    settings: {
      react: {
        version: "detect",
      },
      formComponents: ["Form"],
      linkComponents: [
        { name: "Link", linkAttribute: "to" },
        { name: "NavLink", linkAttribute: "to" },
      ],
      "import/resolver": {
        typescript: {},
      },
    },
  },
  {
    files: ["**/*.{ts,tsx}"],
    plugins: {
      import: importPlugin,
    },
    extends: [
      ...tseslint.configs.recommended,
      ...compat.config(importPlugin.configs.recommended),
      ...compat.config(importPlugin.configs.typescript),
    ],
    settings: {
      "import/internal-regex": "^~/",
      "import/resolver": {
        node: {
          extensions: [".ts", ".tsx"],
        },
        typescript: {
          alwaysTryTypes: true,
        },
      },
    },
  },
);
```

#### Node
最後に`Node`です。設定ファイルに対する`env`を記述しています。
```js:esilntrc.js
    {
      files: [".eslintrc.cjs"],
      env: {
        node: true,
      },
    },
```
`eslint`の設定ファイル名だけ変えて、`env`を`languageOptions.globals`に`globals`パッケージを使って記述します。
```js:eslint.config.js
  {
    files: ['eslint.config.js'],
    languageOptions: {
      globals: {
        ...globals.node
      },
    },
  }
```
#### 最終的なファイル
最終的な`eslint.config.js`は以下のようになりました。
```js:eslint.config.js
import globals from 'globals';
import eslint from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactRecommended from 'eslint-plugin-react/configs/recommended.js';
import reactJSXRuntime from 'eslint-plugin-react/configs/jsx-runtime.js';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
import importPlugin from 'eslint-plugin-import';

const compat = new FlatCompat();

export default tseslint.config(
  {
    ignores: ['node_modules', '.cache', 'build', 'public/build', '.env'],
  },
  {
    ...eslint.configs.recommended,
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: {
        ...globals.browser,
        ...globals.commonjs,
        ...globals.es6,
      },
      parserOptions: {
        ecmaFeatures: {
          jsx: true
        },
      },
    },
  },
  {
    files: ["**/*.{js,jsx,ts,tsx}"],
    ...reactRecommended,
    ...reactJSXRuntime,
    rules: {
      ...reactRecommended.rules,
      ...reactJSXRuntime.rules,
    },
    languageOptions: {
      ...reactRecommended.languageOptions,
      ...reactJSXRuntime.languageOptions,
    },
    plugins: {
      react: reactPlugin,
      ['jsx-a11y']: jsxA11yPlugin,
    },
    extends: [
      ...compat.config(reactHooksPlugin.configs.recommended),
      ...compat.config(jsxA11yPlugin.configs.recommended),
    ],
    settings: {
      react: {
        version: "detect",
      },
      formComponents: ["Form"],
      linkComponents: [
        { name: "Link", linkAttribute: "to" },
        { name: "NavLink", linkAttribute: "to" },
      ],
      "import/resolver": {
        typescript: {},
      },
    },
  },
  {
    files: ["**/*.{ts,tsx}"],
    plugins: {
      import: importPlugin,
    },
    extends: [
      ...tseslint.configs.recommended,
      ...compat.config(importPlugin.configs.recommended),
      ...compat.config(importPlugin.configs.typescript),
    ],
    settings: {
      "import/internal-regex": "^~/",
      "import/resolver": {
        node: {
          extensions: [".ts", ".tsx"],
        },
        typescript: {
          alwaysTryTypes: true,
        },
      },
    },
  },
  {
    files: ['eslint.config.js'],
    languageOptions: {
      globals: {
        ...globals.node
      },
    },
  }
);
```
`npm run lint`で実行できることを確認してください。エラー用のファイルで想定していたエラーと`eslint.config.js`で`eslint-plugin-import`のエラーが表示されているはずです。
確認できたら、`tseslint`のインポート部分を以下のようにして再度表示されないことを確認したら移行完了です。
```js
import { config, configs } from 'typescript-eslint';
```

## おまけ
### VSCodeを使っている方
[VSCodeのESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)ではFlat Configはまだ実験的に導入されているだけなので、利用している場合は`useFlatConfig`を`true`にすることを忘れないようにしてください。
```json:.vscode/settings.json
{
  "eslint.experimental.useFlatConfig": true,
}
```

### ルールをカスタマイズ
`remix`の初期設定は推奨されたまま使われていることが多かったので、明示的にルールを追加する方法を紹介します。
`typescript-eslint`の[`no-unused-vars`](https://typescript-eslint.io/rules/no-unused-vars/)で`_`だけ許可することを考えます。
その場合は`plugins`で`@typescript-eslint`を`typescript-eslint`の`plguin`と対応させて`rules`でそのスコープからルールを持ってきて内容を定義すると有効になります。
```js
import { config, configs, plugin } from 'typescript-eslint';
...
...
  {
    files: ["**/*.{ts,tsx}"],
    plugins: {
      ['@typescript-eslint']: plugin,
    },
    rules: {
      '@typescript-eslint/no-unused-vars': [
        'error',
        {
          argsIgnorePattern: "^_",
          varsIgnorePattern: "^_",
          caughtErrorsIgnorePattern: "^_",
          destructuredArrayIgnorePattern: "^_"
        },
      ],
    },
  },
```
`@typescript-eslint`ではない名前をスコープに定義できますが、`@typescript-eslint`で宣言することが[公式](https://typescript-eslint.io/packages/typescript-eslint#advanced-usage)で推奨されています。