packages/app、packages/ui、packages/eslint-config 拆开之后,同一依赖在各包 package.json 里各写各的范围。有人已经升到 react@^18.3.1,隔壁还停在 ^18.2.0。锁文件里偶发两套解析结果,升级还要改好几处,合并冲突也爱扎在这些文件上。pnpm 官方把这类「版本常量」收进 Catalogs:在 pnpm-workspace.yaml 里定义一处,各包用 catalog: 协议引用。
catalog 实际解决什么
文档开篇把 Catalogs 定义成 workspace 功能:把依赖版本范围写成可复用常量,再在 package.json 里引用。典型写法是 workspace 文件里:
packages:
- packages/*
catalog:
react: ^18.3.1
redux: ^5.0.1
某个包的依赖写成:
{
"dependencies": {
"react": "catalog:",
"redux": "catalog:"
}
}
这和直接写 ^18.3.1 等价,但版本只维护在 catalog 那一处。catalog: 后面可以跟名字(catalog:react18)。省略名字时走默认 catalog。
官方列的好处很具体:
- 尽量让同一依赖在 workspace 里只保留一个版本,少撞运行时冲突、少撑包体积。
- 升级时只改
pnpm-workspace.yaml里的 catalog 条目,不必扫一遍所有package.json。 - 升级不再改各包的
package.json,合并冲突也少一截。
catalog: 可以用在 dependencies / devDependencies / peerDependencies / optionalDependencies,也可以用在 workspace 的 overrides、以及 pnx 一类 specifier。发布或 pnpm pack 时会像 workspace: 一样被替换成真实版本范围,方便给外部包管理器消费——例如包里写的是 catalog:react18,发布后会变成 catalog 里对应的 ^18.x。
默认 catalog 和具名 catalog
两种定义方式:
- 顶层单数
catalog:创建一个名叫default的目录。引用时写catalog:或catalog:default。 - 复数
catalogs:任意起名,例如react17/react18,引用写成catalog:react18。
大仓分批升级时,可以默认 catalog 留旧线,具名 catalog 挂新线,各包按自己的进度切。注意不要同时用 catalog 和 catalogs.default 各写一遍 default,文档说会报「default catalog 被定义多次」。
已有 workspace 想迁过去,官方提了 codemod:pnx codemod pnpm/catalog。新加依赖时,pnpm add --save-catalog 会把范围写进默认 catalog,并在 package.json 里落成 catalog:。--save-catalog-name 指定具名目录。设置项 catalogMode(manual / prefer / strict)控制 pnpm add 会不会自动往默认 catalog 塞——默认仍是 manual。strict 只允许 catalog 里已有的范围。prefer 能对上就优先用 catalog。另有 catalogPrune(旧名 cleanupUnusedCatalogs)可在安装时清掉未使用的条目。
较新的 pnpm 还允许 catalog 里放 workspace:,以及 file: / link:(相对路径按 pnpm-workspace.yaml 所在目录算)。路径类 specifier 在部分版本下不会被 pnpm add 自动塞进 catalog,文档写明这类要留在声明它的那个 package.json 里,避免「同一 catalog 条目对每个包指向不同目录」。
迁移时常见卡点
把已有范围改成 catalog: 之前,先在 pnpm-workspace.yaml 里把高频依赖的范围抄进默认 catalog,再逐包替换。codemod 适合大仓批量改。小仓手改几处也行。替换后跑一次 pnpm install,确认锁文件收敛、各包解析到同一版本。若某个包必须钉死另一条大版本线,不要硬并进 default,单独开具名 catalog,避免「为了统一而统一」把还在迁移中的包拽崩。
peer 依赖也可以走 catalog:。文档允许 peer 字段引用 catalog,这样宿主和插件对 React 一类 peer 的声明能跟主应用共用同一常量,少出现「app 升了、插件 peer 还写旧范围」的告警噪声。真正发布到 npm 的包,记得 catalog 会在 publish 时展开——消费者拿到的是普通 semver 范围,不依赖他们也开了 pnpm catalog。
和「每个包自己写版本」差在哪
不用 catalog 时,每个 package.json 各自持有一份范围字符串,漂不漂全靠人眼和 CI。用了 catalog 之后,版本真相在 workspace 文件。包文件只声明「我吃 catalog 里的那条」。它不是私服,也不替你解决「必须用不同大版本」的产品分歧——那种场景反而适合具名 catalog 并存,而不是强行并成一条。
找不到条目会报 ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC。catalog 里再套 catalog: 会报非法递归定义。如果你们仓里已经靠 overrides 硬拧版本,可以先把高频共享依赖搬进默认 catalog,看升级是不是只剩改一行。你们是先迁 React 一族,还是先收 ESLint / TypeScript 这类工具链?