Open14

Next.js 15のCompatを使わないFlat Config移行

mrskiromrskiro

eslint-config-next

https://github.com/vercel/next.js/issues/71763

pnpm create next-app@canaryの成果物でもFlatCompatが使われている

eslint.config.mjs
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

const eslintConfig = [
  ...compat.extends("next/core-web-vitals", "next/typescript"),
];

export default eslintConfig;

コメントにあるように、@next/eslint-plugin-nextから直接pluginの指定をすればFlatCompatを回避できる

import pluginNext from "@next/eslint-plugin-next";

/** @type {import("eslint").Linter.Config[]} */
export default [
    // eslint-plugin-next
  {
    files: ["**/*.{ts,tsx}"],
    plugins: {
      "@next/next": pluginNext,
    },
    rules: {
      ...pluginNext.configs.recommended.rules,
      ...pluginNext.configs["core-web-vitals"].rules,
    },
  },
]

依存パッケージはeslint-config-nextを消して@next/eslint-plugin-nextだけでいい

eslint-config-nextは以下を内包してるのでこれらを別途設定していく

  • eslint-plugin-react
  • eslint-plugin-import
  • eslint-plugin-jsx-a11y
mrskiromrskiro

typescript-eslint

TypeScriptの解析ができないのでtypescript-eslintを先頭に追加する

ドキュメントではtseslint.config
使用しているが型はTSDocで推論されるし中身を見ても特別必要性を感じなかったので使ってない

https://github.com/typescript-eslint/typescript-eslint/blob/cb0ef9ceb73057bc90fa54229e2d4828432530b5/packages/typescript-eslint/src/config-helper.ts#L89-L141

{...tseslint.configs.recommended}とすると3つの配列が展開されparser周り通常eslintのoverride@typescript-eslint周りのルール追加がされる。

極力どんな設定が何によってされているかを設定ファイルから知れるようにしておきたいので自分はそれぞれ手動で設定した。recommendedが配列の末尾指定になるのが微妙

import tseslint from "typescript-eslint";

/** @type {import("eslint").Linter.Config[]} */
export default [
    // typescript-eslint
  ...tseslint.configs.recommended
  // or
  {
    name: tseslint.configs.base.name,
    languageOptions: tseslint.configs.base.languageOptions,
    plugins: tseslint.configs.base.plugins,
    rules: {
      ...tseslint.configs.eslintRecommended.rules,
      // https://github.com/typescript-eslint/typescript-eslint/blob/cb0ef9ceb73057bc90fa54229e2d4828432530b5/packages/eslint-plugin/src/configs/recommended.ts
      ...tseslint.configs.recommended.at(-1).rules,
     // custom rules...
    },
  },
    // eslint-plugin-next
  {...},
]
mrskiromrskiro

eslint

通常eslintのrecommendedも設定
typescript-eslintより前に設定する

import eslint from "@eslint/js";

/** @type {import("eslint").Linter.Config[]} */
export default [
{
    rules: {
      ...eslint.configs.recommended.rules,
      // custom rules...
    },
  },
    // typescript-eslint
  {...},
    // eslint-plugin-next
  {...},
]
mrskiromrskiro

テストは都度print-configで行う
npx eslint --print-config src/xxx

mrskiromrskiro

eslint-plugin-react

ドキュメントを参考にする
例によってreactPlugin.configs.flat.recommendedそれぞれのプロパティで指定している

ここでの工夫ポイントはeslint-config-nextで指定されていたreact関係のルールを設定すること
不要なら設定しなくてよい
https://github.com/vercel/next.js/blob/f348241e25767e1e08909de19cdab032d63e369b/packages/eslint-config-next/index.js#L64-L79

import pluginReact from "eslint-plugin-react";

/** @type {import("eslint").Linter.Config[]} */
export default [
  // eslint
  {...},
  // typescript-eslint
  {...},
  // eslint-plugin-react
  {
    files: ["**/*.tsx"],
    plugins: pluginReact.configs.flat.recommended.plugins,
    languageOptions: pluginReact.configs.flat.recommended.languageOptions,
    rules: {
      ...pluginReact.configs.flat.recommended.rules,
      // custom rules...
      // https://github.com/vercel/next.js/blob/f348241e25767e1e08909de19cdab032d63e369b/packages/eslint-config-next/index.js#L62-L79
      "react/no-unknown-property": "off",
      "react/react-in-jsx-scope": "off",
      "react/prop-types": "off",
      "react/jsx-no-target-blank": "off",
    },
    settings: {
      react: {
        version: "detect",
      },
    },
  },
  // eslint-plugin-next
  {...},
]
mrskiromrskiro

eslint-plugin-react-hooks

現状の構成に入ってないことに気づいたので追加する

5.0.0でflat configの対応が入った
コメントを参考に設定する

import pluginReactHooks from "eslint-plugin-react-hooks";

/** @type {import("eslint").Linter.Config[]} */
export default [
  // eslint
  {...},
  // typescript-eslint
  {...},
  // eslint-plugin-react
  {...},
  // eslint-plugin-react-hooks
  {
    files: ["**/*.{ts,tsx}"],
    plugins: {
      "react-hooks": pluginReactHooks,
    },
    rules: {
      ...pluginReactHooks.configs.recommended.rules,
    },
  },
  // eslint-plugin-next
  {...},
]
mrskiromrskiro

eslint-plugin-jsx-a11y

ドキュメントを参考に設定する

例によってjsxA11y.flatConfigs.recommendedをそれぞれ指定する。

ここでもeslint-config-nextで設定されていたルールがあったので追加する
https://github.com/vercel/next.js/blob/f348241e25767e1e08909de19cdab032d63e369b/packages/eslint-config-next/index.js#L67-L78

import pluginJsxA11y from "eslint-plugin-jsx-a11y";

/** @type {import("eslint").Linter.Config[]} */
export default [
  // eslint
  {...},
  // typescript-eslint
  {...},
  // eslint-plugin-react
  {...},
  // eslint-plugin-react-hooks
  {...},
  // eslint-plugin-next
  {...},
  // eslint-plugin-jsx-a11y
  {
    files: ["**/*.{ts,tsx}"],
    plugins: pluginJsxA11y.flatConfigs.recommended.plugins,
    rules: {
      ...pluginJsxA11y.flatConfigs.recommended.rules,
      // https://github.com/vercel/next.js/blob/f348241e25767e1e08909de19cdab032d63e369b/packages/eslint-config-next/index.js#L67-L78
      "jsx-a11y/alt-text": [
        "warn",
        {
          elements: ["img"],
          img: ["Image"],
        },
      ],
      "jsx-a11y/aria-props": "warn",
      "jsx-a11y/aria-proptypes": "warn",
      "jsx-a11y/aria-unsupported-elements": "warn",
      "jsx-a11y/role-has-required-aria-props": "warn",
      "jsx-a11y/role-supports-aria-props": "warn",
    },
  },
]
mrskiromrskiro

eslint-plugin-import

ドキュメントを参考に設定する
例によって importPlugin.flatConfigs.recommendedをそれぞれ指定する。
ここでもeslint-config-nextの設定を追加する

https://github.com/vercel/next.js/blob/f348241e25767e1e08909de19cdab032d63e369b/packages/eslint-config-next/index.js#L63

eslint-import-resolver-typescriptを依存関係に追加しないとTypeScriptで機能しない。
ここら辺はeslint-config-nextがよしなにしていた

https://github.com/vercel/next.js/blob/f348241e25767e1e08909de19cdab032d63e369b/packages/eslint-config-next/index.js#L116-L124

import pluginImport from "eslint-plugin-import";

/** @type {import("eslint").Linter.Config[]} */
export default [
  // eslint
  {...},
  // typescript-eslint
  {...},
  // eslint-plugin-react
  {...},
  // eslint-plugin-react-hooks
  {...},
  // eslint-plugin-next
  {...},
  // eslint-plugin-jsx-a11y
  {...},
  // eslint-plugin-import
  {
    files: ["**/*.{ts,tsx}"],
    plugins: pluginImport.flatConfigs.recommended.plugins,
    rules: {
      ...pluginImport.flatConfigs.recommended.rules,
      // https://github.com/vercel/next.js/blob/f348241e25767e1e08909de19cdab032d63e369b/packages/eslint-config-next/index.js#L63
      "import/no-anonymous-default-export": "warn",
      // custom rules...
    },
    settings: {
      "import/resolver": {
        typescript: true,
      },
    },
  },
]
mrskiromrskiro

eslint-plugin-storybook

対象ファイルごとにオブジェクトが分かれているのでここでは...pluginStorybook.configs["flat/recommended"]とした
https://github.com/storybookjs/eslint-plugin-storybook/blob/6202eb23901fc249758cd225f8a10b3032d6a9b8/lib/configs/flat/recommended.ts

import pluginStorybook from "eslint-plugin-storybook";

/** @type {import("eslint").Linter.Config[]} */
export default [
  // eslint
  {...},
  // typescript-eslint
  {...},
  // eslint-plugin-react
  {...},
  // eslint-plugin-react-hooks
  {...},
  // eslint-plugin-next
  {...},
  // eslint-plugin-jsx-a11y
  {...},
  // eslint-plugin-import
  {...},
  // eslint-plugin-storybook
  // https://github.com/storybookjs/eslint-plugin-storybook/blob/6202eb23901fc249758cd225f8a10b3032d6a9b8/lib/configs/flat/recommended.ts
  ...pluginStorybook.configs["flat/recommended"],
]
mrskiromrskiro

eslint-plugin-nextの設定でjsmjs(eslint.configの拡張子)をfilesに設定しないとそれぞれerror とwarningが出る

設定ファイルの拡張子を追加しないとnextがプラグインの存在を見つけられずThe Next.js plugin was not detected in your ESLint configuration. See https://nextjs.org/docs/app/api-reference/config/eslint#migrating-existing-configというwarningが報告され続ける

https://github.com/vercel/next.js/issues/73655
issueたてました

// eslint-plugin-next
  {
    // include js because of reporting `Key "rules": Key "@next/next/no-html-link-for-pages": Could not find plugin "@next/next".`
    // include mjs because of https://github.com/vercel/next.js/issues/73655
    files: ["**/*.{js,mjs,ts,tsx}"],
    plugins: {
      "@next/next": pluginNext,
    },
    rules: {
      ...pluginNext.configs.recommended.rules,
      ...pluginNext.configs["core-web-vitals"].rules,
    },
  },