每份 lockfile 都会被重建成依赖图。npm 的每个 entry 以 node_modules 路径记录它装在哪里,依赖的解析方式就是 Node 自己的规则——先找自己底下的 node_modules,找不到就往上一层找,直到 root。pnpm 的图本来就是平的:每个安装实体以名称加版本当键,依赖直接指向其他键。接着两张图依包名比对,每次跳版分成 major/minor/patch/prerelease,并对每个变更从 root 做广度优先搜索,找出通往该包的最短依赖链。链上第一个在两版之间版本不同的节点,就是把这个变更带进来的那一个。
major 跳级表示升级改到了第一段版号——那是惯例上唯一允许破坏性变更的级别,即使是 transitive 也值得看一眼。新进 transitive 表示这个包没有任何人在 package.json 声明它,是跟着别的依赖进来的。N 个版本并存表示 resolver 没办法用一份副本满足所有 range,同一个包被装了不止一次——多半无害,偶尔会造成状态重复或 instanceof 对不上。来源改变表示 tarball 换了 registry 主机,合并前值得确认一下。
lockfile 常常带着私有 registry 地址与内部包名。解析、比对与溯源全部在你的浏览器里执行——不上传、不存储、不进 analytics。粘贴时打开 network 面板就能验证。
关于这个主题的常见疑问与实用解答。
左边粘贴旧的、右边粘贴新的。在 PR 里,分别打开 base 分支与 head 分支的那份文件(或用 git show main:package-lock.json 与 git show HEAD:package-lock.json)。本机 install 改了 lockfile 之后,git show HEAD:package-lock.json 就是变更前、工作目录里的是变更后。两边必须是同一种 lockfile——npm 对 npm、pnpm 对 pnpm。
直接依赖是你在 package.json 里声明的;transitive 依赖是 lockfile 里其他所有东西——它们在那里,是因为某个直接依赖(或它的依赖)需要。lockfile 大部分都是 transitive,所以 package.json 改一行就能产生几百行变动。表格里的 direct 标签标的是你自己声明的包;没有标签的都是经由某条链进来的,溯源链会告诉你是哪一条。
npm 的 package-lock.json(lockfileVersion 1、2、3)与 pnpm-lock.yaml(lockfileVersion 5、6、9)。npm-shrinkwrap.json 与 package-lock.json 同格式,也能用。yarn.lock(不论是旧版 v1 文本格式或 Berry 的 YAML 格式)会被识别出来但尚不支持,工具会明说而不是乱猜。另外 npm lockfileVersion 1 没有 root 依赖清单,这类文件的 direct 标签是从顶层位置推估、不是读 package.json。
git diff 看的是行不是包:升级一个包会动到它的 version、resolved、integrity 三行,加上每个引用它的 entry,同一个变更散落在整份文件各处。GitHub 的依赖检视会列出新增、移除、改版的包,但只在 GitHub 上、而且只是一份平面清单——它不会告诉你 minimatch 跳 major 是因为 rimraf 升级了。这个工具吃任何你粘贴得出来的两份 lockfile,依包分组变更、分级跳版,并显示解释它的那条链。
对每个变更的包,工具从 root 往外搜索变更后的 lockfile(移除的情况则搜索变更前),保留能抵达该包的最短链。接着沿最短链从 root 往下走,停在第一个在另一版里版本不同的节点——那个节点就是报告的原因,例如「由 rimraf 从 3.0.2 改为 6.0.1 带动」。如果包本身是直接声明的,原因就是 package.json。如果链上完全没有节点改版,多半是 resolver 重新解析或去重造成的位置变动,工具会照实说、不会硬编一个原因。
本身不是。当两个依赖要求的 range 没有任何单一版本能同时满足时,包管理器会两个都装、把其中一个嵌套放进去;lockfile 会分别记录每份副本,这个工具把它列成「N 个版本并存」。这是正常的、多半无害。当这个包持有共享状态、导出会被 instanceof 检查的 class,或重复的那份大到影响 bundle 体积时,才值得处理。看溯源链可以找出是哪个依赖坚持要旧的 range。