Skip to content

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

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,因为它不仅会使我们的构建体积增大,也会因为两个实例给我们的代码埋下隐患

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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