没有 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 的头号原因。
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,没定义时允许直接写 |
strict | catalog 有定义的包必须用 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 Catalog | 高 | pnpm 专属,不换工具最佳 |
本章小结
- pnpm v9 Catalog:版本只在
pnpm-workspace.yaml定义,包里用catalog:引用 - 支持多个命名 catalog(
catalogs:),实验性版本和稳定版本并存 - pnpm publish 自动展开
catalog:→ 真实版本号,外部用户无感 catalogMode: strict强制所有定义过的包必须走 catalog- Vue/Vite/Nuxt 等主流项目已大规模采用——是 2026 年 monorepo 版本管理的事实标准