对等依赖的解析方式
pnpm 最好的特性之一,是在同一个项目中,某个包的特定版本始终只有一套依赖。但这个规则有一个例外,就是带有对等依赖的包。
对等依赖会从依赖图中更靠上位置安装的依赖处解析,因为它们与其父级共享同一版本。这意味着如果 foo@1.0.0 有两个对等依赖(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 分别安装。这两个包也都带有 bar 和 baz,但依赖 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,或指向 foo@1.0.0_bar@1.0.0+baz@1.1.0 内的那个。其结果是,Node.js 模块解析器会找到正确的对等依赖。
如果一个包没有对等依赖,但它的依赖带有在图中更靠上处解析的对等依赖,那么该传递依赖就可能以多套不同的依赖出现在项目里。举例来说,有包 a@1.0.0,它只有一个依赖 b@1.0.0。b@1.0.0 有一个对等依赖 c@^1。a@1.0.0 永远不会解析 b@1.0.0 的对等依赖,于是它也取决于 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。当这样一个循环内的包有对等依赖时,就没有单一的「图中更靠上」的位置可供解析它们,因为从 a 进入循环和从 b 进入循环会导向不同的父级。
pnpm 在一个固定位置切断每个循环。构成循环的包按其包 ID 排序,闭合循环的那条边无论安装在何处进入该循环,都在同一位置被切断。被切断的边仍然是一条真实的依赖,它针对在项目层级上取其目标的一次出现来解析;但遍历绝不会绕着循环走一圈,因此循环内的包不再按每条到达它的路径各解析一次。循环内包的对等依赖仍会沿该顺序解析到最近匹配。
由于切断不再取决于安装所走的路径,锁文件就仅是依赖图的函数。重复安装、重排 pnpm-workspace.yaml 中的 packages 通配、重排 package.json 中的条目,都会产出逐字节相同的锁文件。依赖图循环较多的项目也会得到更小的锁文件,因为循环内的包不再按每条到达它们的路径各得到一个对等变体。
现有锁文件继续有效:--frozen-lockfile 安装会原样消费它们,而跳过解析的安装则完全不改动它们。第一次重新解析的安装(例如在依赖变更后)会为循环包的对等变体重新键控一次,表现为一次性的锁文件差异。
在 pnpm v11 中,由于切断取决于图被遍历的顺序,同样的依赖会因项目和依赖列出的先后顺序不同而产出不同的锁文件。
本页内容译自pnpm 官方文档(MIT License),如有出入以英文原文为准。