pnpm 原理
Oct 06, 2021
随便创建一个空的项目。
mkdir temp1 && cd temp1 && pnpm init
添加 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
可以看到, 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
根据 Flat node_modules is not the only way 这篇文章介绍是为了避免环形软链。
然后我们再做个实验,根据前面的步骤重新创建一个相同的 temp2 的项目,然后在 VSCode 直接把 temp1 下的 is-odd 包的文件内容给改掉。
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 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。