🫶

eslint-plugin-importで秩序のあるフロントエンド開発を実現する

に公開

はじめに

フロントエンド開発では、プロジェクトの設計思想やディレクトリ戦略が遵守されず、モジュール間の無秩序な依存関係が発生すると、モジュール単位の責務が曖昧になることでコードの可読性が低下し、意図しない不具合が発生したり、リファクタリングやスケールが困難になる弊害があります。

eslint-plugin-importno-restricted-pathsルールを活用することで、意図しない依存関係を静的解析によって防ぐことができます。

https://github.com/import-js/eslint-plugin-import

ディレクトリ戦略と依存関係の制限

今回はBulletproof-reactアーキテクチャのようなディレクトリ設計を想定します。

https://github.com/alan2207/bulletproof-react

src/
 ├── features/      # 特定の機能を提供するファイル群
 │   ├── hoge/
 │   ├── foo/
 ├── components/    # 特定のドメイン(機能)に依存しない汎用コンポーネント群
 ├── hooks/         # 特定のドメイン(機能)に依存しない汎用カスタムフック群
 ├── api/           # API通信層

このアーキテクチャの恩恵を最大限に受けるために、以下のようなプロジェクトルールを設定したいと仮定します。

  • features/間の直接的な依存関係を禁止したい
  • components/,hooks/,api/において、特定のfeatures/への直接的な依存関係を禁止したい
  • MUI(UI フレームワーク)から提供されるコンポーネントの直接的な参照を禁止し、components/で一度ラップしてから使用したい

features/間の依存制限

featuresは独立した機能単位のモジュールであり、互いに直接依存することは保守性の低下を招きます。そのため、features/間の直接的な参照を制限することで、より堅牢なアーキテクチャを構築できます。

汎用ファイルから features/ への依存制限

上記と同様の理由から、特定のドメインを持つべきではない汎用的なコンポーネント・関数などについてもfeatures/への直接的な参照を制限します。

特定ライブラリへの依存制限

ライブラリの用途によっては、プロジェクトドメインと密結合になることで保守性・拡張性の低下を招く可能性があります。今回の UI フレームワークの場合はcomponents/のみ経由させることで、ライブラリの恩恵を受けつつ、より明示的な管理が可能になります。

ESLint によるルール適用

ESLint へのルール追加はとてもシンプルで、基本的にtargetパスとfromパスを指定することで適用できます。

target contains the paths where the restricted imports should be applied.

from paths define the folders that are not allowed to be used in an import.

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

import importPlugin from "eslint-plugin-import";

const features = ["hoge", "foo"];

export default [
  {
    name: "import/custom",
    plugins: {
      import: importPlugin,
    },
    rules: {
      "import/no-restricted-paths": [
        "error",
        {
          zones: features.map((feature) => ({
            target: [`src/features/!(${feature})/**`],
            from: [`src/features/${feature}/**`],
          })),
        },
      ],
    },
  },
];

設定したルールに適合しない場合、エラーメッセージが表示されます。

// src/features/hoge/index.ts

// ✅ OK: features/hoge から features/hoge を参照
import { someFunction } from "./utils";

// ❌ NGケース: features/hoge から features/foo を参照
// Unexpected path "../foo/utils" imported in restricted zone.eslintimport/no-restricted-paths
import { someFunction } from "../foo/utils";

汎用ファイルからfeatures/への依存制限も同様で、任意のtargetfromパスを指定してください。

{
  zones: features.map((feature) => ({
    target: ["src/api/**", "src/components/**", "src/hooks/**"],
    from: [`src/features/${feature}/**`],
  })),
}

依存関係を単一のディレクトリのみに制限する場合は、targetに論理否定演算子を使用できます。

{
  zones: {
    target: ["src/!(components)/**"],
    from: ["node_modules/@mui"],
  },
}

まとめ

モジュール間の依存関係制御を ESLint に適用することで、秩序あるフロントエンド開発を実現する助けになります。

チーム開発において、新規メンバーやフロントエンド開発に慣れていないメンバーがコーディングする際に、適切な依存関係の指針を提供できる点が大きなメリットのひとつと思います。

Discussion