FSDのためのESLintプラグイン開発日誌

challengeprojectfsdopensource
🤔現在公開・管理中のオープンソースです。

以下のリンクから使用できます。



背景

航海プラスカリキュラム
航海プラスカリキュラム

航海プラスの過程の中に「クリーンコード」教育がある。

過程を受けながら、リファクタリングの核心要素や、コード品質を高めるための方法を学んだ。

そして、その過程でFeature-Sliced-Design(FSD)の概念を学び、適用してみた。

強制されるルールが多く、プロジェクトやNAVERウェブトゥーンインターンをしながら経験中の実務でこれを適用しようとしたが、適切なLintプラグインがなくて不便さを感じた。

そこで、これを開発することに決めた。

問題状況

もしツールを使うなら、無条件に作ろうとするよりは、既にあるものを活用した方が良い。

私が持っている開発哲学の一つだ。ソフトウェアの長所の一つは「再利用性」だ。だから、リサーチ過程を必ず経るようにしている。

公式ドキュメントではSteigerというツールを推奨していた。

しかし、私が必要な設定はなく、ESLintではない別のライブラリなのでインストールして管理しなければならないという困難があった。

比較項目Steiger(別途CLIツール)ESLintプラグイン
インストールとメンテナンス- プロジェクトに別途ツール(クライアント/サーバー)のインストールが必要
- 追加の依存関係とバージョン管理が必要
- ツールアップデート時にチーム全体に再インストールの案内が必要
- 既存のESLint構成にプラグインを追加するだけ
- 別途ツール管理の負担が軽減
- ESLintのバージョン管理だけ気にすれば良い
開発ワークフローとエディタ統合- 専用CLIを実行する必要があるため、IDE内での即座のフィードバックが難しい場合がある
- エディタごとにSteiger関連プラグインがあるかもしれないが、普遍的ではない可能性がある
- VSCode、WebStormなどほぼすべてのIDEでESLintをすぐに連携
- リアルタイムでリントエラーを確認可能
- GitHooks/CIパイプラインで別途設定なしに自動検査可能
ルール制御と細分化(リントルール)- 提供されるルールを基に使用する必要がある
- ルールの細分化や直接的なカスタマイズ(オープンソース貢献を除く)は制限的
- CLIベースなので、エディタ内のオートフィックス(autofix)機能の使用が難しい場合がある
- ESLintのRuleシステムをそのまま活用可能
- ルールの細分化やエラー/ワーニングなど重大度設定が自由で、例外処理を簡単に構成
- AST分析を通じた深いルール作成が可能
- ESLint autofixなど自動修正の活用が可能
オンボーディングと教育- 新しく入ったチームメンバーにSteigerツールのインストールと使用法を追加で案内する必要がある
- ESLint経験しかない人には別途ツールの学習負担
- ほとんどの開発者がESLintに慣れている
- 「ESLintルールが追加された」程度の案内で済むため学習負担が最小化
- マイグレーションとチームの合意が容易
拡張性とカスタマイズ- ツール自体が提供するオプション、バージョンを考慮する必要がある
- 高度化や修正が必要な場合、ツール自体のアップデートを待つ必要がある
- オープンソース貢献プロセスが複雑になる可能性がある
- オープンソースとしてリリース時、迅速なフィードバックと貢献が可能
- TypeScript拡張時に@typescript-eslint/parserなどと簡単に連携
- プロジェクトの要件に合わせてルールを追加/変更しやすい

上記のような理由で、ESLintプラグインをリサーチした。

検索する過程でいくつかのツールを見つけた。しかし、これらはそれぞれ問題を抱えていた。

ツール説明不便な点
Eslint plugin for FSD best practicesFSD BPを基準に作られたESLintだ。最終管理が4〜5年前で、管理がきちんと行われていない。

そのため、ESLint 9以上の最新バージョンで強制されるFlat Configへの対応が不足していた。
ESLint plugin fsd importおそらく個人が使用するために作られたLintのようだった。管理がきちんとされておらず、機能も制限的だ。

信頼性が低く、使用が難しかった。
@feature-sliced/eslint-config135個のスターと8000人以上がダウンロードしたFSD用Lintだ。きちんとした管理が2022年で、Flat Configへの対応がされていなかった。

この他にもいろいろなツールを探してみたが、ぴったりくるプラグインがなかった。

必要な要素の中で、特に核心的に扱うべき部分がFlat Configへの対応だった。

ESLint 8.xバージョン以前のサポートが終了することを告知している。
ESLint 8.xバージョン以前のサポートが終了することを告知している。

特に、公式ドキュメントでESLint 8.xバージョン以前のサポートが終了すると言及しているので、新規プロジェクトではESLint 9以上の使用が必要だった。

私が見つけた方法では、特定のESLint PluginFlat Configを提供しない場合、プラグイン自体のコードを修正しない限り、これをFlat Configに適用する方法がなかった。

そこで、これを解決するために直接ESLint Pluginを作ることに決めた。

プロジェクト目標

  • Feature-Sliced-Designのコンベンションを遵守するESLint Pluginを作る。
  • Flat Configをサポートする。
  • ESLint Pluginを作りながら、ESLintの動作方式を理解する。

実際、既存の設定を持ってきて使うだけで、ESLintについてきちんと理解していなかった。

そこで今回のチャレンジを通じて、ESLintについてきちんと理解し、それを通じてFeature-Sliced-Designのコンベンションを遵守するESLint Pluginを作ってみることにした。

計画

初めて試みるチャレンジだったので、すべてが新しく、すべてが障壁だった。

足りないものが多いので、戦略的にアプローチする必要があった。

目標を樹立し、これを効果的に達成するために計画を立てた。

計画内容
1. 基本概念の理解と環境準備- ESLintの基本概念と動作方式を学習。
- ESLintプラグインを作るための基本環境設定。
- NPMパッケージおよびモジュール管理の基本学習。
2. ESLintルールの基本構造理解- 簡単なカスタムルールを作成してみる。
- ESLintのAST(Abstract Syntax Tree)とESLint Rule APIの理解。
3. Feature-Sliced Designの理解、
関連ルールの設計と実装
- Feature-Sliced Designの構造と哲学を学習。
- 設計原則をESLintルールに変換する方法を構想。
- FSDのためのルール草案を作成。
4. ESLintプラグイン開発- 複数のルールをプラグインとしてパッケージング。
- プラグインのテストとデバッグ。
5. デプロイ準備とテスト- プラグインのドキュメント作成。
- プラグインをNPMにデプロイする過程を学習。
6. 実際のプロジェクトに適用してテスト- 実際のプロジェクトに適用しながら、正常に動作するかユーザビリティテストを実施。

GPTと一緒に立てたカリキュラムだ。これを基にゆっくり従いながらプロジェクトを進めることにした。

本文もこの過程通りに進める予定だ。

1段階:基本概念の理解と環境準備

目標

  • ESLintとPrettierの役割と動作方式を理解する。
  • ESLintプラグイン開発のための基本環境を設定する。
  • NPMを使用したパッケージ管理と開発環境設定を身につける。

ESLintとは何か?

始める前に、ESLintが何か簡単に見てみよう。

Find and fix problems in your javascript code.

ESLint公式ドキュメントより

上記の説明で確認できるように、JavaScriptコードで問題を見つけて修正するツールだ。

ESLint statically analyzes your code to quickly find problems. It is built into most text editors and you can run ESLint as part of your continuous integration pipeline.

ESLint公式ドキュメントより

ESLintはコードを静的に分析して素早く問題を見つけることと、一貫したコード品質の維持を目的としている。

ライブラリやフレームワークに依存せず、ほとんどのテキストエディタで使用できる。

また、CIパイプラインでもESLintを使用できる。


ESLintの主要機能まとめ

ESLintの主要機能をまとめると次のようになる。

機能説明
文法エラー検出コードで発生可能なエラーを捕捉する。
コードスタイルチェックチームルールやコードスタイルを維持するのを助ける。
自動修正(Auto-fix)可能な場合、問題を自動的に修正する。
拡張可能プラグインやルールを追加して望み通りに拡張できる。

ESLintが重要な理由

ESLintはコード品質を維持し、チームメンバー間のコードコンベンションを一貫して維持するのに重要な役割を果たす。

特に、大規模プロジェクトではこのようなツールが必須だ。

デバッグ時間を削減し、コンベンションに関連する不必要なリソースの浪費を防ぐ。

つまり、生産性を高めるツールなので重要だと言える。


ESLintの動作原理

ESLintは次のような過程で動作する。

1. コード入力

ESLintは検査するJavaScriptまたはTypeScriptファイルを読み込む。

2. パース(Parsing)

コードを分析するためにパーサー(Parser)を使用してAST(Abstract Syntax Tree、抽象構文木)に変換する。

基本的にESLintはespreeというパーサーを使用するが、TypeScriptをサポートするには@typescript-eslint/parserのようなカスタムパーサーを使用することもできる。

ASTはコード構造をツリー形態で表現するデータ構造だ。

JavaScript

例として、このコードは次のようなASTに変換される。

Program
 ├── VariableDeclaration (const sum)
 │    ├── VariableDeclarator
 │    │    ├── Identifier (sum)
 │    │    ├── ArrowFunctionExpression
 │    │         ├── Parameters (a, b)
 │    │         ├── Body (BinaryExpression +)

3. ルール(Rules)適用

ESLintはASTを巡回しながら設定されたルール(Rules)を適用する。

ルールはノードタイプ(Node Type)に従って動作し、特定のパターンが発見されるとエラーを報告したり、自動的に修正したりできる。

例えば、no-consoleルールが有効化されていれば、console.log()のようなコードがあるとESLintが警告を表示する。

このとき、ESLintは各ノードに対してビジター(Visitor)パターンを使用してルールを検査する。

JavaScript

このルールはtempという変数を使用するとESLintが警告を表示する簡単な例だ。

🤔ESLintがビジターパターンを使用する理由

オブジェクト(Element)を維持したまま新しい機能(検査ルール)を追加するため

ESLintはコード(AST)を直接変更しない。

代わりに、特定のノード(オブジェクト)に対して新しい機能(ルール適用)を実行できるようにcreate(context)の中でビジター関数を定義する。

各ノードタイプを訪問(Visit)しながら特定の作業を実行するため

ビジターパターンではVisitorオブジェクトがオブジェクト(Element)を巡回しながら特定の作業を実行するが、ESLintでは各ASTノードタイプごとに訪問できるメソッドを提供する。

例のコードでIdentifier(node)関数はASTでIdentifier(変数名)ノードを訪問するときに実行されるビジター(Visitor)の役割を果たす。

4. レポーティング(Reporting)

  • 検査結果をESLintがコンソールやファイルに出力する。
src/index.js
  2:5  error  Unexpected console statement  no-console
  • 2:5 → 2行目、5番目の位置で問題が発生
  • error → エラーレベル
  • Unexpected console statement → ルールで定義されたメッセージ
  • no-console → 適用されたESLintルール名

5. 自動修正(Auto-fixing)

  • 一部のルールは自動的に修正できる。(--fixオプションを使用)
Bash

例えばsemi(セミコロン強制)ルールが適用されている場合:

JavaScript

自動修正後:

JavaScript

ESLintの主要コンポーネント

ESLintは次のような主要コンポーネントで構成されている。

1. Parser(パーサー)

  • コードをASTに変換する役割をする。
  • 基本的にespreeを使用するが、TypeScriptやJSXをサポートするには別のパーサーを指定する必要がある。
    • TypeScriptサポート:@typescript-eslint/parser
    • Babelサポート:babel-eslint

2. Rule Engine(ルールエンジン)

  • ASTを巡回しながらルールを実行・適用する役割をする。
  • ESLintには内蔵ルールがあるが、カスタムルールを追加することもできる。
  • eslint-pluginを通じてユーザー定義ルールを追加できる。

3. Formatter(出力フォーマッター)

  • ESLintの結果をコンソールやファイルに出力する役割をする。
  • 基本的にstylishフォーマッターを使用するが、--formatオプションを通じてJSON、HTML、Markdownなど様々なフォーマッターを使用できる。
  • 例)eslint src --format json

4. Fixer(自動修正機)

  • --fixオプションを使用すると、ESLintが一部のルールを自動的に修正してくれる。
  • ただし、論理的な問題は修正できない。(例:no-unused-varsのような問題)

ESLint設定ファイルの役割

ESLintの動作方式は設定ファイルを基に決定される。

JavaScript
  • files:特定のファイル拡張子(.ts、.tsx)に対するルールを適用。
  • languageOptions.parser:AST(Abstract Syntax Tree)変換のために@typescript-eslint/parserを使用(TypeScriptコード分析用)。
  • plugins:@typescript-eslint/eslint-pluginを追加してTypeScript専用ESLintルールを有効化。
  • rules:コードスタイルとコード品質ルールを設定。

ESLintの動作を深く理解するための追加概念

1. AST(Abstract Syntax Tree、抽象構文木)を利用したコード分析

ESLintはASTを基に動作するので、ASTを直接確認するとルール作成が容易になる。

  • AST Explorerのようなツールを活用してコードのASTを視覚的に確認できる。

2. プラグインと拡張機能

ESLintはプラグインを追加して機能を拡張できる。(私が本プロジェクトを通じて作りたい要素だ。)

  • 例:React用プラグインの追加
Bash
  • 設定ファイルに追加
JavaScript
  • files/*.jsx、/*.tsxファイルにReact関連ESLintルールを適用。
  • plugins:eslint-plugin-reactを追加してReact関連ルールを有効化。
  • rules:react.configs.recommended.rules → React推奨ルールをそのまま適用(plugin:react/recommendedと同じ)。

基本開発環境の準備

1. Node.jsのインストール

ESLintはNode.js環境で動作するため、Node.jsをインストールする必要がある。

Node.js公式サイトからLTSバージョンをダウンロードしてインストールする。


2. プロジェクトディレクトリの作成

ESLintプラグインを開発するプロジェクトディレクトリを作成する。

Bash

3. NPMプロジェクトの初期化

NPMプロジェクトを初期化する。

Bash

これでpackage.jsonファイルが生成される。このファイルはプロジェクトの設定と依存関係を管理する。

npm initコマンドの実行結果
npm initコマンドの実行結果

4. ESLintのインストール

ESLintをインストールする。

Bash

--save-devオプションを使用して開発依存関係としてインストールする。(開発環境でのみ使用するという意味で、ビルド時には含めないという意味だ。)


5. ESLintの初期化

ESLintを初期化する。

Bash

このコマンドを実行すると、ESLint初期化設定が開始される。

  • "How would you like to use ESLint?" → "To check syntax, find problems, and enforce code style"を選択。
  • "What type of modules does your project use?" → "JavaScript modules (import/export)"を選択。
  • "Which framework does your project use?" → "None of these"を選択(Reactは後で必要に応じて追加できる)。
  • "Does your project use TypeScript?" → "No"を選択(最初はJavaScriptで進めるため)。
  • "Where does your code run?" → "Node"を選択。
  • "Would you like to install them now?" → "Yes"を選択。
  • "Which package manager do you want to use?" → "npm"を選択。
ESLint初期化設定
ESLint初期化設定

Prettierのインストール

Prettierはコードスタイルを自動的にフォーマットしてくれるツールだ。

ESLintと一緒に使用するとコード品質とスタイルの両方を管理できる。

これについては最後に扱う予定なので、まずはインストールだけしておこう。

Bash

ディレクトリ構造の設計

初期に作業するディレクトリ構造はシンプルに維持する。

eslint-fsd-plugin/
├── node_modules/
├── .eslintrc.js
├── package.json
├── package-lock.json
├── src/
└── README.md

2段階:ESLintルールの基本構造理解

目標

  • ESLintがコードを分析する方式(パース、AST活用)を理解する。
  • 簡単なESLintルールを直接作ってみる。
  • ルールを適用して実行する方法を身につける。

ESLintルールをもう一度思い出す

前ですでにESLintがどのように動作するかを詳しく扱ったが、プロジェクト進行のために重要な内容なのでもう一度思い出してみよう。

ESLintは単純な文字列比較ではなくAST(Abstract Syntax Tree、抽象構文木)を基に動作する。

ASTとは?

ASTはコードの構造をツリー形態で表現したものだ。

例えば以下のコードを見てみよう。

JavaScript

このコードのASTはおおよそ次のように表現される。

Program
├── VariableDeclaration (const message = "Hello, world!")
│   ├── Identifier (message)
│   └── Literal ("Hello, world!")
└── ExpressionStatement (console.log(message))
    ├── MemberExpression (console.log)
    └── Identifier (message)

このようにASTを通じてコードの構造をツリー形態で表現する。

このツリーを利用してESLintはコードを分析しルールを適用する。

簡単なESLintプラグインを作る

原理は理解したが、これを活用してどうルールを作れるのか見当がつかない。

そこで、簡単なルールを作りながら実習してみようと思う。

前の例で記述したconsole.logを使えないようにするルールを作ってみよう。

1. ルールディレクトリの作成

まず、ルールを保存するディレクトリを作成する。

Bash

ここで-pオプションは親ディレクトリを自動的に作成するオプションだ。

例えば、src/rulesフォルダを作ろうとしてsrcが存在しない場合、mkdir -pを使用するとsrcから自動的に作成してくれる。


2. ルールファイルの作成

ルールを保存するファイルを作成する。

Bash

3. ルールコードの作成

では、no-console-log.jsファイルにルールコードを作成する。

JavaScript

このコードは次のような役割を果たす。

  1. ESLintはCallExpressionノードを見つける。-> これは関数呼び出しを意味する。
  2. もしconsole.logを呼び出した場合:
    • node.callee.object.name === "console"
    • node.callee.property.name === "log"
  3. 上記の条件を満たすとESLintが警告メッセージを表示する。

4. ルールを基にプラグインを作る

ではeslint.config.mjsで私たちが作ったルールを登録する番だ。

Flat Configでは既存の.eslintrc.js方式とは異なり、プラグインを明示的に登録し、ネームスペースを使用してルールを呼び出す必要がある。

JavaScript

上記のようにplugins属性にプラグインを登録し、[プラグイン名]/[ルール名]形式でルールを呼び出す。

そのため、no-console-log.jsのようなルールを作ったとしても、プラグインは別途設定する必要がある。

現在はルールだけを作ったのであり、プラグインを作ったわけではない。
ESLintはカスタムルール適用のためにプラグインを要求する。
したがって、プラグインを作ってルールを適用する必要がある。

(1) プラグインを作る

index.jsルール(rules)をプラグイン形式で包む必要がある。

Flat Configではプラグイン内でrulesオブジェクトを定義することが必須だ。

このときファイル名はindex.jsでなくても良い。(例:plugin.js

しかし、慣例上プラグインの始点でもあるため、index.jsという名称を使用した。

次のように作ってみよう。

JavaScript

上記のようにrulesオブジェクトを作ってルールを登録する。

ここで重要な点は以下の通りだ。

  • rulesオブジェクトを作ってルールを登録する。
  • rulesオブジェクトを含めてエクスポートする必要がある。
  • export default { rules: { "no-console-log": noConsoleLog } };形式で設定すると、Flat Configで正しく認識される。

5. Flat Configで作ったプラグインを使う

eslint.config.mjsでプラグインを明確に登録し、ネームスペースを設定する必要がある。

ファイルを開いて以下のように追加する。

JavaScript
ここで重要な点
  • pluginsオブジェクト内にfsd: eslintFsdPlugin形式でプラグインを登録する必要がある。
  • rulesで必ず"fsd/no-console-log"のようにネームスペース(fsd/)を付けて呼び出す必要がある。
    • "no-console-log" → ❌ エラー発生
    • "fsd/no-console-log" → ✅ 正しい方式

🤔eslint.config.mjsに既に何かあるのですが...?
JavaScript

上記のように既に内部に値が入っていることがある。

これは上の結果物の説明項目で記述した内容で、

Node.js環境や基本的なJavaScriptルールを適用する設定だ。

この設定はそのままにして、新しく作ったルールを追加すればよい。

JavaScript

Flat Configは配列方式で複数の属性を連結して適用できる。

そのため、上記のように適用すればよい。

JavaScript

ちなみに上記のコードはTypeScriptの型定義を意味する。

これによってTypeScriptが該当変数がLinter.Config[]型であることを知ることができる。

TypeScriptを使用しない場合は削除しても良いが、どうなるか分からないので、できればそのままにしておくことを推奨する。


5. ESLint実行テスト

ではconsole.logが含まれたファイルを作って実行してみよう。

(1) テスト用ファイル作成
Bash

そしてtest.jsファイルに以下のコードを追加する。

JavaScript
(2) ESLint実行
Bash
(3) 実行結果確認
ESLint実行結果
ESLint実行結果

正常にエラーが出力されることを確認できる。

6. npx eslint実行時のデバッグ

すべての設定を正しく適用した後、ESLintがプラグインを正常にロードしているか確認する必要がある。

Bash
デバッグ結果で確認すべき点
  1. Loaded plugin: fsd
    • Loaded plugin: fsdログがあれば、プラグインが正常にロードされたことを意味する。
  2. Applying rule: fsd/no-console-log
    • Applying rule: fsd/no-console-logログがあれば、ルールが適用されていることを意味する。
  3. ESLint実行結果
    test.js
      1:1  error  console.logは使用しないでください。  fsd/no-console-log
    • 上記のようにエラーが発生すれば、正常に動作していることを意味する。

7. まとめ

この段階では以下の内容を進めた。

段階修正事項説明
1️⃣ ルール定義修正rules/no-console-log.jsでmeta.messagesとcontext.report()を正しく設定Flat Configではmeta情報が必須
2️⃣ プラグイン設定index.jsでrulesオブジェクトを含めてエクスポートrules: { "no-console-log": noConsoleLog }形式で定義
3️⃣ Flat Config適用eslint.config.mjsでplugins.fsdとして登録し、"fsd/no-console-log"形式でルール適用Flat Configではpluginsオブジェクトが必須
4️⃣ ESLintデバッグnpx eslint --debug src/test.js実行Loaded plugin: fsdおよびApplying rule: fsd/no-console-log確認
  1. Flat ConfigではESLintプラグインをネームスペースとして登録する必要がある。
    • "no-console-log" → ❌ エラー発生
    • "fsd/no-console-log" → ✅ 正しい方式
  2. rulesオブジェクトをindex.jsでプラグインとしてエクスポートする必要がある。
    • export default { rules: { "no-console-log": noConsoleLog } }; → ✅ 正しい方式
  3. ESLint実行前にnpx eslint --debugでプラグインが正常にロードされているか確認する必要がある。
    • Loaded plugin: fsdログを必ず確認すること。

3段階:Feature-Sliced Design(FSD)ベースのESLintルール設計と実装

ここまで簡単なESLintルールを作ってみた。

これからはFeature-Sliced Design(FSD)を理解し、それに対するルール設計を進めていこう。

目標

  • Feature-Sliced Design(FSD)の概念をもう一度整理する。
  • FSD構造でESLintルールがどのような役割を果たすべきかを設計する。
  • 基本的なFSDルールを直接実装してみる。

Feature-Sliced Design(FSD)とは?

Feature-Sliced Design(FSD)概念
Feature-Sliced Design(FSD)概念

FSDはプロジェクトを機能(feature)中心に構造化して拡張性と保守性を高めるアーキテクチャだ。

フロントエンドプロジェクトを機能中心に構成して保守性と拡張性を最大化するアーキテクチャパターンだ。

従来のpagescomponentsservicesのようなフォルダベースの構造とは異なり、ビジネスドメインと機能(feature)に従って構造化することが核心だ。

FSDの核心原則

1. 機能中心の構造(Feature-Oriented Architecture)

  • 従来のレイヤーベース構造(例:components/services/フォルダにすべてのコンポーネントとサービスが集まる構造)とは異なり、各機能単位でフォルダを分け、その機能に関連するすべての要素(UI、状態、API呼び出し、ロジックなど)を一箇所に配置する方式に従う。
  • 例えば、認証(Authentication)に関連するすべての要素をfeatures/auth/内部に配置し、決済(Payment)関連要素はfeatures/payment/に配置する方式だ。
  • 機能単位でグループ化すると再利用性が増加し、保守が容易になる。

REST APIのようにディレクトリ自体でコードが何をするのかを明確に意味を表す効果がある。

例えばfeatures/auth/ディレクトリは認証処理に関連するすべてのコードを含み、features/payment/ディレクトリは決済処理に関連するすべてのコードを含む。

entities/auth/ディレクトリの場合は認証情報に関連するロジックの処理に関連するコードを含む。

このようにディレクトリ名だけを見ても、どの機能を担当するのかを簡単に把握できるという利点がある。


2. レイヤーベースアーキテクチャ(Layered Architecture)

  • FSDは7つのレイヤーで構成され、上位レイヤーは下位レイヤーを参照できるが、逆に下位レイヤーが上位レイヤーを参照することは禁止されている。(上方向の依存性防止)
  • ✅ 正しい参照関係
    App → Process → Pages → Widgets → Features → Entities → Shared
  • ❌ 間違った参照例
    Features → Pages (X)  // FeaturesレイヤーがPagesを参照してはいけない
    Entities → Features (X)  // EntitiesがFeaturesを参照してはいけない

このレイヤー構造のおかげで、FSDはコード間の結合度を減らし、保守性を最大化できる。


3. 正しい依存性管理(Dependency Rules)

  • FSDでは明確な依存性規則を遵守する必要がある。
  • 特に、importする際に上位要素を参照できないように強制することが核心だ。

規則:上位レイヤーを参照できない
  • features/内部ではpages/またはwidgets/のコードをimportできない。
  • entities/内部ではfeatures/またはwidgets/をimportできない。
  • ✅ 正しいimport例
TypeScript
  • ❌ 間違ったimport例
TypeScript

この原則を守れば、下位モジュールが上位モジュールに依存しないため結合度が低くなり、機能を独立して保守しやすくなる。


4. ドメイン中心設計(Domain-Driven Design, DDD)

  • 各ドメイン(例:ユーザー、商品、決済など)を機能単位でグループ化して設計する。
  • 従来のMVCパターンではモデルとサービスが別々に存在するが、FSDでは機能(feature)中心に関連するすべての要素(UI、状態、API、ロジックなど)を一箇所に集めて配置し、ドメイン駆動設計と類似したアプローチをとる。

こうすることでコード探索が容易になり、機能単位で保守が簡単になるという利点がある。


5. 再利用性とモジュール化の増加

  • Sharedレイヤーに共通的に使用されるユーティリティ、スタイル、API関数、基本コンポーネントなどを配置して重複を防止し、プロジェクト全般で簡単に再利用できるようにする。
  • コンポーネント、状態管理、APIリクエストをFeatures単位でモジュール化して独立した開発が可能になる。
  • 例:
    /shared/
      ├── api/
      ├── ui/
      ├── utils/
  • 例えば、すべてのページで再利用できるButtonコンポーネントはshared/ui/Button.tsxに位置する必要がある。

6. 保守性と拡張性の増加

  • FSDの構造に従えば、プロジェクトが大きくなっても保守が容易になる。
  • 新しい機能を追加する際、既存のコードに影響を与えずに独立して開発できる。
  • リファクタリングが容易 → 特定の機能だけを修正してもプロジェクト全体に影響を与えない。
  • チーム協業が容易になる → 機能単位で作業を分けて進行可能だ。

特に、FSDは文書と関連例が非常に詳細だ。

もしこれを導入するならば、チーム内でアーキテクチャに関して考えの同期化のためのコストを大幅に削減できる。

詳細な文書に基づいて判断を下せばよく、曖昧なことだけ合意すればよいからだ。


7. まとめ

利点説明
コードの可読性と保守性が高まる機能別にコードが整理されているため探しやすい
拡張性に優れているプロジェクトが大きくなるほどFSDの利点が最大化される
依存性が明確になる下位レイヤーから上位レイヤーを参照できないように強制することで結合度を下げる
ドメイン中心開発が可能になるアプリケーションが実質的なビジネスロジックとよく合う
リファクタリングが容易になる機能(feature)単位で修正できるため安定した変更が可能
チームメンバー間の協業が容易になる各機能単位で作業を分けて進行できる


FSDでESLintルールが必要な理由

FSDは機能中心でプロジェクトを構造化して拡張性と保守性を高める方式だ。

しかし、開発者がこれを一貫して維持しなければ、むしろ混乱を招く可能性がある。

問題タイプ問題点
❌ 間違ったレイヤーimportfeatures/からapp/をimportfeaturesはappを知るべきではないが、依存性が生じる
❌ 間違ったパスimportimport Button from "../../shared/ui/Button";相対パス(../../)でimportすると保守が難しくなる
❌ 間違ったSlice間の依存性features/authがfeatures/paymentを直接importお互いに独立であるべきだが、強い依存性が生じる
❌ 間違ったグローバル状態アクセスfeatures/からapp/store.tsをimportfeatureはglobal storeを直接知るべきではない
❌ 間違ったUI要素importentities/からwidgets/をimportビジネスロジック(entities)はUIを知るべきではない

上記のような問題が開発者がよく起こしがちなミスだ。

私はどんな規則を作るのか?

FSDの原則を破るコードを監視する規則を作る。

そして、当然ながらこの規則はFlat Configを基準に作り、プラグインを作って適用する。

オプションは基本的にerrorに設定する。

しかし、当然ながらrulesはユーザーが自由に修正できるので、ユーザーが必要に応じてこれをwarnしたりoffしたりできるように実装する予定だ。


私が作るESLintルールリスト

ルール名説明例(間違ったコード)
forbidden-imports上位レイヤーimportおよびcross-import禁止features/app/ import禁止
no-relative-imports相対パスimport禁止、import時に@shared/uiのようなaliasを使用するよう強制import Button from "../../shared/ui/Button";
no-public-api-sidesteppublic API(index.ts)迂回import禁止import { authSlice } from "../../features/auth/slice.ts";
no-cross-slice-dependencyfeature slice間のimportを禁止(features/authfeatures/payment ❌)import { processPayment } from "../payment";
no-ui-in-business-logicビジネスロジックでのUI import禁止(entitieswidgets防止)import { Header } from "../../widgets/Header";
no-global-store-importsグローバルstore直接import禁止import store from "../../app/store.ts";
ordered-importsimport整列(レイヤー別グループ化)import { Button } from '@shared/ui'; import { User } from '@/entities/user';
  • ルール名はFSDのリンターであるsteigerを参考にして作成した。
  • forbidden-importsを除けば重複する要素なく、steigerでサポートされていない部分についてリンティングをサポートするように設計した。
  • 今後steigerにある内容を追加してみる予定だ。

各ルールの必要性説明

1. forbidden-imports:正しいレイヤー間依存性の適用
  • 上位レイヤーから下位レイヤーをimportすることを禁止する。
  • 例えば、features/からapp/をimportすることを防止する。
  • このルールを通じてFSDのレイヤー構造を守るように誘導する。
TypeScript

2. no-relative-imports:パスalias使用の強制
  • 相対パス(../../)を使用することを禁止し、alias(@shared/ui)を使用するよう強制する。
  • コードの可読性を高め、保守を容易にするためだ。
TypeScript

3. no-public-api-sidesteppublic APIを通じたimportの強制
  • 各slice(featuresentitieswidgets)の内部ファイルを直接importしないよう強制する。
  • 常にindex.tsを通じてimportするよう制限する。
TypeScript

4. no-cross-slice-dependency:Slice間の依存性制限
  • feature slice間のimportを禁止する。
  • features/authからfeatures/paymentをimportすることを防止する。
  • 各featureが独立して動作できるようにするためだ。
TypeScript

5. no-ui-in-business-logic:UIレイヤー間import制限
  • ビジネスロジック(entities)がUI(widgets)をimportできないよう制限する。
TypeScript

6. no-global-store-imports:グローバル状態管理アクセス制限
  • feature、widgets、entitiesなどからグローバル状態(store)を直接importすることを禁止する。
  • グローバル状態はuseStoreuseSelectorのような状態管理フックを通じてアクセスする必要がある。
  • Redux、Zustandなど様々な状態管理ライブラリを考慮した一般的なルールを適用する。
  • features/auth/AuthForm.tsxでstoreを直接importしている。
  • グローバル状態はstoreオブジェクトを直接アクセスする方式ではなく、フックを通じてアクセスする必要がある。

7. ordered-imports:import文のグループ化強制
  • import文をグループ化してコードの可読性を高めるよう強制する。
  • 例えば、import文を同じレイヤーごとにグループ化してコードの可読性を高めることを目標とする。
  • import順序がランダムに混ざっている場合、これを整理できるよう強制する。
  • importの整列がめちゃくちゃになっているのが分かる。
  • 一目で見にくく、保守が難しいという問題が存在する。

ESLintルールの実装

ここまで設計したルールを実装してみよう。


no-relative-importsルールの実装

Bash

no-public-api-sidestepルールの実装

Bash

no-cross-slice-dependencyルールの実装

Bash

no-ui-in-business-logicルールの実装

Bash

no-global-store-importsルールの実装

Bash
🤔注意事項

グローバル状態をどのディレクトリに入れるかはプロジェクトによる。 したがって、プロジェクトごとにこれが考慮される必要があり、FSDで目指すグローバル状態管理要素を基準にしても考慮される必要がある。

ただし、ここでは本当に普遍的な状況を仮定してapp/storeshared/storeを禁止するルールを作った。

store要素がmodelに入る場合もあるので、このルールは今後修正する予定だ。


forbidden-importsルールの実装

レイヤーimport可能対象
appprocesses, pages, widgets, features, entities, shared ✅
processespages, widgets, features, entities, shared ✅
pageswidgets, features, entities, shared ✅
widgetsfeatures, entities, shared ✅
featuresentities, shared ✅
entitiesshared ✅
sharedどのレイヤーもimportしない ✅

このルールは上記の構造を守らせるルールだ。

Bash

ordered-importsルールの実装

Bash

ステップ4:ESLintプラグイン開発およびパッケージング

ESLintプラグインを開発しパッケージングする方法を見てみよう。

目標

  • 個別のルールを一つのESLintプラグインパッケージにまとめる。
  • NPMで配布可能な eslint-plugin-fsd 形式に整理する。
  • テストとデバッグを通じて正常に動作するか確認する。

ESLintプラグイン構造の整理

これまで私たちは rules/ フォルダに個別のルールを実装してきた。

プラグインパッケージとして整理するために、以下のようなディレクトリ構造を作成しよう。

テスト用に含めていた no-console-log.js は削除しよう。

eslint-fsd-plugin/
├── src/
│   ├── rules/
│   │   ├── forbidden-imports.js
│   │   ├── no-relative-imports.js
│   │   ├── no-public-api-sidestep.js
│   │   ├── no-cross-slice-dependency.js
│   │   ├── no-ui-in-business-logic.js
│   │   ├── no-global-store-imports.js
│   │   ├── ordered-imports.js
│   ├── index.js   <-- プラグインエントリポイント
├── tests/         <-- ESLintルールテストコード
├── package.json
├── eslint.config.mjs(テスト用 Flat Config)
├── README.md      <-- ドキュメント

上記のようなディレクトリになることを確認できる。

ESLintプラグインのパッケージ化

これからは、私が作成したESLintルールをパッケージ化し、NPMに配布するための準備過程だ。

1. package.json 設定

ESLintプラグインを一つのパッケージにするために package.json を設定する必要がある。

以下の内容を package.json に追加しよう。

JSON
  • name: eslint-plugin-fsd-lint に設定する。(eslint-plugin-xxx 形式)
  • main: src/index.js をプラグインエントリポイントに指定する。
  • type: module に設定する。
  • scripts: linttest スクリプトを追加する。
    • lint: ESLint 実行。
    • test: テスト実行。
  • keywords: ESLint、plugin、feature-sliced-design、fsd、lint キーワードを追加する。
  • author: 作成者を追加する。(私のGitHubIDを記載した。)
  • license: MIT ライセンスを適用する。
  • dependencies: ESLint が必須なので追加する。
  • peerDependencies: ESLint が必須なので追加する。

2. index.js プラグインエントリポイント作成

JavaScript
  • すべてのルールを rules オブジェクトに登録し、プラグインで使用できるようにする。

プラグインテスト

1. ESLint設定ファイルの修正

テスト用に eslint.config.mjs を修正する。

JavaScript
  • ESLintプラグインをテストできるよう eslint.config.mjsfsdPlugin を登録する。

2. テストファイル追加

tests/ ディレクトリに fsd-rules.test.js ファイルを追加する。

JavaScript
  • ESLint実行後、ルール違反メッセージを出力してテストを進行する。

3. テスト実行

Bash

パッケージNPM配布準備

NPMに配布するには .npmignore ファイルを追加しよう。

tests/
node_modules/
.eslintrc.js
eslint.config.mjs
.idea/
.vscode/
.DS_Store/

ステップ5:プラグインドキュメント化およびNPM配布

実はすでに配布は完了している。

これからは配布された要素を整える作業を行う番だ。

目標

  • README.md ファイルを作成してプラグインの使用方法とルール説明を整理する。
  • GitHubおよびNPMページでユーザーが簡単に設定できるようにガイドを提供する。
  • パッケージ配布完了後、バージョン管理と更新戦略を策定する。

コードのローカライズ

公式的にコードを扱うために、ドキュメント化とローカライズ作業が必要だ。

これまで母国語である韓国語を基にコメントやエラーメッセージなどを作成してきた。

これからは韓国語を取り除き英語に変更し、すべて英語で扱う予定だ。

JavaScript

README.mdを含むドキュメント作成

プラグインの使用方法とルール説明を README.md に作成しよう。

英語だけでなく韓国語もサポートするため、両方作成する方向で進めていきたい。

これに関する要素は画像で添付する。

README.md 作成
README.md 作成

NPM配布

作成したドキュメントを基に再度NPMに配布しよう。

JSON

バージョンを 1.0.0 から 1.0.1 に上げる。

NPMとGithub連携

NPMとGithubを連携すると、Githubにコードをプッシュすると自動的にNPMに配布される機能を使用できる。


package.jsonにGithub リポジトリ情報を追加

GitHubとNPMを連結するには package.json に repository と bugs 情報を追加する必要がある。

JSON

バージョンを 1.0.1 から 1.0.2 に上げる。

  • "repository" → GitHub リポジトリアドレス連結
  • "bugs" → イシュートラッキングURL追加
  • "homepage" → GitHubのREADME.mdリンク追加

package.json 更新後コミット&プッシュ

修正した package.json をGithubに反映する。

Bash

NPM パッケージ更新

更新された情報を含むパッケージを再配布する。

Bash

最後にリリースノートを更新すれば関連作業は完了だ。

CI/CD設定

GitHubで新しいタグをプッシュしてReleaseを作成すると自動的にNPMに配布されるようにしてみよう。

このためにGitHub Actionsを設定してCI/CD(Continuous Integration/Continuous Deployment)を構成する。

GitHub ActionsでNPMに配布するにはNPM Access Tokenが必要だ。

以下の手順に従ってNPMからTokenを発行すればよい。

1️⃣ NPM公式ホームページに移動 2️⃣ 右上のプロフィールをクリック → "Access Tokens"を選択 3️⃣ "Generate New Token"をクリック 4️⃣ "Automation"タイプのTokenを生成(自動化配布に必要) 5️⃣ 発行されたTokenをコピーしておく(後でGitHubに追加する予定)

✅ これでNPM Access Tokenの準備ができた。 (Tokenは露出しないようによく保管する必要がある。)

ステップ6:実際のプロジェクトに適用してみる

作成したLintを実際のプロジェクトに適用してみよう。

幸いにも、航海プラスを進めながらFSDを積極的に活用したプロジェクトがあった。

ここには別のFSD関連Lintが存在しなかったので、ちょうど適した要素だった。

FSDは使っていたが...リントはない...非常に最適な条件だった。

プロジェクトにESLintをインストール

Bash

すでに配布した要素なのでダウンロードして使うだけでよかった。

このプロジェクトはpnpmを使用していたのでこれを基にインストールした。

プロジェクトにESLintインストール
プロジェクトにESLintインストール

正常にインストールされたことを確認できる。

プロジェクトにESLint設定

JavaScript

上記のように設定を追加した。

プロジェクトでESLint実行

Bash

上記のように設定してリントを実行させた。

その結果は次のとおりだ。

リント実行結果
リント実行結果

正常に動作していることを確認できた。

リントfix実行結果
リントfix実行結果

fixも正常に動作していることを確認できた。

まとめ

これまでESLintプラグインを作成してみた。

最初は不便さから始まったことが、思ったより楽しい仕事になった。

スケールもかなり大きくなった。

これを通じてESLintがどのように動作するのか、Flat Configが何なのかをより詳しく明確に理解することができた。

また、初めてNPMパッケージを配布し、Open Sourceを作ってみたが、これも新しい経験だった。

単に配布して終わりではなく、関連して設定することもあったし、実際に配布が行われるとバグに敏感にならざるを得なかった。

現在は規模が小さいが、規模が大きくなればテストももっと厳格にする必要がありそうだし、リントルールももっと厳格にする必要がありそうだ。

旧正月連休中に既存の航海に関する内容復習を兼ね、インターンとして実務で必要になりそうだと思って作ったが、よかったと思う。

ブーストキャンプが終わって最近開発スキルがかなり伸びたようだ。正確にはチャレンジ精神というか?

不便さを避けずに解決しようとする努力が増えたようだ。

しばらくこの状態を維持しながらもっと多くのことを学んでいかなければならない。

今後の計画

  • Steigerでサポートしている内容を少しずつESLintにポーティングしてみようと思う。
  • また、Prettierも設定をカスタマイズできるのでこれについても作ろうかと思うが...他にやることも多いのでもう少し考えてから試そうと思う。