👋

pnpmのモノレポで依存を制限する[クリーンアーキテクチャ]

2022/12/09に公開

この記事は

Chatwork Advent Calendar 2022

9日目の記事です。

https://qiita.com/advent-calendar/2022/chatwork

はじめに

レイヤードアーキテクチャが好きです。

ヘキサゴナルアーキテクチャが好きです。

コードをルールで縛ることが好きです。

ですが、人間がコードを書く以上、ルールは破られるものです。

  • 依存してはいけない層で、依存してはいけないライブラリに依存してしまう
  • 依存してはいけない層で、依存してはいけない層に依存してしまう

コードに関わる人が多ければ多いほど、その問題は多く発生します。

この記事では、設計ルールを踏み外さないために、依存を制限をする方法を紹介します。

コード

https://github.com/agoetc/dependency-limit-by-pnpm-monorepo

環境

ツールの概要 ツールの名前 バージョン
パッケージマネージャ pnpm 7.15.0
ビルドツール vite 3.2.3
UIライブラリ react 18.2.0

ビルドツールとUIライブラリは今回はほぼ関係なく、pnpmの機能で実現しています。

アーキテクチャ

以下のような構成にしています。

ディレクトリ 説明
clients/** domainとusecaseに依存することができます。
usecase domainとadapterに依存することができます。
adapter/** domainに依存することができます。 adapter同士が依存することはできません。
domain どこにも依存せず、純粋なビジネスロジックと代数的データ型を持ちます。

また、依存性逆転の原則に関しては今回採用していません。

モノレポとは

モノリスリポジトリの略。

複数のサービスを1つのリポジトリ(git)で管理する手法を表します。

マイクロサービス化したものを、1つのリポジトリで管理し、運用や保守コストなどを減らすための技術だと認識しています。

pnpmは複数のJavaScriptリポジトリをモノレポとして管理する機能があるため、それを活用します。

おそらく、本来pnpmが想定している利用ケースとは異なるため注意してください。

モノレポの定義

pnpmのモノレポを利用するには、pnpm-workspace.yamlをルートに作成する必要があります。

pnpm-workspace.yaml
packages:
  - 'packages/**'
  - 'clients/**'

ディレクトリ/**と記述することで、ディレクトリ以下の全てを、モノレポ管理の対象とすることができます。

今回はpackages以下と、clients以下をモノレポの対象としたかったため、このような設定としました。

リポジトリの定義

usecaseを例に解説します。

私が定義した/packaege/usecaseは、モノレポにおける、1つのリポジトリです。

1つのリポジトリに対し、1つのpackage.jsonを必要をします。

以下は、usecaseで利用しているpackage.jsonです

package.json
{
  "private": true,
  "name": "usecase",
  "dependencies": {
    "domain": "workspace:^",
    "adapter.great-api": "workspace:^",
    "adapter.nice-api": "workspace:^"
  }
}

nameにモノレポのパッケージ名を書き、

dependenciesに依存するパッケージ、リポジトリを記述します。

usecaseはdomainとadapterに依存することができるので、
以下の3つに依存する構成にしています。

  • domain
  • adapter.great-api
  • adapter.nice-api

この実装をすることによって、依存を可能にします。

裏を返せば、dependenciesに追記しなければ、依存は不可能となります。

以下のようにnice-apiの依存を消した場合、usecaseからは依存できなくなるため、importができない旨のエラーが出力されます。

package.json
{
  "private": true,
  "name": "usecase",
  "dependencies": {
    "domain": "workspace:^",
    "adapter.great-api": "workspace:^",
-    "adapter.nice-api": "workspace:^"
  }
}

domainやadapterなども同じように設定をしています。

https://github.com/agoetc/dependency-limit-by-pnpm-monorepo/tree/master/packages

参考: https://pnpm.io/ja/workspaces

TypeScriptについて

各リポジトリにpackage.jsonを用意するということは、各package.jsonでTypeScriptに依存する必要があります。

各リポジトリでTypeScriptに依存したり、バージョンを揃えるのは大変なため、pnpmのマクロ機能によって問題を解決します。

ルートにpnpmfile.cjsを用意します。

.pnpmfile.cjs
function readPackage(pkg) {
  pkg.devDependencies = {
    ...pkg.devDependencies,
    typescript: '^4.7.4',
  }

  return pkg
}

module.exports = {
  hooks: {
    readPackage,
  },
}

readPackageは、pnpm iを叩いた後、package.jsonを解析したその後に呼び出される関数です。

流れとしては以下です

pnpm iが叩かれる
-> package.jsonが読み込まれる
-> pnpmfile.cjsのreadPackageが実行される

この関数を用いることで、package.jsonを改変することができます。

pkg.devDependencies = {
    ...pkg.devDependencies,
    typescript: '^4.7.4',
  }

上記の処理によって、package.jsondevDependenciesに、追記することができます

そして、この処理は、モノレポで管理している全てのpackage.jsonで行われるため、全てのリポジトリにTypeScriptの同バージョンを挿入することが可能になります。

参考: https://pnpm.io/ja/pnpmfile

tsconfig.jsonについて

package.jsonでTypeScriptに依存しているため、

各リポジトリでtsconfig.jsonを用意する必要があります。

ですが、多くの場合tsconfig.jsonを使い分けたいことはないはずです。

ルートに、以下のような元となるjsonを用意し、

usecaseや、その他のtsconfig.jsonで継承するようにしています。

/tsconfig-base.json
{
  "compilerOptions": {
    "strict": true,
    "target": "ESNext",
    "module": "ESNext",
    "useDefineForClassFields": true,
    "allowJs": false,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true
  }
}
usecase/tsconfig.json
{
  "extends": "../../tsconfig-base.json"
}

クライアントでは、reactなどで別途tsの設定をしたいため、必要なオプションのみ追記しています。

home/tsconfig.json
{
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "jsx": "react-jsx"
  },
  "include": ["../../tsconfig-base.json"]
}

まとめ

pnpmのモノレポを利用して、依存の方向性を制限することができました。

筆者の感想としては、この構成で開発しても問題がなさそうなレベルだと思いましたので、

TypeScriptで個人開発する際は、この構成を利用しようと思います。

再三になりますが、本来pnpmが想定している利用ケースとは異なるので、みなさんもほどほどに活用いただければと思います。

ほなね〜👋

Discussion