原文链接:https://pnpm.io/symlinked-node-modules-structure
node_modules 的符号链接结构
这篇文章仅仅描述没有 peer dependencies 的 pnpm, 是怎么构建 node_modules 结构的。
pnpm 的 node_modules 布局使用符号链接来生成一个嵌套的依赖结构。
在项目的 node_modules/.pnpm 里,每一个包都指向一个硬链接,指向内容寻址的仓库。让我们看看,假设你安装了 foo@1.0.0,它依赖 bar@1.0.0。pnpm 将会硬链接这两个包到 <store>
下, 像下面这样:
node_modules
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
│ ├── index.js
│ └── package.json
└── foo@1.0.0
└── node_modules
└── foo -> <store>/foo
├── index.js
└── package.json
现在 node_modules 下面还只有“真实”文件。一旦所有的包被硬链接到 node_modules,符号链接将会被创建以构建嵌套的依赖结构图。
你可能已经注意到,两个包都被硬链接进 node_modules 文件里的一个子文件。(foo@1.0.0/node_modules/foo)。之所以这么做是因为:
-
允许包导入他们自己。foo 应该能
require('foo/package.json')
或import * as package from "foo/package.json"
-
避免循环符号链接。 一个包的依赖被放置到和这个包相同层级的目录中。对于 Node.js 而言这两者并没有什么不同:依赖是在这个包的 node_modules 里面还是在这个包的上层目录下的 node_modules 里面。(由此知道,为什么 npm 和 yarn 要将包全部提升到 node_modules 下同一层级)
安装的下一步是用符号链接依赖。bar 将会被链接到 foo@1.0.0/node_modules 目录下:
node_modules
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
└── foo@1.0.0
└── node_modules
├── foo -> <store>/foo
└── bar -> ../../bar@1.0.0/node_modules/bar
下一步,直接依赖将会被处理。foo 将被符号链接进根 node_modules 目录,因为 foo 是这个项目的依赖。
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
└── foo@1.0.0
└── node_modules
├── foo -> <store>/foo
└── bar -> ../../bar@1.0.0/node_modules/bar
这是一个非常简单的例子。然而,不管有多少的依赖和多深的依赖图,布局都将保持这个结构。
我们再添加 qar@2.0.0 作为 foo 和 bar 的依赖。新结构将会这样:
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ ├── bar -> <store>/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
├── foo@1.0.0
│ └── node_modules
│ ├── foo -> <store>/foo
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
└── qar@2.0.0
└── node_modules
└── qar -> <store>/qar
正如你所见到的,即使现在依赖图更深了(foo 依赖 bar,bar 又依赖 qar),文件系统的目录深度依旧是相同的。
这个布局第一次看起来可能有点奇怪,但它完全兼容 Node 的处理模块算法。
当处理模块时,Node 会忽略符号链接,所以当 bar 从 foo@1.0.0/node_modules/foo/index.js 文件被引入 required 时,Node 不会使用 foo@1.0.0/node_modules/bar (它是个符号链接),相反,bar 会被解析到它的真实位置(bar@1.0.0/node_modules/bar)。
因此,bar 也能够解析在 bar@1.0.0/node_modules 下的依赖。
这个布局的最大好处是,仅是真实在项目的依赖里的包才能够被获取。而一个扁平化的 node_modules 结构,所有被提升的包都可以被获取到。