モジュラモノリスの依存関係をESLintの設定で強制する
概要
モジュラモノリスの依存関係をESLintの設定で強制する方法を示します。
今回の構成
.
├── README.md
├── bun.lockb
├── eslint.config.js
├── node_modules
├── package-lock.json
├── package.json
├── src
│ ├── index.ts
│ ├── module1
│ │ ├── dir
│ │ │ └── var.ts
│ │ ├── index.ts
│ │ └── var.ts
│ ├── module2
│ │ ├── index.ts
│ │ └── var.ts
│ └── common-module
│ ├── index.ts
│ └── var.ts
└── tsconfig.json
サンプル実装としてHonoでプロジェクトを作成しました。他のTypeScriptのプロジェクトでもこの記事の考え方を当てはめることはできると思います。
Honoのプロジェクトの中にモジュールとするディレクトリを複数作成し、それぞれのモジュールは独立しています。
上記の例では、モジュールとしてmodule1, module2, common-moduleを作成しています。
依存関係のルールは以下のように定めました。
- 各モジュール内でexportしたメンバは原則として同一モジュール内でのみimportして良い
- 1の例外として、他モジュールへexportしたいメンバはモジュールルートのindex.ts(Barrelファイル)でexportして良い
- common-moduleは他モジュールのメンバをimportしてはいけない
common-moduleには各モジュールで共通して用いられるメンバ(ユーティリティ関数や型定義など)を定義します。
実際のプロジェクトではトランザクションの管理やDBの各テーブルへのアクセスのルール設計などが必要ですが、この記事ではあくまでimportに関する部分のみ言及するので、それ以外は割愛します。
ESLintの設定
今回はESLintのFlat Configで設定します。
省機能であればBiomeが手っ取り早いですが、importのルールを設定するなどはBiomeではできないようでした。
まだFlat Configに対応していないプラグインも多い上にAIに聞いてもeslintrcの知識を持ってフィードバックしてくるので少々やりづらいですが、ESLint v10からはFlat Configしか対応しないようなので、こちらを選びました。
設定ファイルは以下のとおりです。
import prettierRecommended from "eslint-config-prettier";
import prettierPlugin from "eslint-plugin-prettier";
import typescriptEslintPlugin from "@typescript-eslint/eslint-plugin";
import typescriptParser from "@typescript-eslint/parser";
export default [
prettierRecommended,
{
files: ["**/*.ts"],
languageOptions: {
parser: typescriptParser,
ecmaVersion: 2021,
sourceType: "module",
globals: {
window: "readonly",
document: "readonly",
console: "readonly",
process: "readonly",
global: "readonly",
},
},
plugins: {
"@typescript-eslint": typescriptEslintPlugin,
prettier: prettierPlugin,
},
rules: {
...typescriptEslintPlugin.configs.recommended.rules,
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"prettier/prettier": "error",
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["src/module1/*", "./module1/*"],
message: "src/module1の中のメンバは直接インポートできません。(src/module1/index.tsを除く)",
},
{
group: ["src/module2/*", "./module2/*"],
message: "src/module2の中のメンバは直接インポートできません。(src/module2/index.tsを除く)",
},
],
},
],
},
},
{
files: ["src/common-module/**/*.ts"], // src/common-module 配下に限定
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["../module1", "../module2"],
message: "src/common-module配下では他のモジュールのメンバをインポートできません。",
},
],
},
],
},
},
{
files: ["*.ts"],
rules: {
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
];
いくつかのimportのルールを制御するプラグインが存在しますが、できるだけ依存パッケージを増やしたくないので、no-restricted-importsで対応しました。
検証
実際にコマンドを実行して検証してみる。
// import不可
// eslintでエラーになる
import { piyo } from "./module1/dir/var";
// import不可
// eslintでエラーになる(common-moduleは他のモジュールのメンバをimportできない)
import { fuga as fugaFromModule1 } from "../module1";
$ eslint .
/src/common-module/var.ts
3:1 error '../module1' import is restricted from being used by a pattern. src/common-module配下では他のモジュールのメンバをインポートできません。 no-restricted-imports
/src/index.ts
12:1 error './module1/dir/var' import is restricted from being used by a pattern. src/module1の中のメンバは直接インポートできません。(src/module1/index.tsを除く) no-restricted-imports
✖ 2 problems (2 errors, 0 warnings)
ちゃんとエラーを吐いてくれたので、これをpre-commitなどで設定すればチームとして依存関係のルールを守れる。
GitHubリポジトリ
サンプル実装のGitHubリポジトリはこちらです。
isao0214/eslint-import-rules-sample
細かな点は実際のソースコードを御覧ください。
Discussion