[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 single node_modules (and get symlinked to their package node_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/ 目录中只保留了最外层的依赖。


参考

github: thzt/test-pnpm-monorepo
pnpm v6.25.1

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容