对等依赖 (peers) 是如何被处理的
pnpm 的最佳特性之一是,在一个项目中,软件包的特定版本将始终具有同一组依赖项。不过,这个规则 有一个例外 - 具有对等依赖 (peer dependencies)的软件包。
对等依赖会从依赖图中更高的已安装的依赖项中解析,因为它们与父级共享相同的版本。这意味着,如果 foo@1.0.0 有两个peers依赖(bar@^1 和 baz@^1),那么它可能在一个项目中有多个不同的依赖项集合。
- foo-parent-1
- bar@1.0.0
- baz@1.0.0
- foo@1.0.0
- foo-parent-2
- bar@1.0.0
- baz@1.1.0
- foo@1.0.0
在上面的示例中, foo@1.0.0 已安装在 foo-parent-1 和 foo-parent-2 中。这两个包都有依赖包 baz 和 bar, 但是它们却依赖着不同版本的 baz。因此, foo@1.0.0 有两组不同的依赖项:一组具有 baz@1.0.0 ,另一组具有 baz@1.1.0。若要支持这些用例,pnpm 必须有几组不同的依赖项,就去硬链接几次 foo@1.0.0。
通常,如果一个包没有对等依赖项,它会被硬链接到其依赖项的符号链接旁边的 node_modules 文件夹,如下所示:
node_modules
└── .pnpm
├── foo@1.0.0
│ └── node_modules
│ ├── foo
│ ├── qux -> ../../qux@1.0.0/node_modules/qux
│ └── plugh -> ../../plugh@1.0.0/node_modules/plugh
├── qux@1.0.0
├── plugh@1.0.0
但是,如果 foo 有对等依赖,那么它可能就会有多组依赖项,所以我们为不同的对等依赖项创建不同的解析:
node_modules
└── .pnpm
├── foo@1.0.0_bar@1.0.0+baz@1.0.0
│ └── node_modules
│ ├── foo
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│ ├── baz -> ../../baz@1.0.0/node_modules/baz
│ ├── qux -> ../../qux@1.0.0/node_modules/qux
│ └── plugh -> ../../plugh@1.0.0/node_modules/plugh
├── foo@1.0.0_bar@1.0.0+baz@1.1.0
│ └── node_modules
│ ├── foo
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│ ├── baz -> ../../baz@1.1.0/node_modules/baz
│ ├── qux -> ../../qux@1.0.0/node_modules/qux
│ └── plugh -> ../../plugh@1.0.0/node_modules/plugh
├── bar@1.0.0
├── baz@1.0.0
├── baz@1.1.0
├── qux@1.0.0
├── plugh@1.0.0
我们创建 foo@1.0.0_bar@1.0.0+baz@1.0.0 或foo@1.0.0_bar@1.0.0+baz@1.1.0 内到 foo 的软链接。因此,Node.js 模块解析器将找到正确的对等依赖。
如果某个包没有对等依赖,但存在依赖项,其含有在途中被更高解析的对等依赖,那么该传递包可以出现在具有不同依赖关系集的项目中。例如,a@1.0.0 具有单个依赖项 b@1.0.0。 b@1.0.0 有一个对等依赖为 c@^1。 a@1.0.0 永远不会解析b@1.0.0的 peer, 所以它也会依赖于 b@1.0.0 的对等。
该结构在 node_modules 中的样子如下。在这个例子中,a@1.0.0 需要在项目的node_modules 中出现两次 - 其中一次是被 c@1.0.0 解析,另一次被 c@1.1.0 再次解析。
node_modules
└── .pnpm
├── a@1.0.0_c@1.0.0
│ └── node_modules
│ ├── a
│ └── b -> ../../b@1.0.0_c@1.0.0/node_modules/b
├── a@1.0.0_c@1.1.0
│ └── node_modules
│ ├── a
│ └── b -> ../../b@1.0.0_c@1.1.0/node_modules/b
├── b@1.0.0_c@1.0.0
│ └── node_modules
│ ├── b
│ └── c -> ../../c@1.0.0/node_modules/c
├── b@1.0.0_c@1.1.0
│ └── node_modules
│ ├── b
│ └── c -> ../../c@1.1.0/node_modules/c
├── c@1.0.0
├── c@1.1.0
循环依赖#
添加于:v12.0.0-rc.5(仅 pnpm v12)
包之间可能以循环的方式相互依赖:a 依赖 b,而 b ——直接地,或通过更多的包——反过来又依赖 a。 When a package inside such a cycle has peer dependencies, there is no single point "higher in the graph" to resolve them from, because entering the cycle at a and entering it at b lead to different parents.
pnpm 会在固定的位置切断每个循环。构成循环的包会按其包 ID 排序,而闭合该循环的边总会在同一点被切断,无论安装过程从何处进入该循环。被切断的边仍然是一条真实的依赖 —— 它会按照其目标在项目层面的一次出现来解析 —— 但遍历永远不会绕着循环行进,因此循环内的一个包不再为每条到达它的路径各解析一次。 Peer dependencies of packages inside a cycle still resolve to the nearest match along that order.
由于切断不再取决于安装所走的路径,锁文件仅由依赖图决定。 Repeated installs, reordered packages globs in pnpm-workspace.yaml, and reordered entries in package.json all produce a byte-identical lockfile. Projects with cycle-heavy dependency graphs also end up with a smaller lockfile, since packages inside a cycle no longer get a separate peer variant per path that reaches them.
Existing lockfiles keep working: --frozen-lockfile installs consume them unchanged, and installs that skip resolution leave them untouched. The first install that re-resolves — after a dependency change, for instance — re-keys the peer variants of cyclic packages once, which shows up as a one-time lockfile diff.
In pnpm v11, where the cut depends on the order the graph is walked, the same dependencies can produce different lockfiles depending on the order projects and dependencies are listed in.