피어가 해결되는 방법

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@1.0.0_bar@1.0.0+baz@1.1.0 안에 있는 foo 심볼릭 링크를 만듭니다. 결과적으로 Node.js 모듈 resolver는 올바른 피어를 찾습니다.

패키지에 피어 의존성이 없지만 그래프에서 상위 해결된 피어와의 의존성이 있는 경우, 해당 전이 패키지는 의존성 세트가 다른 프로젝트에 나타날 수 있습니다. 예를 들어, 단일 의존성인 b@1.0.0 를 갖는 패키지 a@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로 다시 resolve되어 두 번 나타나야 합니다.

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 순으로 정렬되며, 순환을 닫는 간선은 설치 과정이 어디에서 순환에 진입하든 항상 같은 지점에서 끊어집니다. 끊어진 간선도 여전히 실제 종속성이며 프로젝트 수준에서 가져온 대상 항목을 기준으로 해석되지만, 탐색이 순환을 따라 돌지 않으므로 순환 안의 패키지는 더 이상 그 패키지에 도달하는 경로마다 한 번씩 해석되지 않습니다. 순환 안에 있는 패키지의 피어 종속성은 여전히 그 순서를 따라 가장 가까운 일치 항목으로 해석됩니다.

끊는 위치가 더 이상 설치 경로에 의존하지 않으므로, lockfile은 오직 종속성 그래프에 의해서만 결정됩니다. 반복 설치, pnpm-workspace.yaml의 packages glob 순서 변경, package.json의 항목 순서 변경 모두 바이트 단위로 동일한 lockfile을 생성합니다. 순환이 많은 종속성 그래프를 가진 프로젝트는 lockfile도 더 작아집니다. 순환 안의 패키지가 더 이상 그 패키지에 도달하는 경로마다 별도의 피어 변형을 갖지 않기 때문입니다.

기존 lockfile은 계속 작동합니다: --frozen-lockfile 설치는 이를 변경 없이 사용하며, 해석을 건너뛰는 설치는 이를 그대로 둡니다. 예를 들어 종속성 변경 후와 같이 처음으로 다시 해석하는 설치는 순환 패키지의 피어 변형 키를 한 번 다시 생성하며, 이는 일회성 lockfile 차이로 나타납니다.

끊는 위치가 그래프 탐색 순서에 따라 달라지는 pnpm v11에서는, 동일한 종속성이라도 프로젝트와 종속성이 나열된 순서에 따라 서로 다른 lockfile이 생성될 수 있습니다.