pnpm 的内容寻址存储 
pnpm (performant npm) 是一款面向 node.js 的包管理器,其设计目标是 提升安装速度 和 磁盘空间利用效率,相较于 npm 和 yarn classic 等替代方案具有显著优势。pnpm 的核心特性包括:
- 通过链接(hardlink或reflink)从全局存储库引用文件实现高效率的安装速度。
- 通过软链(symbolic link)以及结构化目录设计来强制执行严格的依赖访问规则。
- 内置对单一代码库(monorepo)管理的支持。
- 通过 pnpm-lock.yaml文件确保依赖安装的确定性。
pnpm 的基石是 内容寻址存储 机制。与传统包管理器在每个项目中复制依赖文件不同的是 pnpm 会根据文件内容哈希值(content hash)将文件存储在全局(通常是每个磁盘或文件系统一个)的存储库中。这意味着,如果多个不同的包或同一包的不同版本包含了完全相同的文件,那么在物理磁盘上,该文件仅会存储一份。这是 pnpm 实现显著磁盘空间节省的关键所在。
内容寻址存储 (CAS) 架构 
内容寻址原理 
内容寻址存储 的 核心思想 是:文件以其 内容哈希值 为唯一标识符进行 存储 和 检索。当 pnpm 需要存储一个文件时,他会计算该文件的 内容哈希值。如果全局存储库([[pnpm store path]]/files/)中已存在具有相同哈希值的文件,pnpm 就知道这个文件内容没有变化,无需重复存储。因此,即使 100 个项目都依赖 lodash,或者依赖了 lodash 的不同版本,其中相同的文件在 CAS 中也只存在一份物理副本。这与早期 npm 或 yarn 的 classic 版本中为每个项目的 node_modules 目录下创建依赖副本的做法形成了鲜明对比。
CAS 的优势 
- 磁盘空间效率: CAS显著减少了磁盘空间占用,尤其是在拥有大量项目或依赖项体积庞大的场景下,可以节省数GB的空间。更重要的是,当更新依赖时,pnpm只需向存储库添加版本间 实际发生变化的新文件,而不是复制整个新版本的包。
- 安装速度: 由于只需下载存储库中缺失的文件,并主要通过链接(而非复制)文件到项目中,大大减少了 网络传输 和 磁盘 I/O操作,从而显著提升了安装速度。此外,pnpm对每个包的安装过程可以 并行处理,进一步提高了效率。
存储位置 (storeDir) 
pnpm 存储库的默认位置取决于操作系统和环境变量的设置。可以通过在项目根目录的 .npmrc 文件、工作区配置文件 pnpm-workspace.yaml 或全局配置文件中设置 storeDir 选项来自定义存储库路径。
默认 storeDir 位置
| 操作系统 | 环境变量条件 | 默认路径 | 
|---|---|---|
| Linux | $PNPM_HOME已设置 | $PNPM_HOME/store | 
| Linux | $XDG_DATA_HOME已设置 | $XDG_DATA_HOME/pnpm/store | 
| Linux | 其他情况 | ~/.local/share/pnpm/store | 
| macOS | $PNPM_HOME已设置 | $PNPM_HOME/store | 
| macOS | 其他情况 | ~/Library/pnpm/store | 
| Windows | $PNPM_HOME已设置 | $PNPM_HOME/store | 
| Windows | 其他情况 | ~/AppData/Local/pnpm/store | 
可以通过 pnpm store path 命令查看当前 pnpm 的全局存储库路径。
WARNING
一个至关重要的前提是,为了使 pnpm 能够使用硬链接(hard links)或引用链接(reflinks,也称写时复制链接)来链接文件,存储库必须与进行安装的项目位于同一文件系统或驱动器上。如果 storeDir 指向了不同的驱动器,pnpm 将无法创建这些高效的链接,而会回退到文件复制的方式。这虽然能保证安装成功,但会完全失去 pnpm 在 磁盘空间 和 安装速度 上的核心优势。文件系统的限制是硬链接(通常也包括引用链接)无法跨越分区或驱动器边界。pnpm 会智能地检测这种情况,并在链接不可行时优先保证正确性(通过复制)而非效率。
因此,物理磁盘布局直接影响 pnpm 的性能表现。如果用户没有显式设置 storeDir,并且在不同的驱动器上运行 pnpm install,pnpm 会在每个驱动器上自动创建一个独立的存储库(例如,在驱动器 D: 的根目录下创建 D:\\.pnpm-store)。
pnpm-lock.yaml 的作用 
pnpm-lock.yaml 文件扮演着至关重要的角色。他精确记录了项目解析后的依赖树结构,包括每个依赖项的确切版本及其自身的依赖关系,从而保证了每次安装都能生成完全相同的 node_modules 结构,实现了确定性安装。此文件还存储了从包注册表(registry)获取的每个依赖包的 integrity 哈希值(通常是 SHA-512)。这个哈希值是该包特定版本内容的唯一 指纹。
- 缓存查找的钥匙: 是 - pnpm在全局内容可寻址存储中查找是否已存在该精确内容包的主要依据。- pnpm会尝试定位到由这个- integrity标识的缓存文件。
- 缓存未命中时(已下载过的包)的校验标准: 如果缓存中没有找到对应的包(缓存未命中), - pnpm就需要下载。下载完成后,- pnpm-lock.yaml中的这个- integrity值就成为必须达到的目标哈希值,- pnpm会对下载的包(- tarball文件)自行计算一遍包的- integrity值,并将其与- pnpm-lock.yaml中的- integrity值进行比较,如果一致,则认为下载的包是完整且未经篡改的。
- 缓存未命中时(未下载过的包): 如果缓存中没有找到对应的包(缓存未命中), - pnpm就需要下载。下载完成后,- pnpm自行会计算下载的包(- tarball文件)的- integrity值,并将其与注册表中(默认- http://registry.npmjs.org/)记录的- integrity值进行比较,如果一致,则认为下载的包是完整且未经篡改的,将其记录到- pnpm-lock.yaml中。
PS
- Tarball 完整性: 在下载过程中或下载完成后,pnpm会计算下载到的tarball文件的哈希值,并将其与pnpm-lock.yaml或注册表元数据中记录的预期integrity哈希值进行比较。如果不匹配,则表示下载的文件可能已损坏或被篡改,pnpm会报错并中止安装。值得注意的是,出于安全考虑,npm生态系统已从早期使用的不安全的SHA-1哈希转向了加密强度更高的SHA-512哈希。
- 存储库完整性 (verifyStoreIntegrity):pnpm提供了一个配置项verifyStoreIntegrity,其默认值为true。当此选项启用时,如果pnpm发现CAS中某个文件自上次写入后被修改过,他会在将该文件链接到项目node_modules之前对其内容进行校验。这为防止存储库意外损坏提供了一层额外的保障,但也会带来微小的性能开销。
- 锁文件完整性 (潜在问题): 尽管 pnpm-lock.yaml保证了安装的确定性,但它本身也可能在版本控制操作(如git合并冲突)中被意外破坏或手动错误编辑。目前,如果锁文件与package.json的依赖声明一致,pnpm会倾向于信任锁文件的内容以获得更快的安装速度。然而,社区 讨论 中已经提出了增加锁文件校验和(如lockfileChecksum字段)的可能性,以便在不进行完整依赖重新解析的情况下,快速检测锁文件是否被篡改。
这种设计体现了在 安装速度 和 健壮性 之间的权衡。pnpm 默认优先考虑速度,通过信任最新的锁文件来跳过耗时的 依赖解析 和 验证 步骤。然而,锁文件合并冲突或手动编辑可能导致静默的错误。verifyStoreIntegrity 设置和 提议中的锁文件校验和 机制,都是为了在不过度牺牲性能的前提下增加安全网,这反映了优化包管理器时固有的设计挑战。
文件哈希、索引与 CAS 内存储 
单个文件的内容哈希 
当一个包的 tarball 被成功下载并校验通过后,pnpm 会对其内容进行处理。一个关键步骤是,pnpm 会为包内的 每一个单独文件 计算其内容哈希值(例如 SHA-512)。
这里需要清晰地区分两种哈希值:包完整性哈希 (package integrity hash) 和 文件内容哈希 (file content hash)。
- 包完整性哈希(记录在 - pnpm-lock.yaml中)用于唯一标识和验证整个包的特定版本(即- tarball文件本身)。- 下述是 - compressible包的- pnpm-lock.yaml文件中的相关内容:yaml- lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false importers: .: dependencies: compressible: specifier: 2.0.18 version: 2.0.18 packages: compressible@2.0.18: resolution: { integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== } engines: { node: '>= 0.6' } mime-db@1.54.0: resolution: { integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== } engines: { node: '>= 0.6' } snapshots: compressible@2.0.18: dependencies: mime-db: 1.54.0 mime-db@1.54.0: {}
- 文件内容哈希是用于标识包内每个独立文件的具体内容,是实现 - CAS存储和文件去重的基础,- pnpm会存储包中的每一个文件并按照文件内容哈希值进行存储(- storeDir/v10/files/)。- 下述是 - compressible包在- pnpm全局存储的索引文件,里面会包含包中所有的文件的- integrity值,这些都是- pnpm在下载包时计算得出的。json- { "name": "compressible", "version": "2.0.18", "requiresBuild": false, "files": { "LICENSE": { "checkedAt": 1745925782020, "integrity": "sha512-iwWNtxw0wslfJ9F2n8JXm5zpCbwR7Gu+rFrXsuO44MbiUu1TZov30H2eWdbt7+mvwcEp0nSR1CyUCYsPUbI+WQ==", "mode": 420, "size": 1233 }, "index.js": { "checkedAt": 1745925782021, "integrity": "sha512-1/M/lsoE88CWoC2i0B8H/0/1OqcNqutPKkOZ6r4VRFXLD8XkfgiBjWTaySq6DxOPG3ta5xiVfdPZmn2/2wXidg==", "mode": 420, "size": 1038 }, "package.json": { "checkedAt": 1745925782021, "integrity": "sha512-Wk1cB0QJbo7jJUs0U25uFc/2lqJNNbLaXrcPDQ+S9gxPpNcOZYDnHqLtJ0Ep2893chz2MdxN1mArJloAXbTERw==", "mode": 420, "size": 1311 }, "HISTORY.md": { "checkedAt": 1745925782022, "integrity": "sha512-HjgVdL/NlSwDhkut3BCwnal4ahlNd2eLzPCGPARs0Wy+rjHtAJNL7/s426/dIwbg7NI5wnkxuhLImjx+ULgavg==", "mode": 420, "size": 1976 }, "README.md": { "checkedAt": 1745925782022, "integrity": "sha512-GahpusTGRt2M/0s2vsEMBfDm0bBXMVlnhBVY+xoDcfBKn4rGbeW9PGQWZ7LM1adQWm8Q3WizIF1D1jwI97IS7w==", "mode": 420, "size": 1797 } } }
CAS 中的包文件存储 (v10/files/) 
pnpm 存储库内部有一个特定的目录结构来存放这些基于 内容哈希 的包文件集合。在 pnpm 的 v10 版本中,这个结构位于 storeDir/v10/files/ 路径下。
以 compressible@2.0.18 为例,该包需要存储的文件集合包含如下:
compressible@2.0.18
├── HISTORY.md
├── index.js
├── LICENSE
├── package.json
└── README.md以其中的 index.js 为例,检索 compressible@2.0.18 存储在 pnpm 全局存储库中的索引文件路径为:
00/5debecfe5d5b12fc331c884d132539140d68e036224005693af893b054ba68-compressible@2.0.18.json打开该索引文件,可以看到:
{
  "name": "compressible",
  "version": "2.0.18",
  "requiresBuild": false,
  "files": {
    "LICENSE": {
      "checkedAt": 1745925782020,
      "integrity": "sha512-iwWNtxw0wslfJ9F2n8JXm5zpCbwR7Gu+rFrXsuO44MbiUu1TZov30H2eWdbt7+mvwcEp0nSR1CyUCYsPUbI+WQ==",
      "mode": 420,
      "size": 1233
    },
    "index.js": {
      "checkedAt": 1745925782021,
      "integrity": "sha512-1/M/lsoE88CWoC2i0B8H/0/1OqcNqutPKkOZ6r4VRFXLD8XkfgiBjWTaySq6DxOPG3ta5xiVfdPZmn2/2wXidg==",
      "mode": 420,
      "size": 1038
    },
    "package.json": {
      "checkedAt": 1745925782021,
      "integrity": "sha512-Wk1cB0QJbo7jJUs0U25uFc/2lqJNNbLaXrcPDQ+S9gxPpNcOZYDnHqLtJ0Ep2893chz2MdxN1mArJloAXbTERw==",
      "mode": 420,
      "size": 1311
    },
    "HISTORY.md": {
      "checkedAt": 1745925782022,
      "integrity": "sha512-HjgVdL/NlSwDhkut3BCwnal4ahlNd2eLzPCGPARs0Wy+rjHtAJNL7/s426/dIwbg7NI5wnkxuhLImjx+ULgavg==",
      "mode": 420,
      "size": 1976
    },
    "README.md": {
      "checkedAt": 1745925782022,
      "integrity": "sha512-GahpusTGRt2M/0s2vsEMBfDm0bBXMVlnhBVY+xoDcfBKn4rGbeW9PGQWZ7LM1adQWm8Q3WizIF1D1jwI97IS7w==",
      "mode": 420,
      "size": 1797
    }
  }
}index.js 文件的 integrity 哈希值为
sha512-1/M/lsoE88CWoC2i0B8H/0/1OqcNqutPKkOZ6r4VRFXLD8XkfgiBjWTaySq6DxOPG3ta5xiVfdPZmn2/2wXidg==pnpm 会根据该 integrity 值按如下方式存储文件:
- 内容哈希值通常被转换成十六进制字符串。 ts- function base64ToHexNode(base64: string): string { return Buffer.from(base64, 'base64').toString('hex'); } const hex = base64ToHexNode( 'sha512-1/M/lsoE88CWoC2i0B8H/0/1OqcNqutPKkOZ6r4VRFXLD8XkfgiBjWTaySq6DxOPG3ta5xiVfdPZmn2/2wXidg=='.slice( 7 ) ); console.assert( hex === 'd7f33f96ca04f3c096a02da2d01f07ff4ff53aa70daaeb4f2a4399eabe154455cb0fc5e47e08818d64dac92aba0f138f1b7b5ae718957dd3d99a7dbfdb05e276' );
- 获取到的十六进制哈希值的前两个字符被用作一个子目录的名称。 - 上述获取的 - hex值为bash- d7f33f96ca04f3c096a02da2d01f07ff4ff53aa70daaeb4f2a4399eabe154455cb0fc5e47e08818d64dac92aba0f138f1b7b5ae718957dd3d99a7dbfdb05e276- 前两个字符为 - d7,所以该文件将被存储在- storeDir/v10/files/d7/目录下。
- 哈希值剩余的部分构成了该文件在子目录中的文件名。 - 例子中的剩余部分为 bash- f33f96ca04f3c096a02da2d01f07ff4ff53aa70daaeb4f2a4399eabe154455cb0fc5e47e08818d64dac92aba0f138f1b7b5ae718957dd3d99a7dbfdb05e276
通过观察已存储的其他文件路径信息:
00/0a57539c3ea49736380ab214874f4528b455742d1f21c9d8027015cfae1372aa80382fefb88fd0abdd89c27c2d1332561e5186e4fa9ec8c348fc5768ca5b22
00/0cdacf5df3cc29401b02c11f6b4c473b193dfff269021e66d0749bdbe81b1c2336a5a72d1384a0d721b72064857a031a4c937653ed07c5e38731682c4c3eb8
1f/1a6454d1d08337c03084eb136fcd6f1ca1448a7f10cf150bd6dbb65aef9fa3be5b182f02d9127dc0ceadb3ad9a3a0911a98a7b63538afed05b8c5581e357db可知存储文件的文件名是截取 hash 值的 124 个字符。
综上 compressible@2.0.18/index.js 文件将被存储在
storeDir/v10/files/d7/f33f96ca04f3c096a02da2d01f07ff4ff53aa70daaeb4f2a4399eabe154455cb0fc5e47e08818d64dac92aba0f138f1b7b5ae718957dd3d99a7dbfdb05e276路径中的 v10 表明这是 pnpm 存储布局的第十个主要版本。这意味着 pnpm 的内部结构可能会随着版本迭代而优化和演进,尽管 CAS 的核心概念保持不变。了解这一点很重要,特别是对于那些试图直接与存储库交互(通常不推荐)的用户来说,因为具体的内部布局是实现细节,可能会发生变化。
CAS 中的包元信息文件存储 (v10/index/) 
除了存储单个文件外,pnpm 还需要存储包的元信息,元信息中包含了 包的名称、版本、文件名 与 文件内容哈希值 的映射关系。存储方式(CAS)与 v10/files/ 类似,不过存储元信息文件的目录与存储包文件的目录是分开的,即 v10/index/。
以 compressible@2.0.18 的包元信息文件内容为例:
{
  "name": "compressible",
  "version": "2.0.18",
  "requiresBuild": false,
  "files": {
    "LICENSE": {
      "checkedAt": 1745925782020,
      "integrity": "sha512-iwWNtxw0wslfJ9F2n8JXm5zpCbwR7Gu+rFrXsuO44MbiUu1TZov30H2eWdbt7+mvwcEp0nSR1CyUCYsPUbI+WQ==",
      "mode": 420,
      "size": 1233
    },
    "index.js": {
      "checkedAt": 1745925782021,
      "integrity": "sha512-1/M/lsoE88CWoC2i0B8H/0/1OqcNqutPKkOZ6r4VRFXLD8XkfgiBjWTaySq6DxOPG3ta5xiVfdPZmn2/2wXidg==",
      "mode": 420,
      "size": 1038
    },
    "package.json": {
      "checkedAt": 1745925782021,
      "integrity": "sha512-Wk1cB0QJbo7jJUs0U25uFc/2lqJNNbLaXrcPDQ+S9gxPpNcOZYDnHqLtJ0Ep2893chz2MdxN1mArJloAXbTERw==",
      "mode": 420,
      "size": 1311
    },
    "HISTORY.md": {
      "checkedAt": 1745925782022,
      "integrity": "sha512-HjgVdL/NlSwDhkut3BCwnal4ahlNd2eLzPCGPARs0Wy+rjHtAJNL7/s426/dIwbg7NI5wnkxuhLImjx+ULgavg==",
      "mode": 420,
      "size": 1976
    },
    "README.md": {
      "checkedAt": 1745925782022,
      "integrity": "sha512-GahpusTGRt2M/0s2vsEMBfDm0bBXMVlnhBVY+xoDcfBKn4rGbeW9PGQWZ7LM1adQWm8Q3WizIF1D1jwI97IS7w==",
      "mode": 420,
      "size": 1797
    }
  }
}- 内容: 每个元信息文件相当于该包特定版本的一个清单( - manifest),他包含了包的元数据(如名称- name和版本- version),以及最关键的包内所有 原始文件名 与其对应 内容哈希值 的映射关系。通过这个映射,- pnpm知道如何从- v10/files/目录中找到构成该包的所有文件,并以 正确的目录结构 将他们 组织 在一起。
- 命名/位置: - 包元信息文件的命名规则为 - [[integrity_hash]]-[[package]]@[[version]].json。- 以 - compressible@2.0.18.json为例,从- pnpm-lock.yaml中获取的- compressible包的完整性哈希为:bash- sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==- 通过 - base64ToHexNode函数将该完整性哈希转换为十六进制字符串。ts- const hex = base64ToHexNode( 'sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=='.slice( 7 ) ); console.assert( hex === '005debecfe5d5b12fc331c884d132539140d68e036224005693af893b054ba68cfb51a460d36699743dbd5708ee89783081769d76e8282cf6c331a928e063246' );- 同理使用前两个字符 - 00作为子目录名,但通过观察已存储的其他包元信息的文件名称:bash- 4809a191f68281dd19697243570e3258268c62c2aa4f3c20571b0289715aa9-@pnpm+patch-package@0.0.1.json aa5a6251e7f2de1255b3870b2f9be7e28a82f478bebb03f2f6efadb890269b-base64-js@1.5.1.json 5d3279e22b928e9df782a9528d4cfb45f5b01d788a4a1aaa5206b931f57d6e-meow@11.0.0.json- 可知元信息文件名是截取 - hash值的- 62个字符。因此- compressible@2.0.18.json最后的文件路径为:bash- v10/index/00/5debecfe5d5b12fc331c884d132539140d68e036224005693af893b054ba68-compressible@2.0.18.json
CAS 的链接策略 
上述已经介绍了 CAS 的存储布局,接下来就要介绍 pnpm 是如何将 CAS 中的文件链接到项目中。
虚拟存储 (node_modules/.pnpm) 
pnpm 并不会直接将全局 CAS 中的文件链接到项目根目录下的 node_modules 文件夹。他采用了一种 中间层 结构,即在每个项目的 node_modules 目录下创建一个名为 .pnpm 的隐藏目录,这个目录被称为 虚拟存储。
这个 .pnpm 目录才是实际包含指向全局 CAS 中文件链接(hardlink 或 reflink)的地方。这些链接按照包名和版本的结构进行组织,例如 .pnpm/<pkg-name>@<version>/node_modules/<pkg-name>/。
包导入方法 (packageImportMethod) 
pnpm 如何将文件从全局 CAS 导入到项目的虚拟存储 (.pnpm) 中,是由 packageImportMethod 配置项控制的。以下是各选项的详细说明:
- auto(默认值): 这是- pnpm的首选策略。它会优先尝试使用- clone(即 引用链接 / 写时复制)。如果文件系统不支持- clone,则尝试使用- hardlink(硬链接)。如果硬链接也失败(例如,尝试跨文件系统链接),最后会回退到- copy(普通文件复制)。默认设置旨在智能地选择当前环境下最优的可行选项。
- clone(引用链接/写时复制): 这是 最快 且 最安全 的方法。他创建一个指向原始文件数据的引用。如果之后修改了项目- node_modules中的这个文件,文件系统会自动创建一个新的副本,而不会影响- CAS中的原始文件。这种方式既节省空间(初始时不复制数据),又保证了 隔离性。但它需要底层文件系统的支持(例如- Btrfs,- APFS, 以及支持- reflink的- XFS)。在速度、空间和安全性(隔离性)之间提供了最佳平衡,但依赖于现代文件系统。
- hardlink(硬链接): 创建一个硬链接。这意味着项目- node_modules中的文件条目和- CAS中的文件条目指向磁盘上完全相同的物理数据块。这种方式在 空间效率上 非常高,因为不占用太多的额外空间(记录- inode信息)。但是,它的一个重要后果是,如果在项目- node_modules中直接修改了这个硬链接文件,也会同时修改- CAS中的原始文件,会无意中破坏- CAS,从而影响其他项目。硬链接要求源文件和链接目标必须在同一文件系统上。极度节省空间,但将项目与存储库紧密耦合,存在意外修改存储库文件的风险。
- copy(复制): 执行标准的文件复制操作。这是磁盘空间和安装速度效率最低的方式,但它具有普遍适用性,即使跨文件系统也能工作。是万能的后备方案,但牺牲了- pnpm的主要优势(节省磁盘空间 和 安装速度)。
- clone-or-copy: 优先尝试- clone,如果不支持则回退到- copy。
文件系统的类型(如 Btrfs, XFS, EXT4, APFS)直接决定了 clone 或 hardlink 是否可用。此外,在 docker 构建环境中,由于构建时无法在宿主机文件系统和容器文件系统之间创建 硬链接 或 引用链接,需要采用变通方法,例如使用 BuildKit 的缓存挂载(cache mount)或 pnpm fetch 命令来预先下载依赖。
packageImportMethod 选项比较
| 选项 | 机制 | 磁盘效率 | 速度 | 文件系统依赖 | node_modules可修改性 | 
|---|---|---|---|---|---|
| auto(默认) | 优先 clone, 再 hardlink, 最后 copy | 最优可行 | 最优可行 | 是 | 取决于实际使用的方法 | 
| clone | 引用链接 (COW) | 高 | 最快 | 是 (Btrfs, APFS, etc.) | 安全 (修改时创建副本) | 
| hardlink | 硬链接 | 最高 | 快 | 是 (同一文件系统) | 危险 (直接修改存储库) | 
| copy | 文件复制 | 低 | 慢 | 否 | 安全 (修改的是副本) | 
| clone-or-copy | 优先 clone, 再 copy | 依赖 clone 支持 | 依赖 clone 支持 | 是 (for clone) | 取决于实际使用的方法 | 
根 node_modules 中的符号链接 
链接过程的最后一步是在项目根目录的 node_modules 文件夹中创建符号链接(symlinks)。这些符号链接指向 .pnpm 虚拟存储内相应的包目录。
一个关键的特点是,只有项目的 直接依赖(即在项目 package.json 文件的 dependencies, devDependencies, optionalDependencies 中列出的包)才会被 符号链接 到 node_modules 的根目录。这强制执行了 pnpm 的严格性原则:应用上下文中无法直接 require 或 import 未在其 package.json 中声明的传递依赖项(即依赖的依赖)。当 node.js 解析模块时,它会沿着这些 符号链接 找到 .pnpm 目录中实际的包代码。
结论 
pnpm 的高效性源于其精心设计的 内容寻址存储 以及 软硬链接 机制。他结合了包完整性校验、对包内每个文件进行内容哈希计算、一个全局内容寻址存储库(CAS)、用于记录包结构的索引文件,以及一套复杂的链接策略(优先使用 引用链接 或 硬链接 将包中所有的文件导入到项目内的虚拟存储 .pnpm,再通过 符号链接 将直接依赖项暴露在根 node_modules 中)。这一系列机制共同作用,实现了显著的 磁盘空间节省 和 安装速度提升。
 XiSenao
 XiSenao