🆙

NuxtアプリのESLintをv9にアップグレードして@nuxt/eslintに全乗っかりしてみた

2024/08/09に公開
2

おつかれさまです。オウンドメディア担当の水谷です。
自チームで管理しているフロントエンドアプリ(Nuxtベース)に導入しているESLintをv9にアップグレードするとともに@nuxt/eslintを導入したのでその紹介です。

TL;DR

Before

.eslintrc.js
.eslintrc.js
module.exports = {
    root: true,
    env: {
        browser: true,
        node: true,
        es2021: true
    },
    parser: "vue-eslint-parser",
    extends: [
        "plugin:@typescript-eslint/recommended",
        "@nuxtjs/eslint-config-typescript",
        "prettier",
        "plugin:prettier/recommended",
        "plugin:nuxt/recommended"
    ],
    parserOptions: {
        ecmaVersion: 13,
        parser: "@typescript-eslint/parser",
        warnOnUnsupportedTypeScriptVersion: false
    },
    plugins: ["prettier"],
    rules: {
        semi: ["error", "never"],
        quotes: ["error", "double"],
        "@typescript-eslint/explicit-function-return-type": "off",
        "@typescript-eslint/no-explicit-any": 1,
        "@typescript-eslint/no-inferrable-types": [
            "warn",
            {
                ignoreParameters: true
            }
        ],
        "no-redeclare": "off",
        "@typescript-eslint/no-redeclare": ["error"],
        "@typescript-eslint/no-unused-vars": "off",
        // prettierと矛盾することがあるので、offにしている
        indent: ["off", 4],
        "vue/no-v-html": "off",
        "prettier/prettier": [
            "error",
            {
                singleQuote: false,
                semi: false,
                trailingComma: "none",
                printWidth: 120
            }
        ],
        "nonblock-statement-body-position": "error",
        "no-irregular-whitespace": [
            "error",
            {
                skipTemplates: true
            }
        ],
        "vue/no-multiple-template-root": "off"
    }
}
.eslintignore
.eslintignore
.nuxt
generated
proto
nuxt.config.ts
dist
.eslintrc.js
node_modules

After

nuxt.config.ts
nuxt.config.ts
export default defineNuxtConfig({
    ...,
    modules: ["@nuxt/eslint"]
})
eslint.config.mjs
eslint.config.mjs
import prettierConfig from "eslint-plugin-prettier/recommended"
import withNuxt from "./.nuxt/eslint.config.mjs"

export default withNuxt(
    {
        ignores: [
            "generated",
            "proto",
            "coverage",
            "**/browserMonitoringPrd.js",
            "**/browserMonitoringStg.js"
        ]
    },
    prettierConfig,
    {
        rules: {
            "prettier/prettier": [
                "error",
                {
                    singleQuote: false,
                    semi: false,
                    trailingComma: "none",
                    printWidth: 120
                }
            ]
        }
    }
)
    .override("nuxt/javascript", {
        rules: {
            "no-irregular-whitespace": [
                "error",
                {
                    skipTemplates: true
                }
            ],
            // NOTE: 以下は新たに顕在化したため暫定的に無効化したルール
            "no-constant-binary-expression": "off"
        }
    })
    .override("nuxt/typescript/rules", {
        rules: {
            "@typescript-eslint/no-redeclare": ["error"],
            "@typescript-eslint/no-inferrable-types": [
                "warn",
                {
                    ignoreParameters: true
                }
            ],
            "@typescript-eslint/no-unused-vars": "off",
            // NOTE: 以下は新たに顕在化したため暫定的に無効化したルール
            "@typescript-eslint/consistent-type-imports": "off",
            "@typescript-eslint/no-extraneous-class": "off",
            "@typescript-eslint/unified-signatures": "off"
        }
    })
    .override("nuxt/vue/rules", {
        rules: {
            "vue/no-v-html": "off"
        }
    })
    .override("nuxt/vue/single-root", {
        rules: {
            "vue/no-multiple-template-root": "off"
        }
    })
    .override("nuxt/rules", {
        rules: {
            // NOTE: 以下は新たに顕在化したため暫定的に無効化したルール
            "nuxt/prefer-import-meta": "off"
        }
    })

追記

コメントいただいたご指摘を受け、@nuxt/eslintから@nuxt/eslint-configに乗り換えた記事を投稿しましたので併せてお読みいただけますと幸いです🙇

https://zenn.dev/levtech/articles/e1bd0859f8d677

上記記事では本記事と重複する内容は割愛していますので本記事も引き続きご覧ください🙇

準備: .eslintrc.jsの内容を確認する

このプロジェクトの.eslintrc.jsは自分が参画前かつNuxt2時代[1]に作成されたものでした。
そもそも自分自身、eslintrcの内容を精査したこともなかったので各項目が何を意味しているかも含め確認しました。

root

.eslintrc.js (抜粋)
    root: true,

https://zenn.dev/harusame0616/articles/42a52bc0f9843c#設定ファイルの階層

プロジェクトルートの.eslintrc.jsということを示すフラグです。

env

.eslintrc.js (抜粋)
    env: {
        browser: true,
        node: true,
        es2021: true
    },

https://zenn.dev/kimromi/articles/546923b7281dcb

利用を許可するグローバル変数のセットを提供するものです。

TypeScriptを使っていてplugin:@typescript-eslint/recommendedをextendsしている場合は、ESLintでグローバル変数のチェックはしないためenv設定は必要ないかもしれません。
(上記記事より)

本アプリはガッチリ当てはまるので不要な記述だったかもしれません😅

parser, parserOptions

.eslintrc.js (抜粋)
    parser: "vue-eslint-parser",
    // (中略)
    parserOptions: {
        ecmaVersion: 13,
        parser: "@typescript-eslint/parser",
        warnOnUnsupportedTypeScriptVersion: false
    },

https://qiita.com/ibara1454/items/be73615df332564e7855#parser

名前の通りですが、parserはソースコードを読み取る際のパーサー・parserOptionsはパーサーに渡すオプションです。

本アプリはNuxtベースということでソースファイルは.vueが中心になっているのでこれをパースするためにvue-eslint-parserが指定されているようです。
さらにparserOptions.parserでVueファイル内のscriptタグのパーサーを指定できます。(参考
本アプリではTypeScriptで記述するルールになっているので@typescript-eslint/parserが指定されているようです。

🫤.。oO(envはes2021(12相当)を指定してるのにこっちのecmaVersionは13なの??)

extends

.eslintrc.js (抜粋)
    extends: [
        "plugin:@typescript-eslint/recommended",
        "@nuxtjs/eslint-config-typescript",
        "prettier",
        "plugin:prettier/recommended",
        "plugin:nuxt/recommended"
    ],

https://qiita.com/ibara1454/items/be73615df332564e7855#plugins-と-extends
https://blog.ojisan.io/eslint-plugin-and-extend/

ルールの定義に有効無効や対象ファイルなど、実行するための一通りを構成してくれます。
そのためextendsに設定する一式をconfig(=構成)と呼ぶようです。

plugin:@typescript-eslint/recommended

https://typescript-eslint.io/users/configs/#recommended

名前の通りTypeScriptに最適化されたconfigです。ルールの追加だけでなく、ESLintのcoreルールで競合するものやTypeScriptの書き方にそぐわないものは無効化してくれているそうです。

@nuxtjs/eslint-config-typescript

https://eslint.nuxt.com/legacy/eslint-config-ts

これも名前の通りですが、Nuxt(Nuxt2)用に調整されたconfigのTypeScript版です。

ソースコード[2]を見るとplugin:@typescript-eslint/recommendedをextendsしてくれています。
なのでアプリのeslintrc内で改めて宣言する必要はないようです😅[3]

🫤.。oO(Nuxt2用なのか……アプリはNuxt3に上げてあるのに)

上記リンクにあるようにこれはNuxt2用なのでNuxt3では@nuxt/eslintを使えとのことです。

prettier, plugin:prettier/recommended

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

  • prettiereslint-config-prettierの省略形で、Prettierルールと競合するESLintルールを無効化してくれるconfigです
  • plugin:prettier/recommendedeslint-plugin-prettierに内包されるconfigでPrettierルールをESLintルールとして適用するためのconfigです

なお、plugin:prettier/recommendedによりeslint-config-prettierのセットアップもまとめてしてくれるようです(参考)😅

plugin:nuxt/recommended

https://github.com/nuxt/eslint-plugin-nuxt

こちらはeslint-plugin-nuxtライブラリに内包されているconfigです。
(前述の@nuxtjs/eslint-config-typescriptとの役割分担はよく分かりませんでした…)

なお、こちらもNuxt2用のため、Nuxt3では@nuxt/eslintを使えとのことです😅

plugins

.eslintrc.js (抜粋)
    plugins: ["prettier"],

pluginsはルールの定義のみ追加しますが、extendsと異なり各ルールの有効無効についてはrules(後述)で設定する必要があります。

"prettier"

prettier/prettierルールセットをインポートするための記述と思われます。
が、"extends": ["plugin:prettier/recommended"]することでprettier/prettierルールセット有効化できるので不要な記述でした😅

rules

.eslintrc.js (抜粋)
    rules: {
        // (長いので割愛)
    },

ルールの有効化/無効化およびルールの調整を行うセクションです。extendsおよびpluginsにより利用可能になったルールも調整できます。
(調整方法はルールよってまちまちなので詳細は割愛)

.eslintignore

こちらは.eslintrc.jsとは別ファイルですが、移行に関連するために記載しておきます。

.eslintignore
.nuxt
generated
proto
nuxt.config.ts
dist
.eslintrc.js
node_modules

ファイル名の通り、eslintを適用しないファイルおよびディレクトリを列挙するファイルです。
node_modulesとソースコードをビルドした際に作成されるディレクトリおよびルートディレクトリ直下の設定ファイルが記載されています。

移行: eslint.config.mjsを記述する

上記の既存設定を基本的には踏襲しつつ、必要に応じてライブラリを切り替えたり不要な記述を除去してESLint v9用の設定を記述してきます。

有効なConfigの確認方法について

移行後(eslint.config.mjs作成後)に使える機能になりますが、これを見ながら調整をしていくので先に紹介しておきます。

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

上記のESLint Config Inspectorによりプロジェクトでどんなconfigが取り込まれているのか、どのルールが有効になっているかをブラウザで確認することができます。npxから実行することですぐに立ち上がります。

$ npx @eslint/config-inspector@latest

Nuxt ESLint導入

「準備」セクションで確認した通り、Nuxt3では@nuxt/eslintを使うことが推奨されているのでこの手順に従っていきます。

https://eslint.nuxt.com/packages/module#manual-setup

  • まずは沿ってライブラリを追加
    $ yarn add -D eslint
    $ yarn add @nuxt/eslint
    
  • Nuxtのmodulesを追加
    nuxt.config.ts
        export default defineNuxtConfig({
            ...,
    +       modules: [
    +           '@nuxt/eslint'
    +       ],
        })
    
  • いったんyarn buildまたはyarn devを実行
    • .nuxt配下にeslint.config.mjsが作成されます(次で参照します)
  • ルートディレクトリ直下にeslint.config.mjsを新規作成
    eslint.config.mjs
    import withNuxt from "./.nuxt/eslint.config.mjs"
    
    export default withNuxt()
    

withNuxtの引数について

withNuxtはFlat Configのオブジェクトを可変長引数で受け取るようになっています。そのため、一般的なFlat Configで配列としてexportする設定を展開してwithNuxtの引数にすることができます。

配列をexportするパターン
export default [{...}, {...}, {...}]

↓↓↓

withNuxtを使用するパターン
export default withNuxt({...}, {...}, {...})

ignores設定

上記の状態ではルートディレクトリ配下の全てのファイルがeslint適用対象になってしまうので、まずは適用不要なファイルを除外する設定を入れます。
以前の形式では別ファイル(.eslintignore)に除外設定を記載していましたが、Flat Configではconfig内に記述することになります。
ignoresプロパティにファイル名やディレクトリ名あるいはパスパターンを配列として列挙することでeslintの適用対象から除外することができます。

eslint.config.mjs
-   export default withNuxt()
+   export default withNuxt(
+       {
+           ignores: [
+               "generated",
+               "proto",
+               "coverage",
+               "**/browserMonitoringPrd.js",
+               "**/browserMonitoringStg.js"
+           ]
+       },
+   )

ESLint Config Inspectorで確認するとnode_modules.nuxt, distといった典型的なディレクトリはすでに除外設定が入っていたので、それ以外のディレクトリについて記述を追加しました。
また、nuxt.config.tsやeslint.config.mjsについてはESLint経由でフォーマッターを効かせたかったためにあえて除外設定に入れませんでした。

globals, parserについて

https://qiita.com/Shilaca/items/c494e4dc6b536a5231de#2-env-オプションは無くなり代わりに-globals-を使用するようになりました

以前の形式でのenvと直接互換性のあるプロパティはFlat Configにはありません。そもそもenvの役割は「準備」で書いた通り利用を許可するグローバル変数の定義でした。
なので許可するグローバル変数を定義するプロパティlanguageOptions.globalsを記述することになります。
また、envに相当するグローバル変数のセットがglobalsというライブラリにまとまっているのでそれを利用することで以前の記述内容に近づけることができます。

parserについてはlanguageOptions.parserおよびlanguageOptions.parserOptionsのプロパティで以前の形式と同様に指定することができます。

ただし、ESLint Config Inspectorで確認するとすでにglobalsおよびparserは拡張子ごとに設定されているので、今回はeslint.config.mjsへの明示的な記載は無しとしました。

Shareable Config導入 (旧extends)

次に以前の形式でextends指定していたconfigを適用させます。
以前の形式ではconfig名を文字列で指定しましたが、Flat ConfigではアプリでカスタムしたConfigオブジェクトと並列させる形でライブラリからimportしたConfigオブジェクトを列挙します。

例えば、以前の形式で"plugin:prettier/recommended"として指定していたconfigはeslint-plugin-prettierのREADME通りeslint-plugin-prettier/recommendedからimportできるConfigオブジェクトをeslint.config.mjsに追加すればよいです。

eslint.config.mjs
+   import prettierConfig from "eslint-plugin-prettier/recommended"
    
    export default withNuxt(
        {
            ignores: [
                "generated",
                "proto",
                "coverage",
                "**/browserMonitoringPrd.js",
                "**/browserMonitoringStg.js"
            ]
        },
+       prettierConfig,
    )

ESLint Config Inspectorで確認すると、アップグレード前に導入していたNuxt関連の2つのconfigと@typescript-eslintに相当するルールは(概ね)反映されているようだったので、Shareble Configの追加は上記のprettierのみとしました。

pluginsについて

pluginsについては以前の形式と同様にpluginsプロパティで設定することができます。
ただし、prettier/prettierについては「準備」の項でも書いたとおりShareble Config(旧extends)経由で取り込まれているため、eslint.config.mjsへの明示的な記載は無しとしました。

rules移植

独自にオンオフをカスタマイズするルールについては以前の形式と同様にrulesプロパティで設定することができます。
まずは本アプリでの以前のprettierのルールを移植します。

eslint.config.mjs
    export default withNuxt(
        {
            ignores: [
                "generated",
                "proto",
                "coverage",
                "**/browserMonitoringPrd.js",
                "**/browserMonitoringStg.js"
            ]
        },
        prettierConfig,
+       {
+           rules: {
+               "prettier/prettier": [
+                   "error",
+                   {
+                       singleQuote: false,
+                       semi: false,
+                       trailingComma: "none",
+                       printWidth: 120
+                   }
+               ],
+           }
+       },
    )

次にprettier以外のルール移植なのですが、Nuxt ESLintにより設定されているルールの調整方法は2種類あります。

1. withNuxtの引数のrulesに追加

1つ目の方法はprettierと同様の場所に追加するやり方です。

eslint.config.mjs
    export default withNuxt(
        ...,
        {
            rules: {
                "prettier/prettier": [
                    ...,
                ],
+               "@typescript-eslint/no-unused-vars": "off",
            }
        },
    )

この場合は末尾(最も優先される)独自configにルールが追加されます。

(↓↓↓ESLint Config Inspectorで確認↓↓↓)

ただしこのconfigには現状file matchingに関する設定をしていないため除外設定されていないすべてのファイルに適用されてしまいます。(Applied generally for all files

2. withNuxtの戻り値のoverrideを利用

もう1つはwithNuxtの戻り値のoverrideメソッドを利用するやり方です。

https://eslint.nuxt.com/packages/module#config-customizations

ESLint Config Inspectorで確認すると上記でルールの無効化を試みた@typescript-eslint/no-unused-varsは元々nuxt/typescript/rulesと名付けられたconfigによって有効化されていました。

なので、このconfig名を指定してrulesをoverrideすると該当configのルールがマージされ無効化されます。

eslint.config.mjs
    export default withNuxt(
        ...,
        {
            rules: {
                "prettier/prettier": [
                    ...,
                ],
-               "@typescript-eslint/no-unused-vars": "off",
            }
        },
    )
+       .override("nuxt/typescript/rules", {
+           rules: {
+               "@typescript-eslint/no-unused-vars": "off"
+           }
+       })

(↓↓↓ESLint Config Inspectorで確認↓↓↓)

こちらの場合はfile matchingに関する設定は据え置きかつ明示していないルールには影響を与えずにマージできるので今回はこちらのやり方を採用し、元々入っていたrulesでwithNuxt利用時のデフォルトルールと差異があるものをoverrideしました

eslint.config.mjs
    export default withNuxt(
        ...
    )
+       .override("nuxt/javascript", {
+           rules: {
+               "no-irregular-whitespace": [
+                   "error",
+                   {
+                       skipTemplates: true
+                   }
+               ]
+           }
+       })
+       .override("nuxt/typescript/rules", {
+           rules: {
+               "@typescript-eslint/no-redeclare": ["error"],
+               "@typescript-eslint/no-inferrable-types": [
+                   "warn",
+                   {
+                       ignoreParameters: true
+                   }
+               ],
+               "@typescript-eslint/no-unused-vars": "off"
+           }
+       })
+       .override("nuxt/vue/rules", {
+           rules: {
+               "vue/no-v-html": "off"
+           }
+       })
+       .override("nuxt/vue/single-root", {
+           rules: {
+               "vue/no-multiple-template-root": "off"
+           }
+       })

なお、デフォルトでオフになっていたルールを有効化する際はconfig名やfile matchingの設定内容を基に追加するconfigを決定しました。

rules追加(暫定対応)

以上で基本的な移行作業は完了なのですが、NuxtおよびTypeScriptまわりのShareable Configを@nuxt/eslintに切り替えた影響で今までは出ていなかった箇所でエラーが検知されてしまうようになりました。
今回はeslintのアップグレードのみを目的としソースコードに変更を加えたくなかったため、いったん該当ルールをオフにする対応を取りました。

eslint.config.mjs
    export default withNuxt(
        ...
    )
        .override("nuxt/javascript", {
            rules: {
                "no-irregular-whitespace": [
                    ...,
                ],
+               // NOTE: 以下は新たに顕在化したため暫定的に無効化したルール
+               "no-constant-binary-expression": "off"
            }
        })
        .override("nuxt/typescript/rules", {
            rules: {
                ...,
+               // NOTE: 以下は新たに顕在化したため暫定的に無効化したルール
+               "@typescript-eslint/consistent-type-imports": "off",
+               "@typescript-eslint/no-extraneous-class": "off",
+               "@typescript-eslint/unified-signatures": "off"
            }
        })
        .override("nuxt/vue/rules", {
            ...
        })
        .override("nuxt/vue/single-root", {
            ...
        })
+       .override("nuxt/rules", {
+           rules: {
+               // NOTE: 以下は新たに顕在化したため暫定的に無効化したルール
+               "nuxt/prefer-import-meta": "off"
+           }
+       })

また、逆にデフォルトでルールが無効化されたためにeslint-disable-next-lineを付与していた箇所でUnused eslint-disable directiveというwarningが出るようになってしまいました。
こちらについてはそもそも(それなりの箇所で)無効化してしまっていたルールであったため、今回はデフォルト設定を採用することとしました。

以上で今回の移行作業は完了です!

所感

@nuxt/eslintに切り替えたことで基本的な設定の部分で従来の設定をかなり省略し「巨人の肩の上に立つ」ことができたように感じました。
また、ESLint Config Inspectorで網羅的にルールを確認でき、調整の大きな助けになりました。

一方でprettierに関してはlintの実行時間が長くなってしまいました。prettier/prettierルールの分かりづらさもあるのでESLint Stylisticへの移行も検討したいと考えています。

最後に今回の移行作業で妥協・先送りした事項を残課題として列挙しておきます。

  • @nuxt/eslintをdependenciesに追加している
    • devDependenciesに置きつつmodulesに追加する方法はあるか?
    • もしくはmodulesに追加せずに@nuxt/eslintを利用できるか?
  • 一部のルール抜き差し
    • 有効化されたルールの対応
      • いったん暫定でoffにした
      • 順次リファクタリング予定
    • 無効化されたルールの対応
      • Unused eslint-disable directiveが発生したがいったん無視した
      • 順次無効化マークを除去予定
脚注
  1. 後にNuxt3にアップグレードした ↩︎

  2. 2024-07-31閲覧 ↩︎

  3. ライブラリの推移依存に期待せず絶対に適用したいという意図で明示していた可能性もあるが今となっては経緯は不明 ↩︎

レバテック開発部

Discussion

GANGANGANGAN

コメント失礼します!

もしくはmodulesに追加せずに@nuxt/eslintを利用できるか?

こちらについては@nuxt/eslintではなく@nuxt/eslint-configを用いる方法が適切かと思います。この場合は今までのESLintの利用方法(CIやnpm scriptsで実行する)と似た使い方が可能です。

https://eslint.nuxt.com/packages/config

日本語としては下記記事に記載しております。合わせてご一読頂けると幸いです!

https://zenn.dev/comm_vue_nuxt/articles/setup-nuxt-eslint#update-%40nuxt%2Feslint-config%40v3.x

kurukuruzkurukuruz

コメントありがとうございます!
ご指摘のとおり@nuxt/eslint-configを用いる方法が適切でしたのでそちらに切り替えました!
別記事にしました)

ご提示いただいた記事も大変勉強になりました🙇