Front End

[FE] pnpm 依赖管理浅析

2022-01-20  本文已影响0人  何幻

背景

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

最外层有,

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

上一篇 下一篇

猜你喜欢

热点阅读