ESLint import でカスタムエイリアスに対応する

## 1. はじめに

Node.js プロジェクトでの import 文を自動でソートする際に、カスタムエイリアスの設定を変更したとき、そのままでは意図通りの挙動にならなかったのでその対応方法を記す。

## 2. 前提

### Node.js バージョン

```sh
$ node -v
#v16.10.0
```

### 各種 npm パッケージ(関係ありそうなもののみ列挙)

- `react`: ^18.2.0
- `eslint`: ^8.15.0
- `eslint-plugin-import`: ^2.26.0
- `eslint-plugin-react`: ^7.29.4

### ディレクトリ構造

```
src/
├── components
│   ├── atoms
│   │   ├── Foo.tsx
│   │   ├── Bar.tsx
│   ├── molecules
│   │   ├── Hoge.tsx
│   │   ├── Fuga.tsx
├── constant.tsx
..
```

### その他

- VSCode の設定で、ファイル保存時にフォーマットがなされるようにしておく。
- ESLint のセットアップ等については割愛する。

## 3. 実践

### 3.1. `src/*` に対して `@/*` というエイリアスをあてる

#### 設定
`tsconfig.json` でエイリアス設定を行う。

```tsconfig.json
{
  ..
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@/*": ["./*"],
    },
  },
  ..
}
```

`.eslintrc.json` で `import/order` により import 文のソート設定を行う。


```.eslintrc.json
{
  ..
  "plugins": ["import", "react"],
  "parserOptions": {
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "ignorePatterns": [".eslintrc.json", "tsconfig.json"],
  "rules": {
    "sort-imports": 0,
    "import/order": [
      2,
      {
        "alphabetize": { "order": "asc" },
        "newlines-between": "always",
      }
    ]
  }
}
```

#### フォーマット

適当なファイルを保存した時のソート例を示す。

```tsx
import React from 'react';

import { Foo } from '@/components/atoms/Bar';
import { Bar } from '@/components/atoms/Foo';
import { Fuga } from '@/components/molecules/Fuga';
import { Hoge } from '@/components/molecules/Hoge';
import { piyo } from'@/constant'; 
```

`eslint-plugin-import` では _groups_ というものがあり、import する対象によって分類され、まずそれ単位でソートされる。[^1]
`import React from 'react'` は `external` という group に該当し、その他は `internal` の group に属する。先に react が import されているのは、この group の優先度がデフォルトでは、 **external > internal** となっているためである。
また、各 group の中では、今回はアルファベット順(昇順)にソートされるように `.eslintrc.json` で設定したため、そのような並び替えになっている。


[^1]: https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md#groups-array


この例では、問題のなさそうなソートが実現できているが、次のような場合を考える。


### 3.2. `src/components/*` に対して `@@/*` というエイリアスをあてる

`src/components/*` が肥大化してきたため、`src/*` の他の部分と区別したエイリアスを設定したくなったとする。

#### 設定

```diff_json
{
  ..
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@/*": ["./*"],
+     "@@/*": ["./components/*"],
    },
  },
  ..
}
```

#### フォーマット

先ほどと同じファイルを保存してみる。
この時、エイリアスを追加したので、`@/components` → `@@/` の変更を行なって保存をする。

```tsx
import { Foo } from '@@/atoms/Bar';
import { Bar } from '@@/atoms/Foo';
import { Fuga } from '@@/molecules/Fuga';
import { Hoge } from '@@/molecules/Hoge';
import React from 'react';

import { piyo } from'@/constant'; 
```

今回は、react の import 文よりも先に `components/*` の import 文が来てしまっている。好みの問題だが、できれば先に react などの external なものは先にソートされてほしい。

そこで、`.eslintrc.json` に以下の変更を加える。

```diff_json
{
  ..
  "plugins": ["import", "react"],
  "parserOptions": {
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "ignorePatterns": [".eslintrc.json", "tsconfig.json"],
  "rules": {
    "sort-imports": 0,
    "import/order": [
      2,
      {
        "alphabetize": { "order": "asc" },
        "newlines-between": "always",
+       "pathGroupsExcludedImportTypes": ["builtin", "object"],
+       "pathGroups": [
+         {
+           "pattern": "@@/**",
+           "group": "external",
+           "position": "after"
+         }
+       ]
      }
    ]
  }
}
```

`pathGroups` で、特定のパスに対してソートする位置を指定することができる。今回は、`@@/**` というパスの import 文を `external` groups の **後(after)** に来るように設定している。
ところが、 `@@/` は external group に属し、この group に対してはデフォルトでは `pathGroups` の設定が有効にならないようになっている(`pathGroupsExcludedImportTypes` に `external` が含まれている)。[^2]そこで、`pathGroupsExcludedImportTypes` からこれを外すような設定も必要となる。
以上により、次のようなソート結果となる。

[^2]: `@/` だと external にならずに、`@` や `@@/`だと external group になるようだが、裏は取れていない

```tsx
import React from 'react';

import { Foo } from '@@/atoms/Bar';
import { Bar } from '@@/atoms/Foo';
import { Fuga } from '@@/molecules/Fuga';
import { Hoge } from '@@/molecules/Hoge';

import { piyo } from'@/constant'; 
```

これで違和感のない自然なソート順序となった。


## 4. :book: 参考にさせていただいたページ

https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md

https://zenn.dev/riemonyamada/articles/02e8c172e1eeb1