Chapter 07

整仓版本只写一处

Monorepo 里 20 个包都依赖 React,手写 20 个 "react": "^18.3.1"——升版本要改 20 处,不漏才怪。pnpm v9 的 Catalog 把版本集中在 pnpm-workspace.yaml,各 package.json 写 "react": "catalog:",一处改处处生效。

没有 Catalog 的痛

// apps/web/package.json
{ "dependencies": { "react": "^18.3.1" } }

// apps/mobile/package.json
{ "dependencies": { "react": "^18.2.0" } }   // ← 版本漂移!

// packages/ui/package.json
{ "peerDependencies": { "react": "^18.0.0" } }

// packages/utils/package.json
{ "dependencies": { "react": "^18.3.0" } }

结果:React 装了三个 minor 版本,bundle 体积膨胀,运行时可能因 context mismatch 报错。

运行时翻车实例
React 要求整个应用只有一个 React 实例——不同版本的 React 之间 Hook 状态不兼容,报 "Invalid hook call"。在 monorepo 里几个包装了不同 minor 版本 + 没配好 peer,是这类 bug 的头号原因。

Catalog:一处定义

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'

catalog:
  react: ^18.3.1
  react-dom: ^18.3.1
  typescript: ^5.6.0
  vite: ^5.4.8
  zod: ^3.23.8
// apps/web/package.json
{
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:"
  }
}

// packages/ui/package.json
{
  "peerDependencies": {
    "react": "catalog:"
  }
}

升 React 18.3.2 → 只改 pnpm-workspace.yaml 一处,pnpm install 全仓同步。

多 catalog(命名目录)

catalog:
  react: ^18.3.1

catalogs:
  react19:
    react: ^19.0.0
    react-dom: ^19.0.0
  legacy:
    react: ^17.0.2
// apps/experimental/package.json(试用 React 19)
{
  "dependencies": {
    "react": "catalog:react19",
    "react-dom": "catalog:react19"
  }
}

主应用用默认 catalog 稳定的 React 18,实验应用用 react19 catalog 抢先升级——互不打扰。

发布时会怎样

// 源码里
{ "dependencies": { "react": "catalog:" } }

// pnpm publish 时,package.json 被重写为
{ "dependencies": { "react": "^18.3.1" } }

对 npm 上的用户来说就是正常版本号,完全无感——catalog 是 pnpm 内部的"视图"机制。

和 workspace: 协议是同一套思路
workspace:* 发版时被替换成真实版本,catalog: 被替换成 catalog 里的版本——都是"源码写符号、发布写实值"的胶水。

实际工作流:升级依赖

# 看哪些可升
pnpm outdated -r

# 1. 改 pnpm-workspace.yaml 的 catalog:
#    react: ^18.3.2 → ^19.0.0

# 2. 跑 install
pnpm install

# 3. 全仓重测
pnpm -r test

如果 react 在 50 个包里引用——传统做法要 50 个 PR(或一个 PR 改 50 处,冲突难平);Catalog 做法就是改 1 行。

和 peer + catalog 的组合

# pnpm-workspace.yaml
catalog:
  react: ^18.3.1

catalogMode: strict   # 所有 react 引用都必须走 catalog
catalogMode行为
manual(默认)可以用 catalog: 也可以直接写版本号
prefer优先用 catalog,没定义时允许直接写
strictcatalog 有定义的包必须用 catalog:,否则报错

strict 适合大团队——防止有人偷偷写死版本绕过集中管理。

自动写入 catalog(v10 新功能)

pnpm add zod --save-catalog
# 1. 如果 catalog 里已有 zod → package.json 写 "zod": "catalog:"
# 2. 如果没有 → 新增到 catalog + 所有子包统一写 "catalog:"

降低使用门槛——团队不需要每次手动维护 pnpm-workspace.yaml。

配合 Renovate / Dependabot

// .github/renovate.json
{
  "pnpm": {
    "managerFilePatterns": ["pnpm-workspace.yaml"]
  },
  "packageRules": [
    {
      "matchManagers": ["pnpm"],
      "matchFileNames": ["pnpm-workspace.yaml"],
      "groupName": "catalog"
    }
  ]
}

Renovate 能识别 catalog 字段、一次 PR 升一批依赖——50 个包变成 1 个 PR。

实际项目:Vue 核心仓库的 catalog

# vuejs/core 的 pnpm-workspace.yaml 片段(实际结构)
catalog:
  '@babel/parser': ^7.25.3
  'estree-walker': ^2.0.2
  'magic-string': ^0.30.11
  source-map-js: ^1.2.0
  vite: ^5.4.1
  vitest: ^2.0.5

Vue、Vite、Nuxt、Vitest 等主流仓库都迁到了 Catalog——是 2025 年 monorepo 版本管理的事实标准。

常见坑

catalog: 里没定义那个包
写了 "foo": "catalog:" 但 pnpm-workspace.yaml 的 catalog 里没 foo → install 报错。加上就好。
catalog 和 workspace 的区别
workspace:* 引用 monorepo 内部包;catalog: 引用 npm 上的外部包。完全不冲突,可以同时用。
老 pnpm 版本打开仓库报错
Catalog 是 v9 功能,要求 packageManager: "pnpm@9.x"。Corepack 会自动处理。

对比其它方案

方案集中度缺点
手工同步0版本漂移家常便饭
syncpack(工具)要定时跑 + 可能被人绕过
Nx shared-versions绑定 Nx 生态
Yarn resolutions语义是"强制覆盖",不是"默认值"
pnpm Catalogpnpm 专属,不换工具最佳

本章小结