[FE] pnpm 依赖管理浅析
背景
pnpm 默认会把所有 package 的依赖放到最外层的 node_modules 中,然后建立软链接指向它们。
项目示例
github: thzt/test-pnpm-monorepo 是一个极简版的 monorepo 项目,包含如下文件,
./monorepo
├── package.json
├── packages
| ├── app
| | ├── index.js
| | └── package.json
| └── lib
| ├── index.js
| └── package.json
└── pnpm-workspace.yaml
最外层有,
- package.json:最外层的依赖,或者可以说是所有 package 的公共依赖
- pnpm-workspace.yaml:配置 monorepo 有哪些 package,各 package 的相对路径是什么
- packages/:这个名字可以改,取决于
pnpm-workspace.yaml中的配置
monorepo 的每一个 package,会被放到独立的文件夹中,我们配置的路径为 packages/**。pnpm-workspace.yaml 文件内容如下,
packages:
- 'packages/**'
本例中只包含两个 package,分别为 packages/app/ 和 packages/lib/。依赖关系如下,
packages/app # packages/app 内部依赖了 packages/lib
packages/lib
debug@4.3.3
ms@2.1.2
packages/lib # packages/lib 依赖了外部的 debug
debug@4.3.3 # debug 有自己的依赖 ms
ms@2.1.2
为了让 packages/app/ 能依赖 packages/lib/ 需要设置各自的 package.json 中的 name 字段包含相同的 @scope(本例中为 @test),如下,
# packages/app/package.json
{
"name": "@test/app",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@test/lib": "workspace:^1.0.0"
}
}
#packages/lib/package.json
{
"name": "@test/lib",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"debug": "^4.3.3"
}
}
依赖分析
(1)node_modules 中的文件结构
当我们在外层执行 pnpm install 的时候,pnpm 会创建这些文件,
./monorepo
├── node_modules # [new] 最外层整个 monorepo 项目的依赖
├── package.json
├── packages
| ├── app
| | ├── index.js
| | ├── node_modules # [new] packages/app 的依赖
| | └── package.json
| └── lib
| ├── index.js
| ├── node_modules # [new] packages/lib 的依赖
| └── package.json
├── pnpm-lock.yaml # [new] 整个项目 以及 package 的依赖信息
└── pnpm-workspace.yaml
我们来看一下各级 node_modules 中都有什么内容,依赖都被 “打平” 放到了 .pnpm/ 目录,
node_modules/
.bin/
tsc
tsserver
.pnpm/
node_modules/ # 这里是被 hoist 了,见下文解释
debug/ -> [symlink] ../debug@4.3.3/node_modules/debug
ms/ -> [symlink] ../ms@2.1.2/node_modules/ms
debug@4.3.3/
node_modules/
debug/
ms/ -> [symlink] ../../ms@2.1.2/node_modules/ms
ms@2.1.2/
node_modules/
ms/
typescript@4.5.4/
node_modules/
typescript/
lock.yaml
typescript/ -> [symlink] .pnpm/typescript@4.5.4/node_modules/typescript
.modules.yaml
packages/
app/
node_modules/
@test/
lib/ -> [symlink] ../../../lib
lib/
node_modules/
debug/ -> [symlink] ../../../node_modules/.pnpm/debug@4.3.3/node_modules/debug
值得注意的是:pnpm 默认 hoist 配置为 true(官方文档),
当
true,所有依赖项都被提升到node_modules/.``pnpm。 这使得node_modules所有包都可以访问 未列出的依赖项。
任何一个包,在自己的执行路径上找不到依赖时,最终都会向上到 .pnpm/node_modules 中查找。
我们可以通过添加 .npmrc 配置,来取消这一默认选项,
hoist=false
再执行一次 pnpm install 之后,.pnpm/node_modules 这个目录就不存在了。
(2)依赖链路
然后我们观察一下依赖链路,发现每一级的依赖,都通过 symlink(软链接)梳理好了,
packages/app
@test/lib -> packages/app/node_modules/@test/lib -> [symlink] packages/lib
debug -> packages/lib/node_modules/debug -> [symlink] node_modules/.pnpm/debug@4.3.3/node_modules/debug
ms -> node_modules/.pnpm/debug@4.3.3/node_modules/debug/node_modules/ms -> [上级目录] node_modules/.pnpm/debug@4.3.3/node_modules/ms -> [symlink] node_modules/.pnpm/ms@2.1.2/node_modules/ms
因此,虽然所有依赖都 “打平” 放到了最外层 node_modules 中,但是仍然保证了依赖查找的正确性。
我们可以再重点看一下 ms@2.1.2 的查找过程,
# debug@4.3.3 依赖了 ms
# debug@4.3.3 所在的目录为 node_modules/.pnpm/debug@4.3.3/node_modules/debug
# 所以,优先会从当前所在目录的 ./node_modules 中去找 ms
# 即 node_modules/.pnpm/debug@4.3.3/node_modules/debug/node_modules/ms
# 可是这个目录并不存在没有找到 ms,因此按照 Node.js resolve module 规则,会到上层目录找
# 即 node_modules/.pnpm/debug@4.3.3/node_modules/ms
# 这里恰好有 pnpm 创建的一个 symlink,指向了 node_modules/.pnpm/ms@2.1.2/node_modules/ms
# 因此,最后会找到最外层 node_modules/.pnpm 下面去
这里的关键在于 pnpm 会在 debug 实际执行路径的上级目录,放一个 ms 的软链接。
node_modules/
.pnpm/
debug@4.3.3/
node_modules/
debug/ # debug 执行路径
ms/ -> [symlink] ../../ms@2.1.2/node_modules/ms
ms@2.1.2/
node_modules/
ms/ # debug 依赖的 ms 指向了这里
注意,向上级目录查找时,当前路径为 debug 的实际执行路径,
# packages/lib 引用 debug 的路径
packages/lib/node_modules/debug -> [symlink]node_modules/.pnpm/debug@4.3.3/node_modules/debug
# 实际路径
node_modules/.pnpm/debug@4.3.3/node_modules/debug
# 查找 ms 时
[Right] 从这里往上找 node_modules/.pnpm/debug@4.3.3/node_modules/debug
[Wrong] 从这里往上找 packages/lib/node_modules/debug
所以在手动排查问题时,经常容易出错,要时刻注意当前目录是否在某个软链接下。
分离 lock 文件
我们可以将每个 package 的依赖安装到自己独立的 node_modules 中,用单独的 pnpm-lock.yaml 进行管理。只需要在项目根目录添加 .npmrc 文件,配置内容如下,
shared-workspace-lockfile=false
可在官方文档中找到 shared-workspace-lockfile 的说明,
If this is enabled, pnpm creates a single
pnpm-lock.yamlfile in the root of the workspace. This also means that all dependencies of workspace packages will be in a singlenode_modules(and get symlinked to their packagenode_modulesfolder for Node's module resolution).
这时在执行 pnpm install,会新增这些文件,
./monorepo
├── node_modules # [new] 最外层整个 monorepo 项目的依赖
├── package.json
├── packages
| ├── app
| | ├── index.js
| | ├── node_modules # [new] packages/app 的依赖
| | ├── package.json
| | └── pnpm-lock.yaml # [new] packages/app 的依赖信息 <- [关键]
| └── lib
| ├── index.js
| ├── node_modules # [new] packages/lib 的依赖
| ├── package.json
| └── pnpm-lock.yaml # [new] packages/lib 的依赖信息 <- [关键]
├── pnpm-lock.yaml
└── pnpm-workspace.yaml # [new] 外层项目的依赖信 <- [关键]
我们再来看一下各个 node_modules 中的内容,
node_modules/
.bin/
tsc
tsserver
.pnpm/
typescript@4.5.4/
node_modules/
typescript/
lock.yaml
typescript/ -> [symlink] .pnpm/typescript@4.5.4/node_modules/typescript
.modules.yaml
packages/
app/
node_modules/
.pnpm/ -> [new]
lock.yaml
@test/
lib/ -> [symlink] ../../../lib
.module.yaml -> [new]
lib/
node_modules/
.pnpm/ -> [new]
node_modules/
ms -> [symlink] ../ms@2.1.2/node_modules/ms
debug@4.3.3/
node_modules/
debug/
ms/ -> [symlink] ../../ms@2.1.2/node_modules/ms
ms@2.1.2/
node_modules/
ms/
lock.yaml -> [new]
debug/ -> [symlink] .pnpm/debug@4.3.3/node_modules/debug [路径变了]
.module.yaml
我们发现,每个 node_modules 中都有了一个 .pnpm/ 目录。全局 .pnpm/ 目录中只保留了最外层的依赖。