🥺

メモ:ESLint Flat Config Migration w/ Next.js 15

2025/01/08に公開
Changelog
  • 2025/01/20
    • eslint-plugin-react-hooksがFlat Configに対応しました
  • 2025/01/23
    • カスタムコンフィグ/ルールを正しく定義(files, ignores, etc.)

重い腰を上げて Next.js v15 と Flat Config の対応をした際のメモです。

プロジェクト構成

Packages

npm i -D eslint @eslint/js typescript typescript-eslint \
@eslint/eslintrc @eslint/compat @next/eslint-plugin-next \
eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y \
@vitest/eslint-plugin eslint-plugin-jest-dom eslint-plugin-testing-library eslint-plugin-storybook \
eslint-plugin-import eslint-plugin-simple-import-sort eslint-plugin-unused-imports \
prettier eslint-config-prettier prettier-plugin-tailwindcss
stylelint.config.mjs
stylelint.config.mjs
/** @type {import("stylelint").Config} */
const config = {
  extends: [
    "stylelint-config-standard",
    "stylelint-config-standard-scss",
    "stylelint-config-recess-order",
  ],
  plugins: ["stylelint-scss"],
  rules: {
    "at-rule-no-unknown": null,
    "scss/at-rule-no-unknown": [
      true,
      {
        ignoreAtRules: ["tailwind"],
      },
    ],
  },
};

export default config;
prettier.config.mjs
prettier.config.mjs
/** @type {import("prettier").Config} */
const config = {
  endOfLine: "lf",
  printWidth: 80,
  tabWidth: 2,
  useTabs: false,
  semi: true,
  singleQuote: false,
  quoteProps: "as-needed",
  jsxSingleQuote: false,
  trailingComma: "es5",
  bracketSpacing: true,
  bracketSameLine: false,
  arrowParens: "always",
  plugins: ["prettier-plugin-tailwindcss"],
};

export default config;
.vscode
.vscode/extensions.json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "stylelint.vscode-stylelint",
    "esbenp.prettier-vscode"
  ]
}
.vscode/settings.json
{
  "files.eol": "\n",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.fixAll.stylelint": "explicit",
    "source.organizeImports": "never"
  },
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.tabSize": 2,
    "editor.insertSpaces": true
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.tabSize": 2,
    "editor.insertSpaces": true
  },
  "[typescript]": {
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.tabSize": 2,
  "editor.insertSpaces": true
  },
  "[typescriptreact]": {
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.tabSize": 2,
  "editor.insertSpaces": true
  },
  "css.validate": false,
  "scss.validate": false,
  "stylelint.validate": ["css", "scss"],
  "[css]": {
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.tabSize": 2,
  "editor.insertSpaces": true
  },
  "[scss]": {
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.tabSize": 2,
  "editor.insertSpaces": true
  }
}
next.config.ts
next.config.ts
import type { NextConfig } from "next";
import { dirname, join } from "path";
import { fileURLToPath } from "url";

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

const config: NextConfig = {
  /** @see {@link https://nextjs.org/docs/app/building-your-application/styling/sass} */
  sassOptions: {
    implementation: "sass-embedded",
    includePaths: [join(__dirname, "src/styles")],
  },
};

export default config;

Flat Config

対応する Legacy Config (v8) をコメントに残しました。

eslint.config.mjs
// @ts-check

import { fixupConfigRules } from "@eslint/compat";
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import vitest from "@vitest/eslint-plugin";
import prettier from "eslint-config-prettier";
import { flatConfigs as importflatConfigs } from "eslint-plugin-import";
import jestDom from "eslint-plugin-jest-dom";
import jsxA11y from "eslint-plugin-jsx-a11y";
import react from "eslint-plugin-react";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import storybook from "eslint-plugin-storybook";
import testingLibrary from "eslint-plugin-testing-library";
import unusedImports from "eslint-plugin-unused-imports";
import { dirname } from "path";
import ts from "typescript-eslint";
import { fileURLToPath } from "url";

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

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

const config = ts.config(
  {
    files: ["**/*.{js,jsx,ts,tsx}", "**/*.{cjs,mjs,cts,mts}"],
  },
  {
    ignores: [".next/"], // + Default ignores ["**/node_modules/", ".git/"]
  },
  // plugin:@next/next/recommended
  // Do not use `next/core-web-vitals` because it has duplicate rules with `plugin:react/recommended`, `plugin:react-hooks/recommended`, etc.
  ...fixupConfigRules(compat.extends("plugin:@next/next/recommended")), // Replace "plugin:" syntax when flat config is supported
  // eslint:recommended
  js.configs.recommended,
  // plugin:@typescript-eslint/recommended
  ...ts.configs.recommended,
  // plugin:react/recommended
  react.configs.flat?.recommended ?? {},
  // plugin:react/jsx-runtime
  react.configs.flat?.["jsx-runtime"] ?? {},
  {
    settings: {
      react: {
        version: "detect",
      },
    },
  },
  // plugin:react-hooks/recommended
  ...fixupConfigRules(compat.extends("plugin:react-hooks/recommended")), // Replace "plugin:" syntax when flat config is supported
  // plugin:jsx-a11y/recommended
  jsxA11y.flatConfigs.recommended,
  // plugin:@vitest/recommended
  vitest.configs.recommended,
  // plugin:jest-dom/recommended
  jestDom.configs["flat/recommended"],
  // plugin:testing-library/react
  testingLibrary.configs["flat/react"],
  // plugin:storybook/recommended
  ...storybook.configs["flat/recommended"],
  // plugins: ["import"] - Custom configuration
  {
    plugins: {
      import:
        importflatConfigs.recommended.plugins
          .import /** @see createFlatConfig */,
    },
  },
  // plugins: ["simple-import-sort"] - Recommended configuration
  {
    plugins: {
      "simple-import-sort": simpleImportSort,
    },
    rules: {
      "simple-import-sort/imports": "warn",
      "simple-import-sort/exports": "warn",
      "import/first": "warn",
      "import/newline-after-import": "warn",
      "import/no-duplicates": "warn",
    },
    settings: {
      "import/internal-regex": "^@/",
    },
  },
  // plugins: ["unused-imports"] - Recommended configuration
  {
    plugins: {
      "unused-imports": unusedImports,
    },
    rules: {
      "no-unused-vars": "off",
      "@typescript-eslint/no-unused-vars": "off",
      "unused-imports/no-unused-imports": "warn",
      "unused-imports/no-unused-vars": [
        "warn",
        {
          vars: "all",
          varsIgnorePattern: "^_",
          args: "after-used",
          argsIgnorePattern: "^_",
        },
      ],
    },
  },
  // prettier - Disable eslint rules that conflict with prettier.
  prettier,
  // Custom rules
  {
    rules: {
      "@typescript-eslint/no-explicit-any": "warn",
      "@typescript-eslint/no-empty-object-type": [
        "error",
        {
          allowWithName: "Props$", // Allow empty object types with a name ending with "Props".
        },
      ],
    },
  }
);

export default config;

🥺

より一層、Biomeを使いたくなりました。

リファレンス

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

https://typescript-eslint.io/getting-started

https://nextjs.org/docs/app/api-reference/config/eslint#migrating-existing-config

https://github.com/jsx-eslint/eslint-plugin-react?tab=readme-ov-file#flat-configs

https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks

https://github.com/jsx-eslint/eslint-plugin-jsx-a11y?tab=readme-ov-file#usage---flat-config-eslintconfigjs

https://github.com/vitest-dev/eslint-plugin-vitest?tab=readme-ov-file#recommended

https://github.com/testing-library/eslint-plugin-jest-dom?tab=readme-ov-file#recommended-configuration

https://github.com/testing-library/eslint-plugin-testing-library?tab=readme-ov-file#react

https://github.com/storybookjs/eslint-plugin-storybook?tab=readme-ov-file#configuration-eslintconfigcmjs

https://github.com/import-js/eslint-plugin-import?tab=readme-ov-file#config---flat-eslintconfigjs

https://github.com/lydell/eslint-plugin-simple-import-sort?tab=readme-ov-file#usage

https://github.com/sweepline/eslint-plugin-unused-imports?tab=readme-ov-file#usage

https://github.com/prettier/eslint-config-prettier?tab=readme-ov-file#installation

https://zenn.dev/cybozu_frontend/articles/about-eslint-flat-config

https://zenn.dev/kazukix/articles/eslint-config-2024-09

https://eslint.org/blog/2024/04/eslint-config-inspector

追記(最新)

eslint-plugin-react-hooksがFlat Configをサポートしました。

https://github.com/facebook/react/pull/30774

アプリ開発サークル@IPUT

Discussion