setup-ts-deep-modules について
TypeScript リポジトリで dependency-cruiser を設定し、ディープモジュールの境界を適用するコーディングスキルです。エージェントによる設定、ルールの検証、パッケージの公開入口と内部実装の規約化を支援します。
主な機能
- 01dependency-cruiser の導入と設定
- 02パッケージの入口境界の適用
- 03パッケージ内の相互インポート許可
- 04依存関係の循環を禁止
- 05意図的な違反によるルール検証
- 06パッケージ規約の文書化
活用シーン
- 01TypeScript モノレポの境界設定
- 02テストによる内部実装依存の防止
- 03インポート制限の動作確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mattpocock/ skills --skill setup-ts-deep-modulesインストール後、Claude Code で/setup-ts-deep-modulesを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yamldependency-cruiser.config.cjs
詳細
TS ディープモジュールのセットアップ
このリポジトリの各パッケージをディープモジュールにします。つまり、小さなインターフェースの背後に多くの振る舞いを隠します。パッケージの公開インターフェースはエントリーポイント(パッケージのルートにあるファイル)であり、サブフォルダー内のすべてのものは非公開です。このスキルでは、dependency-cruiserと、エントリーポイントだけを入口にするルールを導入し、そのルールが実際に機能することを確認します。
用語(ディープモジュール、インターフェース、継ぎ目、深さ)については、Skill ツールを「codebase-design」で呼び出し、その用語を一貫して使用してください。
強制する構成
src/packages/
<name>/
index.ts ← an entry point (public). Import this from outside.
client.ts ← another entry point. Packages may expose SEVERAL.
lib/ ← implementation: hidden from outside, free to import each other.
tests/ ← co-located tests + fixtures (a subfolder, so private).
公開インターフェースは、指定されたindex.tsではなく、パッケージのルートファイルです。慣例として、実装はlib/に、テストはtests/に置き、すべてのパッケージが同じ2フォルダー構成になるようにします。ただし、ルール自体は一般的なものです。どのサブフォルダーにあるものでもすべて非公開なので、フォルダーを追加するために設定を拡張することはありません。
すべてerrorとする4つのルールです。
- エントリーポイントの境界:パッケージの外部(アプリのコードまたは別のパッケージ)からは、そのパッケージのエントリーポイント(ルートファイル)のみインポートできます。サブフォルダー内のものはインポートできません。
- パッケージ内の自由なインポート:パッケージ内のファイル同士は自由にインポートできます。
- エントリーポイント経由のテスト:
<pkg>/tests/以下のファイルは、任意のパッケージのエントリーポイントと、同じパッケージのtests/フィクスチャをインポートできます。ただし、どのパッケージのサブフォルダー内の実装もインポートできません(自分のパッケージのものも含みます)。パッケージをまたぐ統合テストは問題ありませんが、ディープインポートはできません。 - 循環依存なし:依存関係に循環があってはなりません。
バレルではなく、エントリーポイントです。 公開インターフェースがすべてのルートファイルであるため、パッケージはすべてを1つの巨大なindex.tsに集約する代わりに、複数の小さなエントリーポイント(index.ts、client.ts、server.tsなど)を公開できます。サブツリー全体を再エクスポートするバレルファイルは推奨しません。エントリーポイントを小さく保ち、実装はサブフォルダーに隠してください。
レイヤリング(どのパッケージがどのパッケージに依存できるか)は別の課題であり、このリポジトリ向けに記入するコメント付きの仮設定として残しています。
手順
1. 環境を検出する
- パッケージマネージャー:
pnpm-lock.yamlがあればpnpm、yarn.lockがあればyarn、bun.lockbがあればbun、それ以外はnpmです。以下のすべてのコマンドで、検出したものを使用してください(pnpm/yarn/npm run/bunx)。 - パッケージのルート:
src/があればsrc/packagesを使用し、なければpackagesを使用します。リポジトリにすでに別の明確な慣例がある場合は、ユーザーに選択を確認してください。 - 既存の設定:
.dependency-cruiser.*ファイルがあるか確認します。存在する場合は、上書きしないでください。4つのルールとオプションを追加し、何を追加したかユーザーに伝えてください。
完了条件: パッケージマネージャー、パッケージのルート、既存設定の有無がすべて確認できていること。
2. dependency-cruiserをインストールする
検出したパッケージマネージャーを使って、dependency-cruiserをdevDependencyとしてインストールします。
完了条件: dependency-cruiserがdevDependenciesにあること。
3. 設定を書く
dependency-cruiser.config.cjsをリポジトリのルートに.dependency-cruiser.cjsとしてコピーします。手順1で検出したルートに合わせてPACKAGES_ROOTを設定してください。ルールはパスの深さに基づき、拡張子に依存しないため、それ以外の変更は不要です。
完了条件: 正しいPACKAGES_ROOTが設定された.dependency-cruiser.cjsが存在し、4つの禁止ルールが含まれていること。
4. チェックに組み込む
lint:boundariesスクリプトを追加します:depcruise <packages-root>(またはdepcruise src)。- リポジトリの包括的なチェックコマンド(すでにtypecheckを実行している
check/ci/validateなどのスクリプト)に組み込みます。tsconfigに手を加えたり、パスエイリアスを追加したりしないでください。 - 包括的なスクリプトがない場合は、
lint:boundariesを追加し、CIに組み込むようユーザーに伝えてください。
完了条件: lint:boundariesが存在し、typecheckと同じコマンドの一部として実行されること。
5. サンプルパッケージのひな形を作成する
コピー用テンプレートとして、<packages-root>/example/ を作成し、コミットします。
index.tsはエントリーポイントです。内部ファイルに処理を委譲する関数を1つエクスポートします(パッケージが単なる処理の受け渡しではなく、明確に深い構造になっていることがわかるようにします)。lib/impl.tsはサブフォルダー内に置く内部ファイルで、index.tsからインポートします。外部からはアクセスできないようにします。tests/example.test.tsでは、エントリーポイントである../indexのみをインポートし、公開関数を検証します。
これはコピーまたは削除できるスターターテンプレートであることをユーザーに伝えます。
完了条件: サンプルパッケージが存在し、ルートのエントリーポイントを通じて動作を公開し、impl がサブフォルダー内に隠されています。
6. ルールが実際に機能することを確認する
これはスキル全体の完了条件です。違反があっても失敗しない設定には価値がありません。
lint:boundariesを実行します。クリーンなサンプルでは 成功 しなければなりません。- 一時的に
tests/example.test.tsにディープインポートを追加します(例:import { thing } from "../lib/impl")。もう一度lint:boundariesを実行します。tests-through-entrypointsにより 失敗 しなければなりません。 - ディープインポートを元に戻します。もう一度実行し、成功 しなければなりません。
完了条件: 成功、ディープインポートによる失敗、再度の成功を確認していること。手順2で失敗しない場合、ルールが正しく設定されていないため、完了前に修正します。
7. 規約を文書化する
パッケージフォルダー内(管理対象のパッケージと同じ階層)に README.md(<packages-root>/README.md)を作成し、以下を説明します。src/packages/<name>/ の構成(ルートにエントリーポイント、実装に lib/、テストに tests/)、「パッケージのエントリーポイント(ルートにあるファイル)を通じてのみインポートする」こと、lint:boundaries の実行方法です。バレルファイルは明確に避けるよう促します。1つのインデックスを通じてサブツリー全体を再エクスポートするのではなく、小さなエントリーポイントを複数公開します。コピー用のスニペットと、4つのルールをそれぞれ1段落にまとめた内容にします。
次に、リポジトリのエージェント向け指示ファイル(CLAUDE.md があればそれを、なければ AGENTS.md。どちらもなければ AGENTS.md を作成)から、この README への参照先を追加します。例のように1行で十分です。Packages are deep modules: see [src/packages/README.md](./src/packages/README.md) before adding or importing one. これにより、エージェントは境界ルールに抵触する前にその存在を把握できます。
完了条件: <packages-root>/README.md が存在し、バレルファイルを避けるよう促していること。また、リポジトリの CLAUDE.md/AGENTS.md からリンクされていること。
注記
- dependency-cruiser のグループマッチングで使われる設定の
$1後方参照により、各パッケージは自身の内部にアクセスでき、外部のパッケージはアクセスできません。これをパッケージごとの個別ルールに分割しないでください。 - 公開/非公開は階層で決まります。パッケージのルートにあるファイルがエントリーポイントで、サブフォルダー内にあるものはすべて非公開です。慣例的なサブフォルダーは実装用の
lib/とテスト用のtests/ですが、ルールでこれらを固定しているわけではありません。どのサブフォルダーも非公開なので、新しいフォルダーを追加しても設定変更は不要です。エントリーポイントの追加はルートにファイルを追加するだけです(バレルは不要です)。 - パッケージはフラットです。ルート直下に子パッケージが1階層だけあります。パッケージ内部はいくらでも深くネストできますが、パッケージ内に別のパッケージを含めることはできません。
- 設定の
module.exportsが"type": "module"のリポジトリでも機能するように、.jsではなく.cjsを使用します。