⤴️

僕たちに本当に必要だったものは@nuxt/eslint-configだった件

2024/08/16に公開

おつかれさまです。オウンドメディア担当の水谷です。
前回の記事で自チームで管理しているフロントエンドアプリ(Nuxtベース)に導入しているESLintをv9にアップグレードするとともに@nuxt/eslintを導入したのですが、@nuxt/eslint-configを用いる方法が適切とのコメントをいただいたので、切り替えてみましたという紹介です。

TL;DR

Before

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"
        }
    })

After

eslint.config.mjs
eslint.config.mjs
import { createConfigForNuxt } from "@nuxt/eslint-config/flat"
import prettierConfig from "eslint-plugin-prettier/recommended"

export default createConfigForNuxt({
    dirs: {
        src: ["src"]
    }
})
    .override("nuxt/javascript", {
        rules: {
            "no-irregular-whitespace": [
                "error",
                {
                    skipTemplates: true
                }
            ],
            // NOTE: 以下は新たに顕在化したため暫定的に無効化したルール
            // TODO: ルールを削除するか検討
            "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: 以下は新たに顕在化したため暫定的に無効化したルール
            // TODO: ルールを削除するか検討
            "@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: 以下は新たに顕在化したため暫定的に無効化したルール
            // TODO: ルールを削除するか検討
            "nuxt/prefer-import-meta": "off"
        }
    })
    .append(
        {
            ignores: ["generated", "proto", "coverage", "**/browserMonitoringPrd.js", "**/browserMonitoringStg.js"]
        },
        prettierConfig,
        {
            rules: {
                "prettier/prettier": [
                    "error",
                    {
                        singleQuote: false,
                        semi: false,
                        trailingComma: "none",
                        printWidth: 120
                    }
                ]
            }
        }
    )

@nuxt/eslint-configへ切り替え

前回の記事のコメントより

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

確かに前回の本アプリでの対応はESLintアップグレードを目的としており、実行方法や運用は従来のままでしたので@nuxt/eslint-configが適切そうです。紹介いただいた記事を参考にeslint.config.mjsを変更していきます。

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

eslint.config.mjs
+   import { createConfigForNuxt } from "@nuxt/eslint-config/flat"
    import prettierConfig from "eslint-plugin-prettier/recommended"
-   import withNuxt from "./.nuxt/eslint.config.mjs"
    
-   export default withNuxt(
-       {
-           ignores: [
-               ...
-           ]
-       },
-       prettierConfig,
-       {
-           rules: {
-               ...
-           }
-       }
-   )
+   export default createConfigForNuxt()
        .override("nuxt/javascript", {
            ...,
        })
        ... // overrideは変更なし
+       .append(
+           {
+               ignores: [
+                   ... // withNuxtの引数内と同じ
+               ]
+           },
+           prettierConfig,
+           {
+               rules: {
+                   ... // withNuxtの引数内と同じ
+               }
+           }
+       )

ベースとなるconfigを生成する関数をライブラリで定義されているcreateConfigForNuxtに変更します。
また、追加の独自configについては関数の引数ではなく、appendメソッドで追加することになります。(今回は最終的なconfigリストの順番に沿うようにoverrideの後に追加しました。)
overrideについてはwithNuxtを利用していた時と同じ使用方法になります。

エラー顕在化とその原因特定について

ルールのカスタマイズだけなのでこれだけで完了!と思いきや実際にeslintを実行するとvue/multi-word-component-namesのエラーが顕在化してしまいました。。。

$ yarn lint

/Users/kurukuruz/work/my-app/src/error.vue
  1:1  error  Component name "error" should always be multi-word  vue/multi-word-component-names

/Users/kurukuruz/work/my-app/src/pages/foo/index.vue
  1:1  error  Component name "index" should always be multi-word  vue/multi-word-component-names

/Users/kurukuruz/work/my-app/src/pages/bar/baz/index.vue
  1:1  error  Component name "index" should always be multi-word  vue/multi-word-component-names

切り替え前後でなぜこのような差異が発生するのか、まずはESLint Config Inspectorで有効化されているルールを確認してみました。

$ npx @eslint/config-inspector@latest
  • Before(@nuxt/eslintを利用しているとき)
  • After(@nuxt/eslint-configを利用し上記configにしたとき)

どちらの場合もいったん**.*.vue全体でエラーを有効化した後、特定のパスのみ無効化しているのですが、この「特定のパス」に差異があることがわかりました。Afterの方はerror.vueなどがrootディレクトリ直下にある前提なのに対して実際にはsrc配下にあるため無効化条件に引っかからないというのが顕在化した直接原因でした。
しかし@nuxt/eslintも内部的には@nuxt/eslint-configを利用しているはずなのになぜ……?残念ながら@nuxt/eslint-configの詳細なカスタマイズ仕様はドキュメント化されていない[1]のでソースコードを読んでみることにします。

ソースコード探索詳細

※記事作成時点でのソースコードのため、閲覧されている時点での最新ソースでは行番号や構成が変わっている可能性があります。

まず、無効化するためのconfig名nuxt/disables/routesでnuxt/eslintリポジトリ内を検索すると以下のソースがconfigを生成していそうなことがわかります。

https://github.com/nuxt/eslint/blob/8729927e24d8e4acf87ccea68dfa0e68e4ebb4b0/packages/eslint-config/src/flat/configs/disables.ts#L33

上記ファイルの前半部分を見るとNuxtESLintConfigOptions型のオプション(をresolveしたもの)からdirsさらにその配下のsrcというプロパティを使ってapp.vueやerror.vueの除外パスを計算していそうなことがわかります。

次にNuxtESLintConfigOptions型の定義を確認します。

https://github.com/nuxt/eslint/blob/8729927e24d8e4acf87ccea68dfa0e68e4ebb4b0/packages/eslint-config/src/flat/types.ts#L67

トップレベルのプロパティとしてdirsがあり、その下にsrcを配列で設定できることがわかります。

また、dirsと同列に存在するfeaturesは公式ドキュメントの「Customizing the Config」に登場するので、dirsも同様にcreateConfigForNuxtの引数内で設定できそうなことがわかります。

https://eslint.nuxt.com/packages/config#customizing-the-config

この結果createConfigForNuxtの引数内でソースディレクトリを設定すればよいことがわかりました。

srcディレクトリの指定

eslint.config.mjs
-   export default createConfigForNuxt()
+   export default createConfigForNuxt({
+       dirs: {
+           src: ["src"]
+       }
+   })

この変更を反映して再度ESLint Config Inspectorを確認すると

無効化対象のパスがsrc/配下に更新されました。
また、eslintを実行しても顕在化していたエラーはなくなりました。

以上で切り替え作業は完了です。

@nuxt/eslint除去

最後に、@nuxt/eslintモジュールを使う必要がなくなったのでNuxt側の設定から除去しておきます。

nuxt.config.ts
    export default defineNuxtConfig({
        ...,
-       modules: ["@nuxt/eslint"]
    })

依存関係からも除去しておきます。

$ yarn remove @nuxt/eslint

以上で今回の作業は全て完了です。

関連記事

ESLint Config Inspectorやルールのoverrideといった本記事で説明を割愛した部分については前回記事をご参照ください🙇

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

脚注
  1. 2024-08-10時点 ↩︎

レバテック開発部

Discussion