📘

ESLintをv8系→v9系にバージョンアップした

に公開

ESLint v9 にバージョンアップした話(Nuxt)

はじめに

この記事では、Nuxt プロジェクトで ESLint を v8 系から v9 系にバージョンアップし、同時に @nuxt/eslint-config を新たに導入した際のポイント、注意点、導入手順についてまとめます。

ESLint v9 では設定形式が変わったことや、eslint.config.js への移行が求められるため、既存プロジェクトとの互換性に配慮した対応が必要です。加えて、今回のアップデートでは @nuxt/eslint-config を組み込むことで Nuxt 向けの推奨設定を効率的に取り込むことができました。


変更の背景

  • Nuxt 3 の開発環境でも最新 ESLint を使いたかった
  • @nuxt/eslint-config を新たに導入して、公式推奨の lint 設定を活用したかった

アップグレード手順(概要)

1. ESLint v9 にアップグレード

npm install -D eslint@9.24.0

※バージョン指定はお好みで!

2. @nuxt/eslint-config を導入

今回のアップデートで @nuxt/eslint-config を新たに追加しました。flat config に対応したバージョンを導入します。

npm install -D @nuxt/eslint-config@1.3.0

※バージョン指定はお好みで!

3. 旧 .eslintrc を削除 or 無効化

ESLint v9 では .eslintrc が無効になって不要になりますが、移行が完全に完了するまでは参照用として置いておきます。

4. eslint.config.mjs を新規作成

// eslint.config.mjs
import withNuxt from './.nuxt/eslint.config.mjs'

export default withNuxt(  
  {
    files: ["**/*.ts", "**/*.vue"],
    rules: {
      "no-console": "warn",
      ...etc
  },
)
  .override('nuxt/typescript/rules', {
    rules: {
      '@typescript-eslint/no-explicit-any': 'off',
      ...etc
    },
  })

ハマりポイントと対処法

✅ 1. eslint コマンドが通らない

課題1: --ignore-pathオプションが eslint.config.mjs に対応していない

対策1: eslint-config-flat-gitignore を使う

npm install -D eslint-config-flat-gitignore
  • eslint.config.mjs で .gitignore を読み込む設定を追加します:
// eslint.config.mjs

import withNuxt from './.nuxt/eslint.config.mjs'
// 追記
import gitignore from "eslint-config-flat-gitignore";

export default withNuxt(
  // 追記
  gitignore(),
  {
    files: ["**/*.ts", "**/*.vue"],
    rules: {
      "no-console": "warn",
      ...etc
  },
)
  .override('nuxt/typescript/rules', {
    rules: {
      '@typescript-eslint/no-explicit-any': 'off',
      ...etc
    },
  })

課題2: .nuxt ディレクトリに eslint.config.mjs が生成されておらず、ESLint がルールを読み込めない

対策2: npm run dev などを一度実行して .nuxt を生成しておく

  • .nuxt 内部に eslint.config.mjs が生成されていないとエラーになるので、.nuxt ファイルをbuildするコマンドを入れることで回避できました

✅ 2. Flat config への移行ツールが活用しきれなかった

ESLint v9 では flat config 形式になったことで設定ファイルの書き方が刷新されたため、これまで使っていた .eslintrc.js.eslintrc.json の内容をそのまま eslint.config.js に移植することができません。

eslint公式で移行手順は記載( https://eslint.org/docs/latest/use/configure/migration-guide )されており、npx@eslint/migrate-config .eslintrc といったコマンドで従来の .eslintrceslint.config.js(または .mjs)形式に変換することができます。しかし、自分たちのプロジェクトでは @nuxt/eslint-configwithNuxt を使用して設定をラップする方針で移行を進めているため、自動変換後の内容をそのまま適用することは難しく、各ルールを 1 つずつ確認・整理しながら移行を進める方針を取りました。

対策:rules の中身をひとつずつコピペ&確認

  • .eslintrc に記載していたルールをそのまま eslint.config.mjsrules に転記
  • プロジェクトのコードに対して一つずつ適用し、エラーが出るか確認しながら微調整

ファイル移行before/after

  • before(.eslintrc

    {
      "extends": [
        "@nuxtjs/eslint-config-typescript",
        "plugin:storybook/recommended"
      ],
      "rules": {
        "arrow-parens": 0,
        "comma-dangle": 0,
        "space-before-function-paren": 0,
        "vue/max-attributes-per-line": 0,
        "vue/multi-word-component-names": 0,
        "vue/singleline-html-element-content-newline": 0,
        "vue/require-default-prop": "error",
        "storybook/prefer-pascal-case": 0,
        "@typescript-eslint/no-unused-vars": [
          "error",
          {
            "argsIgnorePattern": "^_",
            "caughtErrorsIgnorePattern": "^_",
            "destructuredArrayIgnorePattern": "^_",
            "varsIgnorePattern": "^_"
          }
        ],
        "import/order": [
          "error",
          {
            "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"],
            "newlines-between": "always"
          }
        ],
        "@typescript-eslint/consistent-type-imports": "error",
        "@typescript-eslint/naming-convention": [
          "error",
          {
            "selector": "typeAlias",
            "format": ["PascalCase"],
            "filter": {
              "regex": "^_[A-Za-z0-9]+$",
              "match": false
            }
          },
          {
            "selector": "interface",
            "format": ["PascalCase"]
          }
        ]
      }
    }
    
  • after(eslint.config.mjs

    import withNuxt from './.nuxt/eslint.config.mjs'
    import { globalIgnores } from "eslint/config";
    import storybook from 'eslint-plugin-storybook'
    import gitignore from 'eslint-config-flat-gitignore'
    
    export default withNuxt(
      ...storybook.configs['flat/recommended'],
      globalIgnores(
        [
          '.storybook/**',
        ]
      ),
      gitignore(),
      
      {
        files: ['**/*.ts', '**/*.tsx'],
        rules: {
          'no-console': 'off',
          '@typescript-eslint/no-explicit-any': 'off',
          '@typescript-eslint/ban-ts-comment': 'off',
          '@typescript-eslint/no-invalid-void-type': 'off',
          '@typescript-eslint/no-empty-object-type': 'off',
          '@typescript-eslint/no-import-type-side-effects': 'off',
          '@typescript-eslint/no-wrapper-object-types': 'off',
          '@typescript-eslint/no-unused-expressions': 'off',
          '@typescript-eslint/no-dynamic-delete': 'off',
          '@typescript-eslint/unified-signatures': 'off',
          'vue/no-ref-as-operand': 'off',
          'no-unsafe-optional-chaining': 'off',
          "@typescript-eslint/naming-convention": [
            "error",
            {
                "selector": "typeAlias",
                "format": ["PascalCase"],
                "filter": {
                "regex": "^_[A-Za-z0-9]+$",
                "match": false
                }
            },
            {
                "selector": "interface",
                "format": ["PascalCase"]
            }
          ],
          "import/order": [
            "error",
            {
                "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"],
                "newlines-between": "always"
            }
          ],
          "@typescript-eslint/no-unused-vars": [
            "error",
              {
              "argsIgnorePattern": "^_",
              "caughtErrorsIgnorePattern": "^_",
              "destructuredArrayIgnorePattern": "^_",
              "varsIgnorePattern": "^_"
            }
          ],
        }
      },
    )
      .override('nuxt/typescript/rules', {
        rules: {
          '@typescript-eslint/no-explicit-any': 'off',
          '@typescript-eslint/ban-ts-comment': 'off',
          '@typescript-eslint/no-unused-expressions': 'off',
          '@typescript-eslint/unified-signatures': 'off',
        },
      })
      .override('nuxt/vue/rules', {
        rules: {
          'vue/no-ref-as-operand': 'off',
          'no-unsafe-optional-chaining': 'off',
          'vue/require-default-prop': 'error',
        },
      })
    

完全とまでにはいきませんが、新たに出たエラーをある程度直して、eslintで移行前と同じような使用感で利用できるようにはなりました。warningがかなり出るようになったので、まずはエラーのみを見やすくして解消するために—-quietオプションを付けて実行と調整をしました。


所感

ESLint v9 への移行は一見シンプルに見えますが、設定ファイル形式が大きく変わったことで、既存プロジェクトでは思った以上に手間がかかりました。

今回は @nuxt/eslint-config を新たに導入したこともあり、Nuxt プロジェクトに最適化された lint 設定を効率的に取り込めました。公式の flat config 対応状況を確認しつつ慎重に進める必要はありますが、一度移行してしまえば設定がシンプルかつ直感的になり、ルールの管理もしやすくなります。


まとめ

  • ESLint v9 は eslint.config.js への移行が必須
  • Nuxt 向けには @nuxt/eslint-config を導入することで対応が楽に
  • .vue ファイル対応には eslint-plugin-vue の明示的導入が必要
  • -ignore-path は使えないので eslint-config-flat-gitignore を使用する
  • .nuxt がないと ESLint が通らない場合はビルドを事前に行う

ESLint v9 への移行と Nuxt 向け設定の導入を検討している方の参考になれば幸いです。


Discussion