ファインディ株式会社 でフロントエンドのリードをしている新福( @puku0x )です。 弊社のフロントエンドの多くは、プロダクト単位のモノレポで管理されています。 Nx を用いたモノレポでは、アプリケーションや関連モジュールを「プロジェクト」として管理しており、モノレポ内にある多数のプロジェクトを組み合わせてアプリケーションを作るため、各プロジェクトの依存関係の制御が非常に重要となります。 この記事では、Nxが標準搭載しているESLintルール @nx/enforce-module-boundaries を活用し、モノレポにおけるプロジェクトの依存関係の制御についてご紹介します。 Nxについては以前の記事で紹介しておりますので、気になる方は是非ご覧ください。 tech.findy.co.jp モノレポにおける依存関係の課題 @nx/enforce-module-boundaries Findyでの活用事例 まとめ モノレポにおける依存関係の課題 モノレポで開発していくと、モノレポ内のプロジェクトの依存関係が複雑になっていきます。 「ビルドが通らないと思ったら循環参照していた」 、 「関心の異なるモジュールがimportされていた」 など、思い当たるものがあるのではないでしょうか? 複雑な関係を持つプロジェクトの皆さん モノレポを用いた開発では、プロジェクト間の依存関係を適切に制御する必要があります。 つまり、 循環参照の防止 依存許可/拒否のルール定義 をいかに開発プロセスへ落とし込むかが課題となります。 特に、Nxを用いたモノレポでは プロジェクトの粒度がキャッシュヒット率の高さ(=CI速度)に直結する ため、プロジェクトの細分化を積極的に行う傾向があります。 プロジェクト数の増加・複雑化を避けて通れない以上、プロジェクト間の依存関係の制御は必須となるでしょう。 @nx/enforce-module-boundaries Nxには、モノレポ内のプロジェクトの依存関係を制御する仕組みとして、 @nx/enforce-module-boundaries というESLintルールがあります。 nx.dev @nx/enforce-module-boundaries は標準で循環参照を防止します。さらに、各プロジェクトに設定したタグを元に依存関係の制御も可能です。 例として、次のような依存関係を考えます。 libs/componentsからlibs/uiへの単方向依存 それぞれのプロジェクトにタグを設定します。 // libs/components/project.json { "name" : "components" , "tags" : [ "type:ui" ] , ... } // libs/utils/project.json { "name" : "utils" , "tags" : [ "type:util" ] , ... } 次に、ESLintの設定ファイルに @nx/enforce-module-boundaries の設定を追加します。 { files : [ '**/*.ts' , '**/*.tsx' , '**/*.js' , '**/*.jsx' ] , rules : { '@nx/enforce-module-boundaries' : [ 'error' , { enforceBuildableLibDependency : true , allow : [ '^.*/eslint(\\.base)?\\.config\\.[cm]?js$' ] , depConstraints : [ { sourceTag : 'type:ui' , onlyDependOnLibsWithTags : [ 'type:ui' , 'type:util' ] , } , { sourceTag : 'type:util' , onlyDependOnLibsWithTags : [ 'type:util' ] , // type:ui は許可しない } , ] , } , ] , } , } , これにより「 libs/components (type:ui) → libs/utils (type:util) の依存は許可され、逆方向の依存は許可されない」という制約を設定できました。 タグの仕様などについては、公式のドキュメントで解説されてありますので是非ご一読ください。 nx.dev Findyでの活用事例 ここでは、弊社における @nx/enforce-module-boundaries の活用事例を紹介します。 Findy のフロントエンドでは、公式ドキュメントの例に倣い、 scope:* や type:* といったタグを設定してモノレポ内のプロジェクトを管理しています。 共通モジュールを持つプロジェクトを作成する際は、次のように分類することが多いです。 モノレポ全体 の共通モジュールを持つプロジェクト( scope:shared ) アプリケーション内 の共通モジュールを持つプロジェクト( scope:app1 scope:app2 など ) モノレポ全体( scope:shared )とアプリケーション( scope:app1 scope:app2 )の分類 各アプリケーション( type:application )は複数のフィーチャー( type:feature )を持ち、上位レイヤーからのみ依存可能です。また、各フィーチャーはCRUD単位で細分化される場合があります。 アプリケーション( type:application )は複数のフィーチャー( type:feature )を持つ アプリケーション内の共通モジュールを持つプロジェクトには scope:* タグの他にも、用途に応じて type:ui や type:util 、分類が難しいものは type:shared といったタグを設定しています。 フィーチャーとその他の共通モジュールの依存関係 ※ type:ui のプロジェクトは省略されてあります ここまではよろしいでしょうか? ✋では、ここからが本題です。 type:feature の解説で「各フィーチャーはCRUD単位で細分化される場合がある」と述べましたが、 フィーチャー内の共通モジュールを持つプロジェクト の依存関係はどのように定義できるでしょうか? 新たな scope:* タグを作る案もありましたが、設定が複雑になってしまうと予想されたため、Findyでは type:feature-shared タグを作ることにしました。 フィーチャー内共通モジュール( type:feature-shared )の新設 ↑ type:feature と違い type:application からの依存を許可していないのがポイントです。 それでは、これらの依存関係をESLintルールとして記述しましょう。 { files : [ '**/*.ts' , '**/*.tsx' , '**/*.js' , '**/*.jsx' ] , rules : { '@nx/enforce-module-boundaries' : [ 'error' , { enforceBuildableLibDependency : true , allow : [ '^.*/eslint(\\.base)?\\.config\\.[cm]?js$' ] , depConstraints : [ { sourceTag : 'scope:app1' , onlyDependOnLibsWithTags : [ 'scope:app1' , 'scope:shared' ] , } , { sourceTag : 'scope:app2' , onlyDependOnLibsWithTags : [ 'scope:app2' , 'scope:shared' ] , } , { sourceTag : 'scope:shared' , onlyDependOnLibsWithTags : [ 'scope:shared' ] , } , { sourceTag : 'type:application' , onlyDependOnLibsWithTags : [ 'type:feature' , 'type:shared' , 'type:ui' , 'type:util' , ] , } , { sourceTag : 'type:feature' , onlyDependOnLibsWithTags : [ // 'type:feature', // type:feature 同士の依存は許可しない 'type:feature-shared' , // type:feature からのみ依存可能 'type:shared' , 'type:ui' , 'type:util' , ] , } , { sourceTag : 'type:feature-shared' , onlyDependOnLibsWithTags : [ // 'type:feature-shared', // type:feature-shared の乱用を防ぐため許可しない 'type:shared' , 'type:ui' , 'type:util' , ] , } , { sourceTag : 'type:shared' , onlyDependOnLibsWithTags : [ // 'type:shared', // type:shared の乱用を防ぐため許可しない 'type:ui' , 'type:util' , ] , } , { sourceTag : 'type:ui' , onlyDependOnLibsWithTags : [ 'type:ui' , 'type:util' ] , } , { sourceTag : 'type:util' , onlyDependOnLibsWithTags : [ 'type:util' ] , } , ] , } , ] , } , } , こうすることで、 モノレポ全体 の共通モジュールを持つプロジェクト アプリケーション内 の共通モジュールを持つプロジェクト フィーチャー内 の共通モジュールを持つプロジェクト という様々な特性を持つプロジェクトの依存関係を制御できようになりました。 まとめ この記事では @nx/enforce-module-boundaries を用いて、モノレポ内の依存関係を制御するテクニックを紹介しました。 今回は利用しませんでしたが、Nxの公式ブログでは bannedExternalImports を追加で設定し、複数のフレームワークが混在する場合の対応についても触れられています。 nx.dev プロジェクトのタグを毎回設定するのは煩雑ですので、Nxのジェネレータを使って設定済みのフィーチャーを生成すると良いでしょう。ジェネレータについては過去の記事で解説されてありますので、ご興味がありましたらこちらもご参照ください。 tech.findy.co.jp Nxにはモノレポ管理のための便利な機能が多く存在します。 この記事が皆様の参考になれば幸いです。それではまた次回! ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。 herp.careers