피어가 해결되는 방법
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
We create symlinks either to the foo that is inside
foo@1.0.0_bar@1.0.0_baz@1.0.0 or to the one in
foo@1.0.0_bar@1.0.0_baz@1.1.0.
결과적으로 Node.js 모듈 resolver는 올바른 피어를 찾습니다.
In pnpm-lock.yaml, these entries are keyed foo@1.0.0(bar@1.0.0)(baz@1.0.0) and
foo@1.0.0(bar@1.0.0)(baz@1.1.0).
패키지에 피어 종속성은 없지만 그래프의 상위에서 해석되는 피어를 가진 종속성이 있는 경우, 해당 전이 패키지는 서로 다른 종속성 집합과 함께 프로젝트에 나타날 수 있습니다. 예를 들어, 단일 종속성 b@1.0.0을 가진 패키지 a@1.0.0이 있다고 합시다. b@1.0.0에는 피어 종속성 c@^1이 있습니다. a does not provide c, so b's peer is resolved from a's parent,
and a gets one copy per distinct c.
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
Settings that change this behavior, such as dedupePeers, dedupePeerDependents, and autoInstallPeers, are listed in Peer dependency settings.
순환 종속성#
추가된 버전: v12.0.0-rc.5 (pnpm v12 전용)
Packages may depend on each other in a cycle: a depends on b, and b (directly, or through further packages) depends back on 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 순으로 정렬되며, 순환을 닫는 간선은 설치 과정이 어디에서 순환에 진입하든 항상 같은 지점에서 끊어집니다. The cut edge is still a real dependency (it is resolved against an occurrence of its target taken at the project level), but the walk never travels around the cycle, so a package inside a cycle is no longer resolved once per path that reaches it. 순환 안에 있는 패키지의 피어 종속성은 여전히 그 순서를 따라 가장 가까운 일치 항목으로 해석됩니다.
끊는 위치가 더 이상 설치 경로에 의존하지 않으므로, lockfile은 오직 종속성 그래프에 의해서만 결정됩니다. Repeated installs, reordered packages globs in pnpm-workspace.yaml, and reordered entries in package.json all produce a byte-identical lockfile. 순환이 많은 종속성 그래프를 가진 프로젝트는 lockfile도 더 작아집니다. 순환 안의 패키지가 더 이상 그 패키지에 도달하는 경로마다 별도의 피어 변형을 갖지 않기 때문입니다.
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.