# 多个包里同一依赖版本各写各的，pnpm catalog 能不能收到一处？

**URL:** <https://www.shiqidu.com/t/topic/1982>\
**Category:** 开发调优\
**Tags:** 问与答, JavaScript, 前端开发, Node.js, 开源软件\
**Created:** [2026 年11 月 8 日 09:49 UTC](https://www.shiqidu.com/t/topic/1982 "2026-11-08T09:49:21Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![shiqidu\_editor](https://www.shiqidu.com/user_avatar/www.shiqidu.com/shiqidu_editor/32/528_2.png) [@shiqidu\_editor](https://www.shiqidu.com/u/shiqidu_editor)\
发布日期： [2026 年11 月 8 日 09:49 UTC](https://www.shiqidu.com/t/topic/1982/1 "2026-11-08T09:49:21Z")

</div>

`packages/app`、`packages/ui`、`packages/eslint-config` 拆开之后，同一依赖在各包 `package.json` 里各写各的范围。有人已经升到 `react@^18.3.1`，隔壁还停在 `^18.2.0`。锁文件里偶发两套解析结果，升级还要改好几处，合并冲突也爱扎在这些文件上。pnpm 官方把这类「版本常量」收进 [Catalogs](https://pnpm.io/catalogs)：在 `pnpm-workspace.yaml` 里定义一处，各包用 `catalog:` 协议引用。

## catalog 实际解决什么

文档开篇把 Catalogs 定义成 workspace 功能：把依赖版本范围写成可复用常量，再在 `package.json` 里引用。典型写法是 workspace 文件里：

```yaml
packages:
  - packages/*

catalog:
  react: ^18.3.1
  redux: ^5.0.1

```

某个包的依赖写成：

```json
{
  "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 这类工具链？
