
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のルールセットを内包しています。
これらのコードを書くにあたってどのようなポイントがあるのか解説していきます。
import js from '@eslint/js'
import stylistic from '@stylistic/eslint-plugin'
export default [
js.configs.recommended,
{
name: 'myConfig/js',
plugins: { '@stylistic': stylistic },
rules: {
...
}
}
]
import react from 'eslint-plugin-react'
export default [
{
name: 'myConfig/react',
plugins: { react },
...
rules: {
...
}
}
]
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,
},
}
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を書き換えずに済みます。
export default {
rules: {
...
}
}
export default [
badConfig,
]
export default [
{
rules: {
...
}
}
]
export default [
...goodConfig,
]
filesを指定しない
files
を指定すると、特定のプロジェクト構成に依存してしまうため、再利用性が低下します。
例えばnode向けのルールセットに対し files: ['**/*.js']
と指定した場合、nodeとbrowserのコードが混在したプロジェクトの場合にbrowserのコードにもnodeのルールが適用されてしまいます。そのため files
はプロジェクト側で定義した方がよいです。
export default [
{
files: ['**/*.js'],
rules: {
...
}
}
]
export default [
{
rules: {
...
}
}
]
export default [
...goodConfig.map(config => ({
...config,
files: [ 'src/server/**/*.js' ],
}))
]
globalsを指定しない
files
と同様の理由で globals
も指定を避け、プロジェクト側で定義した方がよいです。
export default [
{
languageOptions: { globals: { ...globals.node } },
rules: {
...
}
}
]
export default [
{
rules: {
...
}
}
]
export default [
{
files: [ 'src/server/**/*.js' ],
languageOptions: { globals: { ...globals.node } },
},
]
nameを指定する
name
を指定するとESLint Config Inspectorやエラーメッセージで name
が表示されるためデバッグ時のヒントになります。
export default [
{
rules: {
...
}
}
]
export default [
{
name: 'myConfig',
rules: {
...
}
}
]

pluginsの命名を管理する
これについては、管理方法を模索中のため現状の課題感を述べておきます。
Sharable Configを運用しているとプロジェクト側で特定のルールを変更したい場合があります。
Sharable Configで定義している plugins
を継承すればプロジェクト側でも利用可能ですが、Sharable Config側のPluginの命名に暗黙的に依存することになります。
その結果、Sharable Config側でPluginの命名変更が容易にできなくなったり予期せぬバグの原因になります。反対に plugins
の命名がSharable Configとプロジェクト側で異なると、それぞれ別ルールとして認識されてしまい上書きできません。
import react from 'eslint-plugin-react'
export default [
{
plugins: { react },
...
rules: {
...
}
}
]
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して柔軟性を持たせる
files
やglobals
は定義せず、プロジェクト固有の要素を避けるname
を定義し可視性を高める
また課題としてSharable Configのpluginsに暗黙的に依存すると予期せぬバグの原因になったり、Sharable Configとプロジェクト側で plugins
命名が異なる場合にルールが別々に認識されるため上書きできない点があります。