自分だけの ESLint Config を作る
これはなに
自身のプロジェクトに適合した ESLint Config の設計および実装方法についてまとめたものです。
業務や趣味プロに関わらず、 JavaScript / TypeScript でコーディングする際に ESLint は非常に有用なツールであり、広く使われています。ほとんどの場合において ESLint 基盤はプロジェクトごとに必要なパッケージをインストールして設定ファイルを作成し、構築されます。しかしその設定内容がプロジェクト間で重複することは珍しくなく、プロジェクトの数が増えるにつれ冗長となりがちです。そこで、複数プロジェクト間で共通のルールセットを定義して単一の ESLint Config として再利用することで、メンテナンス性や再利用性の向上が期待できます。本稿では、そのような用途で使える自分だけの ESLint Config を設計・実装する方法を紹介します。
前提
- Flat Config で設定する
- ES Modules に準拠する
ESLint Config の設計
提供する機能を決める
想定する利用シーンを考慮して提供する機能を決めます。本稿では以下の 4 機能を提供することを想定します。
- JavaScript のセオリーに準拠したコーディングに関する設定を提供する
- Node.js のセオリーに準拠したコーディングに関する設定を提供する
- React を用いたコーディングに関する設定を提供する
- TypeScript でのコーディングに関する設定を提供する
全ての利用プロジェクトが上記の技術スタックを採用するならまだしも、なかには React や TypeScript を採用しないプロジェクトがある場合も考えられます。そのため、上記の内容を単一のルールセットとするのではなく、それぞれの設定を個別に提供できるようにするのが望ましいでしょう。以後はこれを前提に進めます。
ルールセットをコンテキストごとに分類する
提供する機能が決まったら、各ルールセットをコンテキストごとに分類します。ESLint のルール数はプラグインを含めると膨大な数になるため、コンテキストごとに分類することでルールセットのメンテナンス性を保持しやすくなります。本稿では以下のように設計します。
.
└── rules/
├── best-practices.js # eslint
├── errors.js
├── es6.js
├── style.js
├── variables.js
├── imports.js # eslint-plugin-import
├── promise.js # eslint-plugin-promise
├── node.js # eslint-plugin-n
├── typescript.js # @eslint-typescript/{eslint-plugin,parser}
├── jsx-a11y.js # eslint-plugin-jsx-a11y
├── react-hooks.js # eslint-plugin-react-hooks
└── react.js # eslint-plugin-react
eslint
本体のルールが best-practice
、 errors
、 es6
、 style
、 variables
に分類されています。この分類は eslint-config-airbnb-base の設計を参考にしたものです。また、プラグインに依存するルールセットは、プラグインごとにコンテキストを分けてファイル化します。
分類したルールセットを提供する単位でグルーピングする
先述したルールセットはあくまでこの ESLint Config のメンテナンス性を高めるためのものであり、利用者が直接参照するにはいささか粒度が細かすぎます。そこで、コンテキストごとに分類したルールセットを利用者へ提供する単位でグルーピングします(ここではグルーピングしたものを config
と呼びます)。これにより、利用者が必要なルールセットを選択しやすくなります。本稿では以下のようなグルーピングで設計します。
.
└── configs/
├── essentials.js # best-practices, errors, es6, imports, promise, style, variables
├── node.js # node
├── react.js # jsx-a11y, react, react-hooks
└── typescript.js # typescript
Config | 依存するルールセット | 説明 |
---|---|---|
essentials |
best-practices errors es6 imports promise style variables
|
JavaScript のセオリーに準拠したコーディングに関する設定 |
node |
node |
Node.js のセオリーに準拠したコーディングに関する設定 |
react |
jsx-a11y react-hooks react
|
React を用いたコーディングに関する設定 |
typescript |
typescript |
TypeScript でのコーディングに関する設定 |
このようにグルーピングすることで、利用者はプロジェクトに合わせて必要な設定を選択できるようになります。例えば最低限の設定だけを適用したい場合は essentials
のみを選択し、React と TypeScript を用いたプロジェクトであればさらに react
と typescript
を追加するといった具合です。
インターフェイスを設計する
提供する設定が決まったら、利用者がそれらをどのような形で参照するかを設計します。本稿における ESLint Config は Flat Config かつ ES Modules に準拠することを前提としているため、以下のようなインターフェイスを設計します。
import { essentials, node, react, typescript } from './configs';
export default [...];
.
└── configs/
+ ├── index.js
├── essentials.js # best-practices, errors, es6, imports, promise, style, variables
├── node.js # node
├── react.js # jsx-a11y, react, react-hooks
└── typescript.js # typescript
このように、グルーピングした設定をモジュールとして単一のファイル(ここでは configs.js
とする)に集約し、利用者は必要な設定をインポートすることで利用できるようにします。各モジュールを named export とすることで、利用者は IDE による入力補完の恩恵を最大限に受けることができます。
最終的なディレクトリー構成
.
├── rules/
│ ├── best-practices.js
│ ├── errors.js
│ ├── es6.js
│ ├── imports.js
│ ├── node.js
│ ├── promise.js
│ ├── react-hooks.js
│ ├── react.js
│ ├── style.js
│ ├── typescript.js
│ └── variables.js
├── configs/
│ ├── essentials.js
│ ├── node.js
│ ├── react.js
│ └── typescript.js
└── package.json
実装
ルールセットを実装する
設計が完了したら各種ルールセットを実装します。
best-practices / errors / es6 / style / variables
これらは ESLint プラグインに依存しないため、eslint
本体のルールをそれぞれのコンテキストに分類して実装します。例えば best-practices
は以下のように実装できます。
export default {
rules: {
'accessor-pairs': ['off'],
'array-callback-return': ['error', { allowImplicit: true }],
'block-scoped-var': ['error'],
// ...
},
};
imports / promise / node / react / react-hooks / jsx-a11y / typescript
imports
は eslint-plugin-import
に依存するため、プラグインをインストールして実装します。また、 settings
フィールドも併せて設定します。
import importPlugin from 'eslint-plugin-import';
export default {
plugins: {
import: importPlugin,
},
settings: {
'import/resolver': {
node: {
extensions: ['.mjs', '.js', '.json', '.ts'],
},
// Resolve the problem of incorrect recognition of alias paths by TypeScript compiler options.
// https://github.com/import-js/eslint-plugin-import/issues/1485#issuecomment-535351922
typescript: {},
},
'import/extensions': ['.js', '.mjs', '.jsx', 'ts', 'tsx'],
'import/core-modules': [],
'import/ignore': [
'node_modules',
'\\.(coffee|scss|css|less|hbs|svg|json|jpg|jpeg|png|webp)$',
],
// TODO: Remove this once eslint-plugin-import supports Flat Config completely.
// https://github.com/import-js/eslint-plugin-import/issues/2556#issuecomment-1419518561
'import/parsers': {
espree: ['.js', '.mjs', '.jsx', 'ts', 'tsx'],
},
},
rules: {
...importPlugin.configs.recommended.rules,
...importPlugin.configs.errors.rules,
// 追加のルール例
'import/no-unresolved': ['error', { commonjs: true, caseSensitive: true }],
// ...
},
};
eslint-plugin-import
が提供する recommended
や errors
ルールセットをベースにしたい場合は、以下のように Spread Operator を用いて取り入れ、そのうえで追加のルールを記述します。
rules: {
...importPlugin.configs.recommended.rules,
...importPlugin.configs.errors.rules,
// 追加のルール例
'import/no-unresolved': ['error', { commonjs: true, caseSensitive: true }],
// ...
},
その他のルールセットも必要に応じて languageOptions
や parserOptions
などの設定を追加して実装しますが、基本的には上記の imports
と同様の設計で問題ありません。
promise コード例
import promisePlugin from 'eslint-plugin-promise';
export default {
plugins: {
promise: promisePlugin,
},
rules: {
...promisePlugin.configs.recommended.rules,
// 追加のルール例
'promise/always-return': ['off', { ignoreLastCallback: true }],
// ...
},
};
node コード例
import n from 'eslint-plugin-n';
export default {
plugins: {
n,
},
rules: {
...n.configs['flat/recommended'].rules,
// 追加のルール例
'n/global-require': ['error'],
// ...
},
};
react コード例
import react from 'eslint-plugin-react';
export default {
plugins: {
react,
},
languageOptions: {
...react.configs.recommended.languageOptions,
},
settings: {
react: {
version: 'detect',
},
},
rules: {
...react.configs.recommended.rules,
// 追加のルール例
'react/display-name': ['off', { ignoreTranspilerName: false }],
// ...
},
};
react-hooks コード例
import reactHooks from 'eslint-plugin-react-hooks';
export default {
plugins: {
'react-hooks': reactHooks,
},
rules: {
'react-hooks/exhaustive-deps': ['error'],
'react-hooks/rules-of-hooks': ['error'],
},
};
eslint-plugin-react-hooks
は recommended ルールセットを提供していないため、全てのルールを明示的に指定します。
jsx-a11y コード例
import jsxA11y from 'eslint-plugin-jsx-a11y';
export default {
plugins: {
'jsx-a11y': jsxA11y,
},
rules: {
...jsxA11y.configs.recommended.rules,
// 追加のルール例
'jsx-a11y/anchor-is-valid': ['error', { components: ['Link'] }],
// ...
},
};
typescript コード例
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import parser from '@typescript-eslint/parser';
import bestPractices from './best-practices.js';
export default {
plugins: {
'@typescript-eslint': typescriptEslint,
},
languageOptions: {
parser,
parserOptions: {
project: true,
},
},
rules: {
...typescriptEslint.configs['eslint-recommended'].overrides[0].rules,
...typescriptEslint.configs['strict-type-checked'].rules,
...typescriptEslint.configs['stylistic-type-checked'].rules,
// 追加のルール例
'@typescript-eslint/dot-notation': bestPractices.rules['dot-notation'],
// ESLint 本体のルールを TypeScript 用に上書きする場合は以下のように記述する
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': bestPractices.rules['no-unused-expressions'],
// ...
}
};
typescript-eslint は ESLint 本体が提供するルールを拡張したものをいくつか含んています。そのようなルールを設定する場合は、設定値の重複を避けるために best-practices
や variables
などのルールセットを参照します。
rules: {
// ESLint 本体のルールを無効化する
'no-unused-expressions': 'off',
// TypeScript 用に拡張された同種のルールを設定する
'@typescript-eslint/no-unused-expressions': bestPractices.rules['no-unused-expressions'],
},
ルールセットをグルーピングした設定を実装する
ルールセットを実装したら、それらをグルーピングした設定を実装します。
essentials
best-practices
、 errors
、 es6
、 style
、 variables
、 imports
、 promise
をグルーピングしたものです。
import bestPracticesRuleSet from '../rules/best-practices.js';
import errorsRuleSet from '../rules/errors.js';
import es6RuleSet from '../rules/es6.js';
import importsRuleSet from '../rules/imports.js';
import promiseRuleSet from '../rules/promise.js';
import styleRuleSet from '../rules/style.js';
import variablesRuleSet from '../rules/variables.js';
export default [
{
languageOptions: {
parserOptions: {
ecmaVersion: 2023,
sourceType: 'module',
},
},
},
bestPracticesRuleSet,
errorsRuleSet,
es6RuleSet,
styleRuleSet,
variablesRuleSet,
importsRuleSet,
promiseRuleSet,
];
配列の先頭で languageOptions
フィールドを設定し、その後に各ルールセットを追加します。この languageOptions
フィールドは ESLint 本体に関する設定のため best-practices
、errors
、es6
、style
、variables
のいずれかに記述することもできますが、どれか 1 つに記述するのはちぐはぐな印象となり、逆に全てに記述するのは冗長すぎるため、例外的に essentials
に記述することにしました。
node
./rules/node.js
のみなので、これをインポートしてそのまま返すだけです。
import nodeRuleSet from '../rules/node.js';
export default [nodeRuleSet];
react
jsx-a11y
、 react
、 react-hooks
をグルーピングしたものです。これも ./configs/node
と同様に 3 つのルールセットをインポートしてそのまま返すだけです。
// @ts-check
import jsxA11yRuleSet from '../rules/jsx-a11y.js';
import reactHooksRuleSet from '../rules/react-hooks.js';
import reactRuleSet from '../rules/react.js';
export default [
reactRuleSet,
reactHooksRuleSet,
jsxA11yRuleSet,
];
typescript
./rules/typescript.js
のみですが、このルールセットは TypeScript ファイルのみを対象とするため、files
フィールドを設定して TypeScript 関連ファイル以外に適用されないようにします。
import typescriptRuleSet from '../rules/typescript.js';
export default [
{
files: ['**/*.@(ts|tsx|cts|mts)'],
...typescriptRuleSet,
},
];
グルーピングした設定を単一のモジュールファイルに集約する
#インターフェイスを設計する でも述べた通り、グルーピングした設定を単一のモジュールファイルに集約します。
import essentials from './essentials.js';
import node from './node.js';
import react from './react.js';
import typescript from './typescript.js';
export { essentials, node, react, typescript };
これで利用者は configs/index.js
をインポートし、そこから必要な設定を選択して利用できるようになります。
import { essentials, node, react, typescript } from './configs';
export default [
...essentials,
...node,
...react,
...typescript,
];
各設定は配列型であるため、利用する際は Spread Operator を用いて展開します。本来なら node
や typescript
はそれぞれ 1 つのルールセットしか持たないので配列型である必要はありませんが、利用者が統一的なインターフェイスで設定を参照できるようにするため、 essentials
や react
に合わせて配列型としています。
Node パッケージ化する
実装した ESLint Config を複数のプロジェクトで利用するならば、Node パッケージ化を検討するのが望ましいでしょう。パッケージは npmjs に公開してもよいですし、GitHub パッケージとして特定の Organization に所属するリポジトリーに閉じて公開するのもよいでしょう。スコープが Monorepo であれば、その Monorepo のサブパッケージとして配置し、他のサブパッケージから参照することも可能です。
パッケージを定義する
package.json
を以下のように定義します。
{
"name": "@example/eslint-config",
"version": "1.0.0",
"type": "module", // 1.
"main": "./configs/index.js", // 2.
"files": [
"configs",
"rules"
],
"devDependencies": {
"eslint": "8.57.0",
"typescript": "5.4.5"
},
"peerDependencies": { // 3.
"eslint": "^8.56.0",
"typescript": "^4.7.5 || ^5.0.0"
},
"peerDependenciesMeta": { // 4.
"typescript": {
"optional": true
}
}
}
以下、各フィールドの説明です。
type
1. Flat Config は ES Modules に準拠しているため、type
フィールドに "module"
を指定します。
main
2. このパッケージのエントリーポイントとなるファイルを指定します。 configs/index.js
がインターフェイスとなるため、これを指定します。代わりに exports
フィールドを指定することも可能です。
{
"type": "module",
- "main": "./configs/index.js",
+ "exports": {
+ ".": {
+ "import": "./configs/index.js"
+ }
+ }
"files": [
"configs",
"rules"
],
}
peerDependencies
3. このパッケージと併せてインストールが必要な Node パッケージを指定します。この ESLint Config は eslint
と typescript
に依存しているため、これらを指定します。それぞれのバージョン番号は、この ESLint Config の動作を保証するものを指定します。
peerDependenciesMeta
4. essentials
、 node
、 react
しか利用しないプロジェクトであれば TypeScript は不要のため、 typescript
パッケージをオプショナルにするために peerDependenciesMeta
フィールドを指定します。
ESLint プラグインの扱いについて
本稿で紹介した ESLint Config は eslint-plugin-import
、 eslint-plugin-promise
、 eslint-plugin-node
、 @typescript-eslint/eslint-plugin
、 eslint-plugin-jsx-a11y
、 eslint-plugin-react-hooks
、 eslint-plugin-react
に依存しています。よってこの ESLint Config を使用するには eslint
本体に加えてこれらのプラグインもインストールする必要があります。どちらが適切でしょうか。
eslint-config-airbnb
の場合
利用者側でこれらのプラグインを明示的にインストールさせる設計となっており、全て peerDependencies
として指定されています。
eslint-config-react-app
の場合
一方で eslint-config-react-app
は dependencies
として指定されているため、eslint-config-react-app
をインストールする際にこれらのプラグインも併せてインストールされる設計となっています。
ようするにこれらプラグインの管理を ESLint Config と利用者のどちらに任せるかという違いです。どちらが適切かは双方のメンテナンスコストを考慮して判断するのがよいでしょう。
Discussion