Skip to content

peer dependencies - 隐式传递的危害 #38

@z2014

Description

@z2014

我们业务在 CI 中接入了 [statoscope] 插件,帮助我们分析构建产物的内容,通过 statoscope 插件,发现了构建产物中引入了一些重复的npm包

我们将内容进行简化归类得到下面 3 种重复包的情况

1. package-a 有两个不同的版本

2. package-a 有两个相同的版本,但是链接符后面的包版本不一致

node_modules/.pnpm/@pkg-a@1.93.0_@pkg-b@0.2.2_@pkg-c@1.12.0_@ten_el4uhwub3qermsb2ug24hbwt4q/node_modules/pkg-a

node_modules/.pnpm/pkg-a@1.93.0_@pkg-b@0.3.0_@pkg-c@1.12.0__@ten_l3pmpxjfhmbdedp66axwibu23u/node_modules/@pkg-a

3. package-a 有两个相同的版本,连接符后面的包版本也是相同的

node_modules/.pnpm/@pkg-a@4.0.63_@pkg-b@3.13.0_@tenc_wzqvgos5wvtse35fehnxnhj2da/node_modules/@pkg-a
node_modules/.pnpm/@pkg-a@4.0.63_@pkg-b@3.13.0_@tenc_7zuxjpgexhn6u5ehnylczjhybe/node_modules/@pkg-a

问题分析

1. 两个不同版本的包
package-a 有两个不同的版本

这种情况大部分原因是因为依赖包的版本写死了,导致package-a的版本无法满足更高版本的version,导致安装了两个不同版本,这种问题比较常见于 dependencies 中,如果不是指定了某一个确认版本,那么建议在安装依赖时使用 ^ 来表达,当依赖的是1.0.0 版本以下的包时,^也是不会升级高版本的

2. 有两个相同的版本,但是连接符后面的包版本不一致
.pnpm目录下的目录生成规则是 package-a@版本_(package-a-peer)@版本

所以后面的连接符代表的是当前包的 peer 依赖版本,因为 peer 的版本不一致,所以 pnpm 认为当前 package-a 也应该是两个包,聊到这里,就需要讲到 peer transitive 的概念了,具体可以看下面这篇文档

how-peers-are-resolved

pkg-a
  dep:
    - pkg-b
    - lodash@1.0
pkg-b:
  dep: pkg-c@1.0
pkg-c:
  peer: lodash

由于 peer transitive,所以 pkg-c 最终安装的版本是

pkg-c@1.0_lodash@1.0

所以当我们项目下有两个相同版本不同peer 版本的实例时,就说明

pkg-a 中声明的peer包版本有无法兼容的包版本实例,因此因为 peer 版本的不同,所以导致 pkg-a也有了多个版本

3. 有两个相同的版本,连接符后面的版本也一致
这个也是 peer 引发的问题中最隐蔽的

pnpm ls pkg --depth Infinity
我们通过上述命令查看当前包的依赖关系,在 .pnpm 目录下找到两个版本包对应的引用,真相终于浮出水面
image

因为依赖关系比较绕,我们直接用真实环境包名的缩写来描述了

当我们项目下安装了 package-a和utils两个包时,我们最终安装了两个相同版本的 utils实例,我们来看看这里的依赖关系

package-a
  dep:
    - landuage-pack
    - utils
    - i18n@20

language-pack
  dep: i18n@21
  peer: i18n

utils
  dep: language-pack

首先 language-pack 包中声明了 i18n为 peer dep,

所以package-a 依赖的 language-pack是 language-pack+i18n@21 的版本

而 package-a中的 utils 包也依赖了 language-pack, 但是由于 peer transitive,

所以 package-a中的 utils 包依赖的language-pack是 language-pack+i18n@20 的版本

到这里为止由于 peer的依赖版本不同,所以导致了 language-pack包是两份实例,

那么最关键的来了,我们根目录下也依赖了 utils 包,而这个utils包依赖的 language-pack是 language-pack+i18n@21

而package-a中的utils包依赖的是language-pack+i18n@20 的版本

所以,即使utils包的版本是同一个,但是由于他们的 dep language-pack的不同,所以也导致了 utils 的不同

这就是这个问题的原因

总结

看到这里,上面的多实例问题也全部都找到了原因,很多包希望将另一个包 b 作为单例的方式引入,但同时又依赖它,所以就想通过声明为了 peer,使用 peer 来保证 npm 包的单例,但这本身就是 hack 的一种方式,因为你必须要保证所有依赖了你这个包的任意包,都将包 b声明为 peer,这样你才能使用和真正的宿主相同的版本,否则就会被提前拦截,导致一些非预期内的行为,不仅希望的package-b不是单例,有可能包本身也会被打包成两份

那究竟什么时候才能使用 peer 呢🤔?它是用来解决什么问题的,我们看看开源框架是怎么应用的
image
image

可以看到,比如 webpack-dev-middleware 是 webpack 的中间件,所以必须在 webpack 中使用

当把一个依赖声明为 peer 时,它代表着表示当前这个包的运行环境,而并非依赖,依赖应该声明在 dep 中

综上,当我们考虑发布一个npm包时,需要更加谨慎的使用 peer,因为它不仅会使我们的构建体积增大,也会因为两个实例给我们的代码埋下隐患

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions