symlink된 `node_modules` 구조

정보

이 글은 피어 종속성이 있는 패키지가 없을 때 pnpm의 node_modules가 어떻게 구성되는지만 설명합니다. 피어가 있는 종속성의 더 복잡한 시나리오는 피어 종속성 해석 방식을 참조하세요.

pnpm의 node_modules 레이아웃은 심볼릭 링크를 사용하여 종속성의 중첩 구조를 만듭니다.

node_modules 안에 있는 모든 패키지의 모든 파일은 콘텐츠 주소 지정 스토어에 대한 하드 링크입니다. 예를 들어 bar@1.0.0에 의존하는 foo@1.0.0을 설치한다고 가정해 봅시다. pnpm은 두 패키지를 다음과 같이 node_modules에 하드 링크합니다:

node_modules
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       └── bar
    │           ├── index.js     -> <store>/001
    │           └── package.json -> <store>/002
    └── foo@1.0.0
        └── node_modules
            └── foo
                ├── index.js     -> <store>/003
                └── package.json -> <store>/004

이것들이 node_modules에 있는 유일한 "실제" 파일입니다. 모든 패키지가 node_modules에 하드 링크되면, 중첩된 종속성 그래프 구조를 만들기 위해 심볼릭 링크가 생성됩니다.

눈치채셨겠지만, 두 패키지 모두 node_modules 폴더 안의 하위 폴더(foo@1.0.0/node_modules/foo)에 하드 링크됩니다. 이것은 다음을 위해 필요합니다:

  1. 패키지가 자기 자신을 가져올 수 있도록 합니다. foo는 require('foo/package.json') 또는 import * as package from "foo/package.json"을 할 수 있어야 합니다.
  2. 순환 심볼릭 링크를 방지합니다. 패키지의 종속성은 해당 패키지가 있는 폴더와 같은 폴더에 배치됩니다. Node.js에서는 종속성이 패키지의 node_modules 안에 있든 상위 디렉터리의 다른 node_modules에 있든 차이가 없습니다.

설치의 다음 단계는 의존성을 심볼릭 링크하는 것입니다. bar는 foo@1.0.0/node_modules 폴더에 심볼릭 링크됩니다:

node_modules
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       └── bar -> <store>
    └── foo@1.0.0
        └── node_modules
            ├── foo -> <store>
            └── bar -> ../../bar@1.0.0/node_modules/bar

다음으로, 직접 의존성이 처리됩니다. foo는 프로젝트의 종속성이므로 foo는 루트 node_modules 폴더에 심볼릭 링크됩니다:

node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       └── bar -> <store>
    └── foo@1.0.0
        └── node_modules
            ├── foo -> <store>
            └── bar -> ../../bar@1.0.0/node_modules/bar

이것은 매우 간단한 예입니다. 그러나 이 레이아웃은 의존성 수와 의존성 그래프의 깊이에 관계없이 이 구조를 유지합니다.

이제 bar와 foo의 종속성으로 qar@2.0.0을 추가해 봅시다. 새로운 구조는 다음과 같습니다.

node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       ├── bar -> <store>
    │       └── qar -> ../../qar@2.0.0/node_modules/qar
    ├── foo@1.0.0
    │   └── node_modules
    │       ├── foo -> <store>
    │       ├── bar -> ../../bar@1.0.0/node_modules/bar
    │       └── qar -> ../../qar@2.0.0/node_modules/qar
    └── qar@2.0.0
        └── node_modules
            └── qar -> <store>

보시다시피 이제 그래프가 더 깊어졌지만(foo > bar > qar), 파일 시스템의 디렉터리 깊이는 여전히 같습니다.

이 레이아웃은 언뜻 보기에는 이상해보일 수 있지만, Node의 모듈 resolution 알고리즘과 완벽하게 호환됩니다. 모듈을 해석할 때 Node는 심볼릭 링크를 무시하므로, foo@1.0.0/node_modules/foo/index.js에서 bar를 require하면 Node는 foo@1.0.0/node_modules/bar에 있는 bar를 사용하지 않고, 대신 bar를 실제 위치(bar@1.0.0/node_modules/bar)로 해석합니다. 그 결과 bar도 bar@1.0.0/node_modules에 있는 자신의 종속성을 해석할 수 있습니다.

이 레이아웃의 큰 장점은 실제로 의존성에 있는 패키지에만 액세스할 수 있다는 것입니다. 평탄화된 node_modules 구조에서는 호이스팅된 모든 패키지에 접근할 수 있습니다. 이것이 왜 장점인지 자세히 알아보려면 "pnpm's strictness helps to avoid silly bugs"를 참조하세요

안타깝게도 생태계의 많은 패키지가 잘못되어 있습니다 — 자신의 package.json에 나열되지 않은 종속성을 사용합니다. 새 사용자가 겪는 문제를 최소화하기 위해 pnpm은 기본적으로 모든 종속성을 node_modules/.pnpm/node_modules로 호이스팅합니다. 이 호이스팅을 비활성화하려면 hoist를 false로 설정하세요.