[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.yaml
file 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_modules
folder 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/
目录中只保留了最外层的依赖。