syncpack: monorepo の package.json の依存の版をそろえる

  • syncpack
  • pnpm
  • monorepo

monorepo では、packages/web が react 19.1.0 を、packages/api が react 18.3.1 を持つような不一致が、誰も気づかないまま残りやすい。syncpack は、ワークスペース内のすべての package.json を読み、同じ依存の版が食い違っている箇所を報告して修正する CLI である1。npm パッケージとしては OS ごとのネイティブバイナリが optionalDependencies で配布されていて、現在の版は 15.3.3 である2。

コマンド内容
lint版と範囲の不一致を報告し、終了コード 0 か 1 を返す
fixlint が報告した問題のうち自動修正できるものを書き換える
updatenpm レジストリの最新版に更新する
formatpackage.json のフィールドの順序をそろえる
list依存を有効なものと無効なものに分けて一覧する
json依存のインスタンスごとに 1 行の JSON を出力する

不一致の検出と修正

2 つのパッケージが react、zod、typescript を違う版で持っている状態で lint を実行する。

$ syncpack lint
= Default Version Group ========================================================
   2x react
      ✘ 18.3.1 → 19.1.0 in packages/api/package.json at .dependencies (DiffersToHighestOrLowestSemver)
   3x typescript
      ✘ ^5.8.0 → 5.9.3 in packages/web/package.json at .devDependencies (DiffersToHighestOrLowestSemver)
   2x zod
      ✘ ^3.24.0 → ^3.25.0 in packages/api/package.json at .dependencies (DiffersToHighestOrLowestSemver)
✗ Issues found

既定の方針では、見つかった中で最も高い semver にそろえる。末尾の DiffersToHighestOrLowestSemver がその方針に対する違反の status code である。fix を実行すると矢印の向きが逆になり、各 package.json が書き換わる。

$ syncpack fix
   2x react
      ✓ 19.1.0 ← 18.3.1 in packages/api/package.json at .dependencies

Version Groups で方針を分ける

依存を Version Group に分けると、グループごとに別の方針を持たせられる。設定は .syncpackrc.json か syncpack.config.ts に書き、TypeScript の場合は import("syncpack").RcFile の型が使える3。

方針動作
Highest / Lowest Semver見つかった中で最も高い、または低い版にそろえる
PinnedpinVersion に書いた版に固定する
Snapped To指定したパッケージが持つ版に他を合わせる
Same Range版はそろえず、semver の範囲が重なっていれば許容する
Bannedその依存の使用を禁止する
Catalogpnpm か Bun の catalog からの参照を強制する
Ignored検査から除外する

出典は Version Groups の一覧である4。グループは上から順に照合され、依存は最初に一致したグループだけに属する。

Banned

HTTP クライアントを fetch へ統一するために、axios を禁止する。label に書いた文がそのまま出力の見出しになる。

{
  "versionGroups": [
    {
      "label": "HTTP クライアントは fetch を使う",
      "dependencies": ["axios", "ky", "got"],
      "isBanned": true
    }
  ]
}
$ syncpack lint
= HTTP クライアントは fetch を使う ===================================
   1x axios
      ✘ 1.7.0 →  in packages/web/package.json at .dependencies (IsBanned)

IsBanned は自動修正の対象なので、fix を実行すると axios の行が package.json から削除される5。禁止の理由を label に書いておけば、CI の失敗ログがそのまま説明になる。

Catalog

pnpm の catalog は、版を pnpm-workspace.yaml の 1 箇所に書き、各 package.json からは catalog: で参照する仕組みである。既存のリポジトリを catalog に移すには、policy: "catalog" のグループを作る。

{
  "versionGroups": [
    { "label": "本番の依存は catalog から取る", "dependencyTypes": ["prod"], "policy": "catalog" }
  ]
}
$ syncpack lint
= 本番の依存は catalog から取る ======================================
   2x react
      ✘ 19.1.0 → catalog: in packages/api/package.json at .dependencies (MissingFromCatalog)
      ✘ 19.1.0 → catalog: in packages/web/package.json at .dependencies (MissingFromCatalog)

fix を実行すると、pnpm-workspace.yaml に catalog: の節が追加されて版がそこに移り、package.json 側は "react": "catalog:" に置き換わる6。

packages:
  - packages/*
catalog:
  react: 19.1.0
  zod: ^3.25.0

catalog に移した後の版の更新は、syncpack update --dependency-types pnpmCatalog で catalog の項目だけを対象にできる1。

Semver Groups で範囲の書き方を決める

版そのものではなく ^ や ~ の付け方を決めるのが Semver Groups である。本番の依存は固定、devDependencies は ~、peerDependencies は ^ のように、依存の種類ごとに範囲を指定する7。

{
  "semverGroups": [
    { "dependencyTypes": ["prod"], "range": "" },
    { "dependencyTypes": ["dev"], "range": "~" },
    { "dependencyTypes": ["peer"], "range": "^" }
  ]
}

出典

  1. syncpack README. https://github.com/JamieMason/syncpack ↩ ↩2

  2. npm の syncpack パッケージ。 https://www.npmjs.com/package/syncpack ↩

  3. Syncpack - Config File. https://syncpack.dev/config/syncpackrc ↩

  4. Syncpack - Version Groups. https://syncpack.dev/version-groups/ ↩

  5. Syncpack - Banned Version Groups. https://syncpack.dev/version-groups/banned/ ↩

  6. Syncpack - Catalog Version Groups. https://syncpack.dev/version-groups/catalog/ ↩

  7. Syncpack - Semver Groups. https://syncpack.dev/semver-groups/ ↩