pnpm 9.5 でリリースされた Catalogs 機能を使ってモノレポ内の依存パッケージのバージョンを揃える
Catalogs とは何か
依存関係のバージョン範囲を再利用可能な定数として定義でき、それを package.json
から参照できる機能です。
ドキュメントの例をそのまま引用すると、以下のように pnpm-workspace.yaml
に catalog:
もしくは catalogs:
で依存ごとのバージョン範囲を定義できます。
pnpm-workspace.yamlpackages: - packages/* # Define a catalog of version ranges. catalog: react: ^18.3.1
その定義されたバージョン範囲を workspace 内の package.json
で参照できる、というものです。
packages/example-app/package.json{ "name": "@example/app", "dependencies": { "react": "catalog:", "redux": "catalog:" } }
Catalogs を使うモチベーション
Catalogs のメリットについては、ドキュメントではざっくり以下の3点が挙げられています。
- 単一バージョンを維持できることで、パッケージの重複によるバグの発生を防ぐ
-
pnpm-workspace.yml
を編集するだけで依存関係のアップグレードが完了する - 依存関係アップグレード時に
package.json
を編集しなくてよくなり、git でのコンフリクトを避けられる
筆者の環境では特に1つ目の、「パッケージの重複によるバグを防ぐ」というのが大きなモチベーションとなりました。
モノレポと共通パッケージとパッケージ重複
筆者の環境では一部の Web Frontend もモノレポで運用しており、そのなかで一部の共通実装はモノレポ内パッケージとして切り出されています。以下は架空の例ですが、おおまかにこのような構造となっています。
.
├── apps
│ ├── foo-webapp
│ ├── bar-webapp
│ └── ...
├── packages
│ ├── webapp-foundation
│ └── ...
├── package.json
├── pnpm-lock.yaml
└── pnpm-workspace.yaml
たとえば各 webapp が @apollo/client
を使っており、その共通ユーティリティや初期化処理などを仮に webapp-foundation
というパッケージに切り出している という状態であるとします。
このときの依存関係は以下のようになることが多いでしょう。
- 各 webapp は
@apollo/client
とwebapp-foundation
を dependencies に持つ -
webapp-foundation
は@apollo/client
を dev 及び peer の dependencies に持つ- ランタイムでは依存関係の制約だけつけ、実体には依存させない
- テストでは実体が必要なので devDependencies として実体を見れるようにする
しかし、これでは問題が起きることがあります。
各 webapp が利用する @apollo/client
のバージョンと、webapp-foundation
の devDependencies の @apollo/client
のバージョンがずれた場合に、開発環境では2つの @apollo/client
が存在することになります(パッケージの重複)。これは node_modules
の上では通常の dependencies と devDependencies を区別していないことに起因します。
パッケージ重複が起きると単純にバンドルサイズが膨らむのも問題ですが、それ以上にバグの原因になりうるのが危険です。
- 内部に状態を持つパッケージで問題が起きる
- クラスも別で定義される
- たとえば共通パッケージ側で
ApolloClient
の初期化をしていると、useQuery
などが返すerror
は共通パッケージ側のApolloError
になる。このとき@apollo/client
のパッケージ重複が起きると、webapp 側で import したApolloError
でerror instanceof ApolloError
をしてもfalse
となる
- たとえば共通パッケージ側で
- パッケージ側でモジュール拡張(module augumentation)により型を拡張している場合に、それが利用側で反映されなくなる
- たとえば共通パッケージ側で
@apollo/client
のDefaultContext
を declaration merging で拡張しているときに、共通パッケージ側と webapp 側で@apollo/client
のパッケージ重複が起きると、declaration merging の結果が webapp 側で反映されなくなる
- たとえば共通パッケージ側で
- etc.
このパッケージ重複を避けるためには各 webapp と webapp-foundation
の間で @apollo/client
のバージョンを丁寧に揃えてあげるのが手っ取り早いですが、それも手作業でやってしまうとアップグレード時の更新漏れなどで気づかぬ間に再発する可能性もあります。
…と、めちゃめちゃ困っていたのですが、 pnpm 9.5 で Catalogs 機能がリリースされて光明が差したのでした。
ところで、じゃあぜんぶ Catalogs でいいか!というとそうでもなく、悩ましい点は存在します。
- 全 webapp が同一バージョンのパッケージ利用を強制されるのは楽な反面、アップグレード時に全体をせーので上げる必要があり、そのへんの運用が若干悩ましい
- Dependabot や Renovate は問題ないか
- 未確認だが、現状未対応でもそのうち追従してくれるだろうと楽観している
とはいえそれ以前の困りが大きいので、とりあえず Catalogs を使い始めてあとから反省していければというところです。
Discussion