tomlog

ESLintでSharable Configを作成する際のポイント

Posted:2024-12-03

この記事は株式会社エス・エム・エス Advent Calendar 2024 vol.2 12月3日の記事です。

はじめに

ESLintのflat configではlegacy configと比べESLint自身の責務が減っており、シンプルになっています。一方ユーザーはconfigオブジェクトのマージの仕組みに沿ってオブジェクトを組み立てていく必要があります。
本記事では、複数のプロジェクトに共通の設定を提供するSharable Configを作成する際のポイントについて述べていきます。

完成形のサンプルコード

下記のコードはSharable Config(myConfig)とそれを利用するプロジェクト側のeslint.config.jsです。
Sharable Configは複数の外部Pluginに依存しており、jsとreactのルールセットを内包しています。
これらのコードを書くにあたってどのようなポイントがあるのか解説していきます。

myConfig/js.js
import js from '@eslint/js'
import stylistic from '@stylistic/eslint-plugin'
 
export default [
  js.configs.recommended,
  {
    name: 'myConfig/js',
    plugins: { '@stylistic': stylistic },
    rules: {
      ...
    }
  }
]
myConfig/react.js
import react from 'eslint-plugin-react'
 
export default [
  {
    name: 'myConfig/react',
    plugins: { react },
    ...
    rules: {
      ...
    }
  }
]
myConfig/index.js
import stylistic from '@stylistic/eslint-plugin'
import reactPlugin from 'eslint-plugin-react'
 
import js from './js.js'
import react from './react.js'
 
export default {
  configs: {
    js,
    react,
  },
}
eslint.config.js
import globals from 'globals'
import myConfig from 'myConfig'
 
export default [
  ...myConfig.configs.js.map(config => ({
    ...config,
    files: [ '**/*.{js,jsx}' ],
  })),
  ...myConfig.configs.react.map(config => ({
    ...config,
    files: [ 'src/client/**/*.jsx' ],
  })),
  {
    files: ['src/client/**/*.jsx'],
    rules: {
      'react/...': '...'
    }
  },
  {
    files: [ 'src/server/**/*.js' ],
    languageOptions: { globals: { ...globals.node } },
  },
  {
    files: [ 'src/client/**/*.{js,jsx}' ],
    languageOptions: { globals: { ...globals.browser } },
  },
]
 

Sharable Configを作成する際のポイント

ルールセットは配列でexportする

各ルールセットを配列としてexportすることでプロジェクト側の取り回しやすさが向上します。
ルールセットの中身が単一のconfigオブジェクトの場合も配列にしておくことで、複数のconfigオブジェクトを扱うことになった際、プロジェクト側のconfigを書き換えずに済みます。

bad
Sharable Config
export default {
  rules: {
    ...
  }
}
eslint.config.js
export default [
  badConfig,
]
good
Sharable Config
export default [
  {
    rules: {
      ...
    }
  }
]
eslint.config.js
export default [
  ...goodConfig,
]

filesを指定しない

files を指定すると、特定のプロジェクト構成に依存してしまうため、再利用性が低下します。
例えばnode向けのルールセットに対し files: ['**/*.js'] と指定した場合、nodeとbrowserのコードが混在したプロジェクトの場合にbrowserのコードにもnodeのルールが適用されてしまいます。そのため files はプロジェクト側で定義した方がよいです。

bad
Sharable Config
export default [
  {
    files: ['**/*.js'],
    rules: {
      ...
    }
  }
]
good
Sharable Config
export default [
  {
    rules: {
      ...
    }
  }
]
eslint.config.js
export default [
  ...goodConfig.map(config => ({
    ...config,
    files: [ 'src/server/**/*.js' ],
  }))
]

globalsを指定しない

files と同様の理由で globals も指定を避け、プロジェクト側で定義した方がよいです。

bad
Sharable Config
export default [
  {
    languageOptions: { globals: { ...globals.node } },
    rules: {
      ...
    }
  }
]
good
Sharable Config
export default [
  {
    rules: {
      ...
    }
  }
]
eslint.config.js
export default [
  {
    files: [ 'src/server/**/*.js' ],
    languageOptions: { globals: { ...globals.node } },
  },
]

nameを指定する

name を指定するとESLint Config Inspectorやエラーメッセージで name が表示されるためデバッグ時のヒントになります。

bad
Sharable Config
export default [
  {
    rules: {
      ...
    }
  }
]
good
Sharable Config
export default [
  {
    name: 'myConfig',
    rules: {
      ...
    }
  }
]
inspector画面。左はanonymousになっているのに対し、右はnameであるmyConfigが表示されている
inspector画面。左はanonymousになっているのに対し、右はnameであるmyConfigが表示されている

pluginsの命名を管理する

これについては、管理方法を模索中のため現状の課題感を述べておきます。
Sharable Configを運用しているとプロジェクト側で特定のルールを変更したい場合があります。
Sharable Configで定義している plugins を継承すればプロジェクト側でも利用可能ですが、Sharable Config側のPluginの命名に暗黙的に依存することになります。
その結果、Sharable Config側でPluginの命名変更が容易にできなくなったり予期せぬバグの原因になります。反対に plugins の命名がSharable Configとプロジェクト側で異なると、それぞれ別ルールとして認識されてしまい上書きできません。

myConfig/react.js
import react from 'eslint-plugin-react'
 
export default [
  {
    plugins: { react },
    ...
    rules: {
      ...
    }
  }
]
eslint.config.js
import myConfig from 'myConfig'
import react from 'eslint-plugin-react'
 
export default [
  ...myConfig.configs.react.map(config => ({
    ...config,
    files: [ 'src/client/**/*.jsx' ],
  })),
  {
    files: ['src/client/**/*.jsx'],
    rules: {
      'react/foo': 2, // myConfig側のreactという命名に依存するが、ルールは定義できる
    }
  },
  {
    files: ['src/client/**/*.jsx'],
    plugins: { 'react-a': react },
    rules: {
      /**
       * react/fooと同一のルールではあるが、pluginの命名が異なるため、
       * react/fooのルールは有効のまま
       **/
      'react-a/foo': 0,
    }
  },
]

まとめ

ESLintのSharable Configを作成する際には、以下のポイントを意識することで、再利用性高く、使いやすい設定を提供できます。

  • ルールセットを配列形式でexportして柔軟性を持たせる
  • filesglobals は定義せず、プロジェクト固有の要素を避ける
  • name を定義し可視性を高める

また課題としてSharable Configのpluginsに暗黙的に依存すると予期せぬバグの原因になったり、Sharable Configとプロジェクト側で plugins 命名が異なる場合にルールが別々に認識されるため上書きできない点があります。

参考