📙

pnpm 9.5 でリリースされた Catalogs 機能を使ってモノレポ内の依存パッケージのバージョンを揃える

2024/07/30に公開

Catalogs とは何か

依存関係のバージョン範囲を再利用可能な定数として定義でき、それを package.json から参照できる機能です。

https://pnpm.io/catalogs

ドキュメントの例をそのまま引用すると、以下のように pnpm-workspace.yamlcatalog: もしくは catalogs: で依存ごとのバージョン範囲を定義できます。

pnpm-workspace.yaml
packages:
  - 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/clientwebapp-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 した ApolloErrorerror instanceof ApolloError をしても false となる
  • パッケージ側でモジュール拡張(module augumentation)により型を拡張している場合に、それが利用側で反映されなくなる
    • たとえば共通パッケージ側で @apollo/clientDefaultContext を declaration merging で拡張しているときに、共通パッケージ側と webapp 側で @apollo/client のパッケージ重複が起きると、declaration merging の結果が webapp 側で反映されなくなる
  • etc.

このパッケージ重複を避けるためには各 webapp と webapp-foundation の間で @apollo/client のバージョンを丁寧に揃えてあげるのが手っ取り早いですが、それも手作業でやってしまうとアップグレード時の更新漏れなどで気づかぬ間に再発する可能性もあります。

…と、めちゃめちゃ困っていたのですが、 pnpm 9.5 で Catalogs 機能がリリースされて光明が差したのでした。


ところで、じゃあぜんぶ Catalogs でいいか!というとそうでもなく、悩ましい点は存在します。

  • 全 webapp が同一バージョンのパッケージ利用を強制されるのは楽な反面、アップグレード時に全体をせーので上げる必要があり、そのへんの運用が若干悩ましい
  • Dependabot や Renovate は問題ないか
    • 未確認だが、現状未対応でもそのうち追従してくれるだろうと楽観している

とはいえそれ以前の困りが大きいので、とりあえず Catalogs を使い始めてあとから反省していければというところです。

LayerX

Discussion