# Astro プロジェクトの ESLint Flat Config への移行


ESLint v9 より旧 `.eslintrc.*` ファイルのデフォルトでのサポートが廃止され、Flat Config がデフォルトとなった。この記事では、Astro プロジェクトの ESLint 設定を Flat Config に移行した際の手順を紹介する。

https://eslint.org/blog/2024/04/eslint-v9.0.0-released/

## 旧コンフィグの内容を整理する

移行前の設定ファイル:

```js title=.eslintrc.cjs showLineNumbers
// @ts-check
const { defineConfig } = require("eslint-define-config")

module.exports = defineConfig({
  root: true,
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:astro/recommended",
    "prettier",
  ],
  env: {
    browser: true,
    node: true,
  },
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module",
  },
  rules: {
    "no-undef": "off",
  },
  plugins: ["@typescript-eslint"],
  ignorePatterns: ["node_modules", "dist"],
  overrides: [
    {
      // Define the configuration for `.astro` file.
      files: ["*.astro"],
      // Allows Astro components to be parsed.
      parser: "astro-eslint-parser",
      // Parse the script in `.astro` as TypeScript by adding the following configuration.
      // It's the setting you need when using TypeScript.
      parserOptions: {
        parser: "@typescript-eslint/parser",
        extraFileExtensions: [".astro"],
      },
      rules: {
        // override/add rules settings here, such as:
        // "astro/no-set-html-directive": "error"
      },
    },
    {
      files: "*.cjs",
      rules: {
        "@typescript-eslint/no-var-requires": "off",
      },
    },
  ],
})
```

まずは要らなそうな設定項目を削除する。Common JS は流石にもう書かないので、その設定を削除する。

```js title=.eslintrc.cjs showLineNumbers diff
  // @ts-check
  const { defineConfig } = require("eslint-define-config")

  module.exports = defineConfig({
    root: true,
    extends: [
      "eslint:recommended",
      "plugin:@typescript-eslint/recommended",
      "plugin:astro/recommended",
      "prettier",
    ],
    env: {
      browser: true,
      node: true,
    },
    parser: "@typescript-eslint/parser",
    parserOptions: {
      ecmaVersion: "latest",
      sourceType: "module",
    },
    rules: {
      "no-undef": "off",
    },
    plugins: ["@typescript-eslint"],
    ignorePatterns: ["node_modules", "dist"],
    overrides: [
      {
        // Define the configuration for `.astro` file.
        files: ["*.astro"],
        // Allows Astro components to be parsed.
        parser: "astro-eslint-parser",
        // Parse the script in `.astro` as TypeScript by adding the following configuration.
        // It's the setting you need when using TypeScript.
        parserOptions: {
          parser: "@typescript-eslint/parser",
          extraFileExtensions: [".astro"],
        },
        rules: {
          // override/add rules settings here, such as:
          // "astro/no-set-html-directive": "error"
        },
      },
-     {
-       files: "*.cjs",
-       rules: {
-         "@typescript-eslint/no-var-requires": "off",
-       },
-     },
    ],
  })
```

## Flat Config への移行

続いて、実際に Flat Config への移行を行う。Flat Config では設定ファイル名が `eslint.config.js` に変更されたので、新たに `eslint.config.js` を作成する。

```sh
touch eslint.config.js
```

### typescript-eslint の移行

まず、最も重要そうな `typescript-eslint` の設定を移行する。

[公式ドキュメントの移行ガイド](https://typescript-eslint.io/packages/typescript-eslint/#migrating-from-legacy-config-setups) に従って、`typescript-eslint` を新たに導入する。

```sh npm2yarn
npm install typescript-eslint
```

旧コンフィグで使っていた、`@typescript-eslint/parser``@typescript-eslint/eslint-plugin` を削除する。

```sh npm2yarn
npm uninstall @typescript-eslint/parser @typescript-eslint/eslint-plugin
```

`eslint.config.js` に typescript-eslint の設定を移行する。

```js title=eslint.config.js showLineNumbers
// @ts-check

import eslint from "@eslint/js"
import tsEslint from "typescript-eslint"

export default tsEslint.config(
  eslint.configs.recommended,
  ...tsEslint.configs.recommended
)
```

https://typescript-eslint.io/packages/typescript-eslint/#migrating-from-legacy-config-setups

### eslint-plugin-astro の移行

`eslint-plugin-astro` の設定を移行する。

```sh npm2yarn
npm install --save-dev eslint-plugin-astro
```

```js title=eslint.config.js showLineNumbers diff
  // @ts-check

  import eslint from "@eslint/js"
+ import eslintPluginAstro from "eslint-plugin-astro"
  import tsEslint from "typescript-eslint"

  export default tsEslint.config(
    eslint.configs.recommended,
    ...tsEslint.configs.recommended,
+   ...eslintPluginAstro.configs["flat/recommended"],
+   ...eslintPluginAstro.configs["flat/jsx-a11y-strict"]
  )
```

https://github.com/ota-meshi/eslint-plugin-astro

### eslint-config-prettier の移行

`eslint-config-prettier` の設定を移行する。

```sh npm2yarn
npm install --save-dev eslint-config-prettier
```

```js title=eslint.config.js showLineNumbers diff
  // @ts-check

  import eslint from "@eslint/js"
  import eslintPluginAstro from "eslint-plugin-astro"
+ import eslintConfigPrettier from "eslint-config-prettier"
  import tsEslint from "typescript-eslint"

  export default tsEslint.config(
    eslint.configs.recommended,
    ...tsEslint.configs.recommended,
    ...eslintPluginAstro.configs["flat/recommended"],
    ...eslintPluginAstro.configs["flat/jsx-a11y-strict"],
+   eslintConfigPrettier
  )
```

https://github.com/prettier/eslint-config-prettier

### .gitignore の内容を ESLint でも無視する

`.gitignore` の内容を ESLint でも無視するように設定する。

CLI オプションの `--ignore-path` は廃止されたので、`eslint.config.js``ignores` フィールドに無視するパスを追加する必要がある。しかし、これを手動で行うのは面倒なので、[`eslint-config-flat-gitignore`](https://www.npmjs.com/package/eslint-config-flat-gitignore) を導入する。

```sh npm2yarn
npm install --save-dev eslint-config-flat-gitignore
```

```js title=eslint.config.js showLineNumbers diff
  // @ts-check

  import eslint from "@eslint/js"
+ import gitignore from "eslint-config-flat-gitignore"
  import eslintConfigPrettier from "eslint-config-prettier"
  import eslintPluginAstro from "eslint-plugin-astro"
  import tsEslint from "typescript-eslint"

  export default tsEslint.config(
+   gitignore(),
    eslint.configs.recommended,
    ...tsEslint.configs.recommended,
    ...eslintPluginAstro.configs["flat/recommended"],
    ...eslintPluginAstro.configs["flat/jsx-a11y-strict"],
    eslintConfigPrettier
  )
```

`gitignore()` は一番最初に読み込むことが推奨されているので、他の設定よりも前に記述する。

https://github.com/antfu/eslint-config-flat-gitignore

### グローバル変数の設定の移行

Flat Configでは、`env` フィールドが廃止され、`globals` フィールドでのみグローバル変数の設定が可能となった。これに伴い、`env` フィールドで設定していたランタイム依存のグローバル変数を `globals` フィールドに移行する。ただし、ランタイム依存のグローバル変数を全て手動で書くのは不可能なので、[`globals`](https://github.com/sindresorhus/globals) パッケージ を利用する。

```sh npm2yarn
npm install --save-dev globals
```

```js title=eslint.config.js showLineNumbers diff
  // @ts-check

  import eslint from "@eslint/js"
  import gitignore from "eslint-config-flat-gitignore"
  import eslintConfigPrettier from "eslint-config-prettier"
  import eslintPluginAstro from "eslint-plugin-astro"
+ import globals from "globals"
  import tsEslint from "typescript-eslint"

  export default tsEslint.config(
    gitignore(),
    eslint.configs.recommended,
    ...tsEslint.configs.recommended,
    ...eslintPluginAstro.configs["flat/recommended"],
    ...eslintPluginAstro.configs["flat/jsx-a11y-strict"],
    eslintConfigPrettier,
+   {
+     languageOptions: {
+       globals: {
+         ...globals.browser,
+         ...globals.node,
+       },
+     },
+   }
  )
```

ここでは、ブラウザと Node.js 依存のグローバル変数を設定している。

https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options

https://github.com/sindresorhus/globals

### TypeScript ファイルで `no-undef` を無効化する

未定義変数のチェックは TypeScript 側で行うため、TypeScript ファイルでは ESLint の `no-undef` ルールは無効化する。

> We strongly recommend that you do not use the no-undef lint rule on TypeScript projects. The checks it provides are already provided by TypeScript without the need for configuration - TypeScript just does this significantly better.
>
> \- [TypeScript ESLint](https://typescript-eslint.io/troubleshooting/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors)

```js title=eslint.config.js showLineNumbers diff
  // @ts-check

  import eslint from "@eslint/js"
  import gitignore from "eslint-config-flat-gitignore"
  import eslintConfigPrettier from "eslint-config-prettier"
  import eslintPluginAstro from "eslint-plugin-astro"
  import globals from "globals"
  import tsEslint from "typescript-eslint"

  export default tsEslint.config(
    gitignore(),
    eslint.configs.recommended,
    ...tsEslint.configs.recommended,
+   {
+     files: ["**/*.{ts,tsx,mts,cts,astro}"],
+     rules: {
+       "no-undef": "off",
+     },
+   },
    ...eslintPluginAstro.configs["flat/recommended"],
    ...eslintPluginAstro.configs["flat/jsx-a11y-strict"],
    eslintConfigPrettier,
    {
      languageOptions: {
        globals: {
          ...globals.browser,
          ...globals.node,
        },
      },
    }
  )
```

https://typescript-eslint.io/troubleshooting/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors

## ESLint を実行するスクリプトの変更

`package.json``lint` スクリプトを変更する。

```json title=package.json showLineNumbers diff
  {
    "scripts": {
-     "lint": "eslint --ext '.js,.cjs,mjs,.ts,.jsx,.tsx,.astro' .",
-     "lint:fix": "eslint --ext '.js,.cjs,.mjs,.ts,.jsx,.tsx,.astro' --fix .",
+     "lint": "eslint .",
+     "lint:fix": "eslint --fix .",
    }
  }
```

## VSCode の設定

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

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

## おまけ(CommonJS から ESM への移行)

今回移行したプロジェクトでは、prettier等の設定ファイルを CommonJS で記述していた。しかし、今回の Flat Config への移行に伴い ESLint での CommonJS に対するコンフィグは削除した他、流石にもうコンフィグといえど CommonJS は書きたくないので ESM に移行する。

ついでに、ESLint のコンフィグファイルの名前が `eslint.config.js` に変更されたのに合わせて、他のコンフィグファイルも `.hogerc.cjs` から `hoge.config.js` に名前を変更する。

```js title=lint-staged.config.js showLineNumbers diff
  // @ts-check

  /** @type {import("lint-staged").Config} */
- module.exports = {
+ export default {
    "*.{js,cjs,ts,jsx,tsx,astro}": ["eslint --fix", "prettier --write"],
    "*.{md,html,json,yaml,yml}": ["prettier --write"],
  }
```

```js title=prettier.config.js showLineNumbers diff
  // @ts-check
  
  /** @type {import("prettier").Options} */
- module.exports = {
+ export default {
    printWidth: 80,
    tabWidth: 2,
    plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
    semi: false,
    trailingComma: "es5",
    overrides: [
      {
        files: "*.astro",
        options: {
          parser: "astro",
        },
      },
    ],
  }
```

```ts title=tailwind.config.ts showLineNumbers diff
- const { addDynamicIconSelectors } = require("@iconify/tailwind")
- const defaultTheem = require("tailwindcss/defaultTheme")
+ import { addDynamicIconSelectors } from "@iconify/tailwind"
+ import type { Config } from "tailwindcss"
+ import defaultTheme from "tailwindcss/defaultTheme"

- /** @type {import('tailwindcss').Config} */
- module.exports = {
+ export default {
    darkMode: ["class"],
    content: ["./src/**/*.{astro,js,jsx,ts,tsx,html,mdx}"],
    theme: {
      container: {
        center: true,
        padding: "2rem",
        screens: {
          "2xl": "1400px",
        },
      },
      extend: {
        colors: {
          border: "hsl(var(--border))",
          input: "hsl(var(--input))",
          ring: "hsl(var(--ring))",
          background: "hsl(var(--background))",
          foreground: "hsl(var(--foreground))",
          primary: {
            DEFAULT: "hsl(var(--primary))",
            foreground: "hsl(var(--primary-foreground))",
          },
          secondary: {
            DEFAULT: "hsl(var(--secondary))",
            foreground: "hsl(var(--secondary-foreground))",
          },
          destructive: {
            DEFAULT: "hsl(var(--destructive))",
            foreground: "hsl(var(--destructive-foreground))",
          },
          muted: {
            DEFAULT: "hsl(var(--muted))",
            foreground: "hsl(var(--muted-foreground))",
          },
          accent: {
            DEFAULT: "hsl(var(--accent))",
            foreground: "hsl(var(--accent-foreground))",
          },
          popover: {
            DEFAULT: "hsl(var(--popover))",
            foreground: "hsl(var(--popover-foreground))",
          },
          card: {
            DEFAULT: "hsl(var(--card))",
            foreground: "hsl(var(--card-foreground))",
          },
          stone: {
            1: "rgb(var(--stone-1) / <alpha-value>)",
            2: "rgb(var(--stone-2) / <alpha-value>)",
            3: "rgb(var(--stone-3) / <alpha-value>)",
            4: "rgb(var(--stone-4) / <alpha-value>)",
            5: "rgb(var(--stone-5) / <alpha-value>)",
            6: "rgb(var(--stone-6) / <alpha-value>)",
            7: "rgb(var(--stone-7) / <alpha-value>)",
            8: "rgb(var(--stone-8) / <alpha-value>)",
            9: "rgb(var(--stone-9) / <alpha-value>)",
            10: "rgb(var(--stone-10) / <alpha-value>)",
          },
        },
        borderRadius: {
          lg: "var(--radius)",
          md: "calc(var(--radius) - 2px)",
          sm: "calc(var(--radius) - 4px)",
        },
        keyframes: {
          "accordion-down": {
-           from: { height: 0 },
+           from: { height: "0" },
            to: { height: "var(--radix-accordion-content-height)" },
          },
          "accordion-up": {
            from: { height: "var(--radix-accordion-content-height)" },
-           to: { height: 0 },
+           to: { height: "0" },
          },
        },
        animation: {
          "accordion-down": "accordion-down 0.2s ease-out",
          "accordion-up": "accordion-up 0.2s ease-out",
        },
        fontFamily: {
          mono: ["UDEV Gothic LG", ...defaultTheem.fontFamily.mono],
          times: ["Times New Roman", "Times", "serif"],
        },
        fontSize: {
          "4.5xl": "2.5rem",
        },
        typography: {
          DEFAULT: {
            css: {
              "blockquote p:first-of-type::before": { content: "none" },
              "blockquote p:first-of-type::after": { content: "none" },
            },
          },
        },
      },
    },
    plugins: [
      require("tailwindcss-animate"),
      require("@tailwindcss/typography"),
      require("@tailwindcss/container-queries"),
      addDynamicIconSelectors(),
    ],
- }
+ } satisfies Config
```

## おわりに

Flat Config では、設定ファイルが `.eslintrc.*` から `eslint.config.js` に変更され、設定の書き方も大幅に変更された。移行には手間がかかるが、新しい設定ファイルの書き方はより柔軟で、設定の共有が容易になった。設定ファイルが JS のみになったため、旧コンフィグでの `extends: ["eslint:recommended"]` といった文字列での指定は無くなり、`eslint.configs.recommended` のようにオブジェクトで指定するようになった。これにより、直感的に設定を追加・削除できるようになった。

少し前までは多くのプラグインが Flat Config に未対応であり移行は大変苦しかった。しかし現在では typescript-eslint など主要なプラグインのほとんどが対応しているため、移行はかなり容易になっている。

いつかは移行せざるを得ないので、早めに移行しておくことをおすすめする。

GitHub リポジトリ:

https://github.com/r4ai/r4ai.dev

完成した `eslint.config.js`

https://github.com/r4ai/r4ai.dev/blob/117336e86ce32ecbd61f544bb31f0699c30d2041/eslint.config.js

## 参考文献

https://github.com/search?q=path%3Aeslint.config.js+eslint-plugin-astro+typescript-eslint&type=code&p=1

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

https://typescript-eslint.io/packages/typescript-eslint/#migrating-from-legacy-config-setups

https://github.com/ota-meshi/eslint-plugin-astro

https://github.com/prettier/eslint-config-prettier