随便创建一个空的项目。

mkdir temp1 && cd temp1 && pnpm init
mkdir temp1 && cd temp1 && pnpm init

添加 is-odd 这个包 。

 pnpm add is-odd
 pnpm add is-odd

你就会得到下面的这样一个目录结构。

.
└── node_modules
    ├── .pnpm
    │   ├── is-odd@3.0.1
    │   │   └── node_modules
    │   │       ├── is-odd
    │   │       └── is-number
    │   └── is-number@6.0.0
    │       └── node_modules
    │           └── is-number
    └── is-odd
.
└── node_modules
    ├── .pnpm
    │   ├── is-odd@3.0.1
    │   │   └── node_modules
    │   │       ├── is-odd
    │   │       └── is-number
    │   └── is-number@6.0.0
    │       └── node_modules
    │           └── is-number
    └── is-odd

可以看到, is-odd 依赖了 is-number 这个包。

pnpm 在安装这两个包时,会把这两个包下载到 ~/.pnpm-store 这个目录下,然后在 node_modules/.pnpm 下创建文件的硬链接到 ~/.pnpm-store,如此这般,当多个不同的项目用到同一个包的时候,无需重新下载,只需创建硬链接就行了。

当你在项目中使用 is-odd 这个包时,根据 Node 的模块解析机制,会解析到 node_modules/is-odd ,但在这里,node_modules/is-odd 是指向 node_modules/.pnpm/is-odd@3.0.1/node_modules/is-odd 的软链接,所以最后实际读取的内容是在 node_modules/.pnpm/is-odd@3.0.1/node_modules/is-odd

然后, is-odd 中去查找 is-number 时,又会解析到 node_modules/.pnpm/is-odd@3.0.1/node_modules/is-number,而这又是一个指向到 node_modules/.pnpm/is-number@6.0.0/node_modules/is-number 的软链接。

你可能会奇怪,为什么 node_modules/.pnpm/is-odd@3.0.1 下面的结构不是这样

is-odd@3.0.1
    ├── index.ks
    ├── package.json
    └── node_modules
is-odd@3.0.1
    ├── index.ks
    ├── package.json
    └── node_modules

根据 Flat node_modules is not the only way 这篇文章介绍是为了避免环形软链。

然后我们再做个实验,根据前面的步骤重新创建一个相同的 temp2 的项目,然后在 VSCode 直接把 temp1 下的 is-odd 包的文件内容给改掉。

module.exports = function isOdd(value) {
  console.log('hello')
}
module.exports = function isOdd(value) {
  console.log('hello')
}

然后在 temp2 当中使用这个包,会发现会打出这行 log。这也很好理解,因为不管是 temp1 还是 temp2 下的 is-odd 的文件都是指向 ~/.pnpm 目录下某个文件的硬链接,因此实际上就是同一个文件。

我们试着在 temp2 目录执行下 pnpm store status,会输出:

➜ pnpm store status
ERR_PNPM_MODIFIED_DEPENDENCY  Packages in the store have been mutated

These packages are modified:
bnpm.byted.org/is-odd/3.0.1

You can run pnpm install to refetch the modified packages
➜ pnpm store status
ERR_PNPM_MODIFIED_DEPENDENCY  Packages in the store have been mutated

These packages are modified:
bnpm.byted.org/is-odd/3.0.1

You can run pnpm install to refetch the modified packages

这里按照说明直接 pnpm install 没用,需要加上 --force 参数,或者把 node_modules 删掉再重新 pnpm i。重新安装后会发现,temp2 下的 is-odd 文件内容恢复了,而 temp1 没有。

pnpm 可以增量将文件添加到存储库中。官方说法是

如果你用到了某依赖项的不同版本,那么只会将有差异的文件添加到仓库。 例如,如果它有100个文件,而新版本只改变了其中1个文件。那么 pnpm update 只会向存储中心添加1个新文件,不会仅仅因为单一的改变而克隆整个依赖。

因此猜测这里的行为可能是通过某种 hash 算法算出本地版本的远端版本不一致,然后将不一致的那个文件下载下来然后创建硬链接。

后来带着疑问和好奇去搜了下 issue,搜到了 issue#601,作者在这以后引入了 pnpm verify 命令,通过计算包的 sha1 来验证包是否被篡改,见这个 pr。后来,pnpm verify改名成现在的 pnpm store status ,再后来,将校验逻辑重构成更高效的 ssri

References