そうだ、import順を整理しよう。~ESLint-Plugin-Importの導入~

はじめに

 こんにちは!今年新卒で入社したK.Mです!今回の記事では、私がプロジェクトで使ったeslint-plugin-importについて紹介したいと思います!

eslint-plugin-importとは?

 eslint-plugin-importは、eslintのプラグインであり、import順に関するルールを設定することができます。settings.jsonの"source.organizeImports": "explicit"を用いることでも同様にimport順を並べ替えることができますが、こちらは細かいルール設定ができません。一方、eslint-plugin-importを用いることで、プロジェクトごとに細かなルール設定を行うことができます!

プラグインの導入

 プラグインのインストールについては、公式に書かれている通りに、以下のコマンドを実行することでインストールすることができます!

npm install eslint-plugin-import --save-dev

続いて、インストールしたプラグインを適用させるために、eslintrc.cjsファイルのpluginsのにimportを加えます。

module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parser: '@typescript-eslint/parser',
  plugins: ['react-refresh' , 'import'],//pluginsにimportを追加!
  rules: {
  // ここに色々なルールを書く
  },
}

後はルールを書いていくだけです!それでは、並べ替えに関するルールの書き方について見ていきたいと思います!

importの並び替え

並び替えに関するルールは、import/orderで以下のように記述することができます。

    "import/order": [
      "error", //他のeslintルール同様、errorとするかwarningとするか設定
      {
        groups: [//大まかな並び順を設定
          "builtin",
          "external",
          "sibling",
          "parent",
          "object",
          "type",
          "index",
        ],
        alphabetize: { order: "asc", caseInsensitive: false },//グループ内でアルファベット順に並べるかを設定
        "newlines-between": "never",// グループ間の改行に関する設定
        pathGroups: [// 特定パスのimportを任意のグループに配置するための設定
          {
            pattern: "@storybook/**",
            group: "external",
            position: "before",
          },
        ],
      },
    ],

上記のプロパティ以外にもいくつかありますが、今回はこちらのプロパティについて説明していきます!

groups

groupsプロパティは、あらかじめ定義されているグループ名をもとにした並び順を設定することができます。あらかじめ定義されたグループは以下のものがあります。

  • builtin
    • 組み込みモジュール(import fs from 'fs'import path from 'path'等)が該当します。
  • external
    • 外部モジュール(node_module等に格納されたモジュール等)が該当します。
  • sibling
    • 同階層あるいは子階層のファイルからimportされたものが該当します。
  • parent
    • 親以上のファイルからimportされたもの該当します。
  • object
    • importを用いたエイリアス付与の構文が該当します。(TypeScript限定)
  • index
    • 同ディレクトリのindexファイルが該当します。
  • type
    • typeインポートが該当します。(TypeScript限定)
  • unknown
    • 上記のグループすべてに該当しないもの

また、上記のグループ以外にプロジェクト内の特定のモジュールエイリアスやパス設定を使用して、内部モジュールのインポート順を制御するために使用するinternalというグループが存在します。internalは、デフォルトで該当するものはありませんが、後述するpathGroupで使用することで、任意のパスをinternalとして取り扱うことができます!

これらのグループ名を好きな順序で並べた配列を設定することで、importの並び順を制御します!例として、以下のようにgroupsを設定します。

        groups: [
          "builtin",
          "external",
          "sibling",
          "parent",
          "index",
          "object",
          "type",
        ],

 すると、次のようなimport順とする必要があります。

// 1. "builtin"グループ
import fs from 'fs';
import path from 'path';
// 2. "external" グループ
import _ from 'lodash';
import chalk from 'chalk';
// 3. "sibling" グループ
import bar from './bar';
import baz from './bar/baz';
// 4. "parent"グループ
import foo from '../foo';
import qux from '../../foo/qux';
// 5. "index"グループ
import main from './';
// 6. "object"グループ
import log = console.log;
// 7. "type" グループ
import type { Foo } from './foo';

また、グループ名は省略する事もでき、省略した場合は指定したものの後ろに配置する必要があります!

        groups: [
          "sibling",
          "parent",
          "index",
          "object",
          "type",
        ], //"builtin"と"external"を省略
// 1. "sibling" modules from the same or a sibling's directory
import bar from './bar';
import baz from './bar/baz';
// 2. modules from a "parent" directory
import foo from '../foo';
import qux from '../../foo/qux';
// 3. "index" of the current directory
import main from './';
// 4. "object"-imports 
import log = console.log;
// 5. "type" imports 
import type { Foo } from './foo';
// 6. node "builtin" modules and "external" modules
import fs from 'fs';
import path from 'path';
import _ from 'lodash';
import chalk from 'chalk';

ただし、指定していなくても別のグループに該当する場合、そのグループに準拠した並びになります!

        groups: [
          "sibling",
          "parent",
          "index",
        ], //"type"を削除
// 1. "sibling" modules from the same or a sibling's directory
import bar from './bar';
import baz from './bar/baz';
import type { Foo } from './foo';//この場合はsiblingとして扱われる
// 2. modules from a "parent" directory
import foo from '../foo';
import qux from '../../foo/qux';
// 3. "index" of the current directory
import main from './';

また、グループ名を配列にすることで、それぞれを同一のグループとして扱うことができます。

        groups: [
          ["sibling" , "parent"],
          "index",
        ], //siblingとparentを配列に変更
// 1. "sibling" と"parent"は同じ場所にまとめられる
import bar from './bar';
import baz from './bar/baz';
import foo from '../foo';
import qux from '../../foo/qux';
// 2. "index" of the current directory
import main from './';

 長くなりましたがgroupsプロパティは

  • あらかじめ定義されたグルーピングをもとに、並びをカスタマイズできる
  • 省略されたグルーピングは、他に該当するグルーピングがない場合は最後尾に並べる
  • 省略されたグルーピングでも、他に該当するグループがある場合はそちらに分類される
  • 配列とすることで、それらのグループを同じグループとして並べる

ことができます!

pathGroups

pathGroupsプロパティは、特定パスを任意のグループに設定することができます!上記のgroupsプロパティで大体並びを制御できますが、pathGroupsプロパティを以下のように書くことで、より細かい並びを設定することができます!

    "import/order": [
      "error", 
      {
        groups: [
          buildin,
          external,
          [parent , sibling],
          type
        ],
        pathGroups: [// 今回は"@storybook/**"となるものをすべてexternalとするように指定
          {
            pattern: "@storybook/**", //パスの設定
            group: "external", //groupsの設定
            position: "before", //groupsのどこに置くか設定。省略可
          },
        ],
      },
    ],

 このルールでのimportの並び順は次のようになります!

import type { Meta, StoryObj } from "@storybook/react";//type importであるがexternalとして扱われる
import { ComponentProps, useState } from "react";
import { HStack, VStack } from "styled-system/jsx";
import Hoge from "../components/Hoge";
//@storybook/react以外のtypeimportは通常のtypeimportとして、[parent , sibling]の後に配置される
import type {hogeType} from "../components/Hoge";

pathGroupsは、pathgroupspositionの3つのプロパティを持つオブジェクトの配列として設定します。配列になっているので、パスを複数種類設定したい場合はその数だけオブジェクトを追加することで、複数のpathGroupを設定することができます。 続いてそれぞれのプロパティについて、

  • path (必須)
    • グルーピングしたいオブジェクトの相対パスを書きます!エイリアスを使用することも可能です。
  • group(必須)
    • pathで指定したファイルパスに該当するものをどのgroupsとして扱うか設定します。
  • position
    • groupの中でどこに置くかを指定できます。"before"であればグループ内の先頭に、"after"であればグループ内の最後尾に置かれるようになります。必須ではないので、特に指定がない場合はグループ内のどこにおいてもよくなります!

先程例に挙げたpathGroupのルールでは@storybook/reactからインポートしたものはtypeインポートであってもexternalとして扱われ、かつposition : "before"を指定したのでexternalグループの先頭に置かれています!pathGroupsを特に指定しない場合は書かなくてもよいですが、個人的にはpathGroupsの機能がこのプラグインの真骨頂だと思っているので、ぜひ使うことをお勧めします!

alphabetize

alphabetizeプロパティは、同一グループ内の並べ方を設定します!並べ方といってもこちらはそこまで自由度は高くなく、import先のパスをもとにアルファベット順で並べるのみとなっています。

    "import/order": [
      "error", 
      {
          alphabetize : {order: asc,  caseInsensitive: false}
      },
    ],

 このルールでのimportの並び順は次のようになります!

import AComponent from "./components/AComponent";
import BComponent from "./components/BComponent";
//caseInsensitive: falseのため大小文字は区別して並べられる
import aComponent from "./components/aComponent";
//ファイルパスをもとにして並べるため、../となる場合は./より後に配置される
import AComponentInExternal from "../components/AComponentInExternal"

 alphabetizeはordercaseInsensitibeのプロパティを持つオブジェクトを設定します。(本当はもう一つorderImportKindというのもあるようですが、私の環境ではうまく作動しなかったので今回は割愛します😿)それぞれの説明ですが、

  • order
    • asc、desc、ignoreの3つの値をとり、それぞれ順番に昇順、降順、設定なしとなります。
  • caseInsensitibe
    • trueまたはfalseの値をとり、trueであれば大小文字関係なく昇降順に、falseであれば大小文字を考慮して昇降順に並べます。

 一点注意ですが、例にも挙げてる通りファイルパスを基準にアルファベット順に並べられるので、インポートモジュールの名前を基準に並べてもらえない点は気を付ける必要があります!

newlines-between

 最後に紹介するnewlines-betweenプロパティは、importステートメント間における空行に関するルールを追加します。newlines-betweenプロパティに指定できるルールは以下の通りです!

  • ignore(デフォルト)
    • 空行に関するルールを設けない
  • always
    • グループが変わるときに空行を追加することを強制することができます。後述するalways-and-inside-groupsと違って、同じグループのインポートステートメント間での改行はできなくなります。
  • always-and-inside-groups
    • グループ間に関する空行のルールはalwaysと同じですが、同じグループ間内の空行に関するルールはなくなります。
  • never
    • グループ間の空行追加もできなくなります。

個人的には、わざわざ設定するのであればneveralwaysをお勧めします!always-and-inside-groupsは同一グループの空行に関するルールがないため、逆に可読性が落ちるように思えました(笑)

まとめ

 今回はeslintのpluginであるeslint-plugin-importの簡単な使い方を紹介しました!苦労した点としては、公式ドキュメントにはgroupsプロパティの詳細な内容が書かれておらず、いろんな記事を読んだり、chatGPTに聞いてみたり、実際に手を動かして挙動を試してみたりと、かなり手探り状態でlintルールを設定しました(笑)。ただ、試行錯誤の末にプロジェクトに導入することができた瞬間は非常に達成感を感じました!また、学生のころから環境構築はIDEに頼ってきたうえ、大した環境構築をする機会がなかった私ですが、今回の取り組みを機に(ほんのちょっぴりだけですが)環境構築のやり方について理解が深まったような気がします。

 最後になりましたが、ここまで読んでくださってありがとうございます!この記事がplugin-importを導入で悩んでいる方の参考になれば幸いです!

KENTEMでは、様々な拠点でエンジニアを大募集しています!
建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。
recruit.kentem.jp career.kentem.jp