📖

Next.js で eslint の flat config を設定してみる

2024/01/08に公開

Next.js で新しくプロジェクトを立ち上げるにあたり、どうせなら flat config を試してみたいと思ったのですが、結構たいへんだったのでその備忘録になります。

結論、今はまだ Next.js で flat config を使うのは待ったほうが良いと思います。(Next.js の対応を待ちましょう)

はじめに

執筆時点の最新である Next.js v14.0.4 の package.json に記載されている eslint は ^8 となっており、デフォルトで flat config は導入されていないため、手作業で flat config を導入する必要があります。

以下の discussions が参考になりました。
https://github.com/vercel/next.js/discussions/49337

将来 Next.js で flat config が正式にサポートされた場合にこの記事で設定した内容が機能しなくなる可能性があるため、特別な理由がない限り flat config が正式にサポートされるのを待つことをおすすめします。

また、flat config の具体的な内容には触れないので、詳しくは公式docを参照ください。

バージョン

本記事執筆時に試したバージョンは以下の通りです。

  • Next.js: v14.0.4
  • eslint: v8.56.0

リポジトリ

本記事執筆で試したコードは以下のリポジトリに push してあります。

https://github.com/hiroki-sato-workman/nextjs-eslint-flat-config-sample/

ESM で試したサンプル

https://github.com/hiroki-sato-workman/nextjs-eslint-flat-config-sample/tree/with-esm

NextJs のプロジェクトを作成

詳しくは以下の公式docを参照してください。今回は tailwind 等は使わないのでその他はデフォルトで進めます。

npx create-next-app@latest

eslint を flat config にする

flat config では extends が廃止されたため next/core-web-vitals に含まれるモジュールや設定を手動で導入する必要があります。

eslint.config.js の作成

flat config では今までの .eslintrc は廃止され eslint.config.js に統一され、またプロジェクトのルートに配置する必要があります。

# .eslintrc.json を削除して eslint.config.js を追加
rm .eslintrc.json
touch eslint.config.js

関連モジュールのインストール

以下のモジュールをインストールします。

next/core-web-vitals が読み込んでいたモジュール郡

  • @next/eslint-plugin-next
  • eslint-plugin-react
  • eslint-plugin-react-hooks

typescript の parser(ないとエラーになるので)

  • @typescript-eslint/parser
npm i -D @next/eslint-plugin-next eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/parser

eslint.config.js を記述

const nextPlugin = require('@next/eslint-plugin-next');
const reactPlugin = require('eslint-plugin-react');
const hooksPlugin = require('eslint-plugin-react-hooks');
const typescriptParser = require('@typescript-eslint/parser');

module.exports = [
  {
    languageOptions: {
      parser: typescriptParser
    },
    files: ['**/*.ts', '**/*.tsx'],
    plugins: {
      react: reactPlugin,
      'react-hooks': hooksPlugin,
      '@next/next': nextPlugin,
    },
    rules: {
      ...reactPlugin.configs['jsx-runtime'].rules,
      ...hooksPlugin.configs.recommended.rules,
      ...nextPlugin.configs.recommended.rules,
      ...nextPlugin.configs['core-web-vitals'].rules,
      '@next/next/no-img-element': 'error',
    },
  },
  {
    ignores: ['./.next/*'],
  },
];

eslint 実行コマンドの変更

package.json の scripts にデフォルトで記載されている lint コマンドは next lint となっています。
next lint は flat config に対応していないため、eslint コマンドに変更します。

package.json
"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
- "lint": "next lint"
+ "lint": "eslint './**/*.{ts,tsx}'"
},

vscode の設定

vscode はデフォルトで flat config に対応していないため、.vscode/settings.json に以下の設定を追加します。

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

ちなみに私は webstorm 派なのですが、設定が足りないのかうまく機能しませんでした 🥲

動作確認

next/core-web-vitals のデフォルトでは @next/next/inline-script-id がエラーになるので、app/page.tsx でエラーになるように変更します。

app/page.tsx
export default function Home() {
  return (
    <main className={styles.main}>
+     <Script>{`console.log('Hello world!');`}</Script>
      <div className={styles.description}>
        <p>
          Get started by editing&nbsp;
          <code className={styles.code}>app/page.tsx</code>
        </p>

npm run lint コマンドを実行してエラーになることを確認します。

独自のルールを追加(変更)する

@next/next/inline-script-id を無効にするルールを追加してみます。
後ろに追加したルールが優先されるので注意して追加します。

eslint.config.js
const nextPlugin = require('@next/eslint-plugin-next');
const reactPlugin = require('eslint-plugin-react');
const hooksPlugin = require('eslint-plugin-react-hooks');
const typescriptParser = require('@typescript-eslint/parser');

+const myRules = {
+  '@next/next/inline-script-id': 'off',
+}

module.exports = [
  {
    languageOptions: {
      parser: typescriptParser
    },
    files: ['**/*.ts', '**/*.tsx'],
    plugins: {
      react: reactPlugin,
      'react-hooks': hooksPlugin,
      '@next/next': nextPlugin,
    },
    rules: {
      ...reactPlugin.configs['jsx-runtime'].rules,
      ...hooksPlugin.configs.recommended.rules,
      ...nextPlugin.configs.recommended.rules,
      ...nextPlugin.configs['core-web-vitals'].rules,
      '@next/next/no-img-element': 'error',
+     ...myRules,
    },
  },
  {
    ignores: ['./.next/*'],
  },
];

npm run lint コマンドを実行してエラーが消えることを確認します。

通常の js のように設定を追加できるので今までと比べると変更しやすく設定も見やすくなりそうですね 😃

[おまけ] flat config(eslint.config.js) を ESM で書く

単純に ESM に置き換えると SyntaxError: Cannot use import statement outside a module となるため、各種設定の変更が必要になります。

以下の記事を参考にさせていただきました 🙇

https://qiita.com/Shilaca/items/c494e4dc6b536a5231de#commonjs-なプロジェクトでも-es-module-形式で書きたいsyntaxerror-cannot-use-import-statement-outside-a-module-が出る

以下の設定をすることにより ESM で設定ファイルを記載できます。

  1. eslint.config.jseslint.config.mjs に変更

  2. eslint.config.mjs を ESM の記載方法に変更

    eslint.config.mjs
    -const nextPlugin = require('@next/eslint-plugin-next');
    -const reactPlugin = require('eslint-plugin-react');
    -const hooksPlugin = require('eslint-plugin-react-hooks');
    -const typescriptParser = require('@typescript-eslint/parser');
    +import nextPlugin from '@next/eslint-plugin-next';
    +import reactPlugin from 'eslint-plugin-react';
    +import hooksPlugin from 'eslint-plugin-react-hooks';
    +import typescriptParser from '@typescript-eslint/parser'
    
    -module.exports = [
    +export default [
    
    
  3. eslint 実行コマンドを修正

    package.json
    "scripts": {
      "dev": "next dev",
      "build": "next build",
      "start": "next start",
    - "lint": "eslint './**/*.{ts,tsx}'"
    + "lint": "ESLINT_USE_FLAT_CONFIG=true eslint -c eslint.config.mjs './**/*.{ts,tsx}'"
    },
    
  4. vscode + flst config はデフォルトで eslint.config.js を参照するようになっているので設定を変更します

    .vscode/settings.json
    {
      "eslint.experimental.useFlatConfig": true,
    + "eslint.options": {
    +   "overrideConfigFile": "eslint.config.mjs"
    + }
    }
    

余談

今更 CommonJS で書きたくないので諸々デフォルトで ESM に対応してほしい。

ESM で書くとファイル名が eslint 公式が言っているファイルから変わってしまうかつ設定も少し複雑になるので、個人的には現状は CommonJS で書いておくのが良いと思います。(eslint 公式のサンプルは ESM なのでそこは違うけど。。)

Discussion