Skip to main content
Version: 11.x

符号链接的 `node_modules` 结构

info

本文仅描述了在没有具有同行依赖的包的情况下,pnpm 的 node_modules 是如何构建的。对于具有同行依赖的更复杂情况,请参见 如何解析同行依赖

🌐 This article only describes how pnpm's node_modules are structured when there are no packages with peer dependencies. For the more complex scenario of dependencies with peers, see how peers are resolved.

pnpm 的 node_modules 布局使用符号链接来创建嵌套的依赖结构。

🌐 pnpm's node_modules layout uses symbolic links to create a nested structure of dependencies.

node_modules 中的每个包的每个文件都是指向内容可寻址存储的硬链接。假设你安装了依赖 bar@1.0.0foo@1.0.0。pnpm 会像这样将两个包硬链接到 node_modules

🌐 Every file of every package inside node_modules is a hard link to the content-addressable store. Let's say you install foo@1.0.0 that depends on bar@1.0.0. pnpm will hard link both packages to node_modules like this:

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,就会创建符号链接以构建嵌套依赖图结构。

🌐 These are the only "real" files in node_modules. Once all the packages are hard linked to node_modules, symbolic links are created to build the nested dependency graph structure.

正如你可能已经注意到的,两个软件包都被硬链接到 node_modules 文件夹内的一个子文件夹(foo@1.0.0/node_modules/foo)中。这是为了:

🌐 As you might have noticed, both packages are hard linked into a subfolder inside a node_modules folder (foo@1.0.0/node_modules/foo). This is needed to:

  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 文件夹:

🌐 The next stage of installation is symlinking dependencies. bar is going to be symlinked to the foo@1.0.0/node_modules folder:

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 将被创建符号链接到根 node_modules 文件夹,因为 foo 是项目的依赖:

🌐 Next, direct dependencies are handled. foo is going to be symlinked into the root node_modules folder because foo is a dependency of the project:

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

这是一个非常简单的例子。然而,无论依赖的数量和依赖图的深度如何,布局都会保持这种结构。

🌐 This is a very simple example. However, the layout will maintain this structure regardless of the number of dependencies and the depth of the dependency graph.

让我们将 qar@2.0.0 添加为 barfoo 的依赖。新的结构将如下所示:

🌐 Let's add qar@2.0.0 as a dependency of bar and foo. This is how the new structure will look:

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),文件系统中的目录深度仍然相同。

🌐 As you may see, even though the graph is deeper now (foo > bar > qar), the directory depth in the file system is still the same.

这个布局乍一看可能显得奇怪,但它与 Node 的模块解析算法完全兼容!在解析模块时,Node 会忽略符号链接,所以当从 foo@1.0.0/node_modules/foo/index.js 中需要 bar 时,Node 不会使用 foo@1.0.0/node_modules/bar 下的 bar,而是将 bar 解析到它的实际位置 (bar@1.0.0/node_modules/bar)。因此,bar 也可以解析位于 bar@1.0.0/node_modules 中的依赖。

🌐 This layout might look weird at first glance, but it is completely compatible with Node's module resolution algorithm! When resolving modules, Node ignores symlinks, so when bar is required from foo@1.0.0/node_modules/foo/index.js, Node does not use bar at foo@1.0.0/node_modules/bar, but instead, bar is resolved to its real location (bar@1.0.0/node_modules/bar). As a consequence, bar can also resolve its dependencies which are in bar@1.0.0/node_modules.

这种布局的一个很大优势是,只有真正属于依赖的包可以被访问。在扁平化的 node_modules 结构中,所有提升的包都可以被访问。要了解为什么这是一个优势,请参阅 "pnpm 的严格性有助于避免愚蠢的错误"

🌐 A great bonus of this layout is that only packages that are really in the dependencies are accessible. With a flattened node_modules structure, all hoisted packages are accessible. To read more about why this is an advantage, see "pnpm's strictness helps to avoid silly bugs"

不幸的是,生态系统中许多软件包是损坏的——它们使用了未在其 package.json 中列出的依赖。为了尽量减少新用户遇到的问题,pnpm 默认将所有依赖提升到 node_modules/.pnpm/node_modules。要禁用这种提升,请将 hoist 设置为 false

🌐 Unfortunately, many packages in the ecosystem are broken — they use dependencies that are not listed in their package.json. To minimize the number of issues new users encounter, pnpm hoists all dependencies by default into node_modules/.pnpm/node_modules. To disable this hoisting, set hoist to false.