◆

pnpm patch 给 npm 包打补丁修复 bug

2024-05-20 07:140

场景

项目中使用的 npm 包中有 bug,自己发现了这个 bug,并知道在打包好的代码中如何解决。

这种情况以前很难办。给上游提 PR 等合并、等发版,周期长得没法接受;直接改 node_modules 里的文件,下次 install 就被覆盖,而且改动不进版本控制,别人和 CI 拿不到;fork 一份自己发包,等于背上长期维护成本,还要改所有引用处的包名。

pnpm 内置的 patch 命令给了第四种选择:把你对包的改动生成一个 diff 文件存进仓库,之后每次安装依赖,pnpm 会在包解压之后自动把这个 diff 应用上去。改动进了 git,团队和 CI 自动生效,包名和版本号都不用变。上游修好之后升级版本、删掉补丁就行。

需要说清楚的是,改的是已经打包好的产物,不是包的源码。所以要在 dist 里那些压缩或编译过的文件上定位问题,可读性差一些,但胜在改动小、精准。

步骤

  1. 安装包

补丁是针对具体版本的,所以第一步得先正常装上这个包,确定要修的版本号:

pnpm i vite-plugin-checker
  1. 执行 patch 命令,注意要指定要打补丁包的版本

版本号必须写全,不能只给包名。因为补丁是绑定在具体版本上的——包的产物代码在不同版本间会变,diff 的上下文对不上就打不进去,pnpm 需要知道这个补丁属于哪个版本:

pnpm patch vite-plugin-checker@0.6.4

执行完成后会生成这个包的一个临时文件夹,如下图所示

命令的输出里会打印出这个临时目录的绝对路径,下面几步都要用到,先复制下来。这个目录是 pnpm 从 store 里解压出来的一份干净副本,它记住了修改前的状态,之后靠对比这份副本和你改完的内容来生成 diff:

pnpm patch 命令执行后的终端输出,提示已将包解压到系统临时目录并给出该目录的绝对路径
  1. 进入这个临时文件夹
cd /private/var/folders/x5/0v4twn7x05n9jwsg5yzhx5j00000gn/T/29d5af98dc344e8a5db467928f5331b8

如下图所示,这个文件就是这个包的源码,我们找到需要修改的文件,进行修改

下图是临时目录里的文件结构,和 node_modules 里那个包的内容完全一致:package.json、dist 目录、类型声明文件都在。要改的是 dist 下的产物文件,可以用编辑器直接打开这个临时目录操作:

临时目录中的包内容,包含 package.json、dist 目录和类型声明文件,与 node_modules 中的该包结构一致

改动要尽量小。补丁本质是 diff,改得越多、上下文越长,将来包升级后 diff 打不上去的概率就越高。理想情况是只改一两行——定位到出问题的那个判断或者那个变量,最小化修正。顺手格式化整个文件是最忌讳的,那会让 diff 变成整文件替换。

  1. 修改完成后回到项目目录执行 patch-commit,后面跟着的就是之前生成的临时文件夹
pnpm patch-commit /private/var/folders/x5/0v4twn7x05n9jwsg5yzhx5j00000gn/T/29d5af98dc344e8a5db467928f5331b8

注意要回到项目根目录执行,而不是在临时目录里执行。这条命令做三件事:对比临时目录和原始副本生成 diff、把 diff 写进项目的 patches/ 目录、在 package.json 里注册这个补丁。

  1. 执行上面的命令后已经打补丁成功了

可以观察到如下图所示,项目中生成了 patches 目录,同时 package.json 中添加了如下代码

{
  "patchedDependencies": {
    "vite-plugin-checker@0.6.4": "patches/vite-plugin-checker@0.6.4.patch"
  }
}

这段 patchedDependencies 是 pnpm 应用补丁的依据,key 是「包名@版本」,value 是补丁文件的相对路径。注意较新版本的 pnpm 把这个字段挪进了 pnpm 对象里(pnpm.patchedDependencies),旧版是顶层,看自己的版本对应写。

下图是执行完之后项目的变化:根目录多出 patches/ 目录,里面是以「包名@版本.patch」命名的 diff 文件,package.json 里也多了对应的注册项:

项目根目录新增的 patches 目录及其中的 .patch 文件,以及 package.json 中新增的 patchedDependencies 配置

收尾和注意事项

patches/ 目录必须提交到 git。这是整个方案的意义所在——别人 clone 下来 pnpm install,补丁自动生效,不需要任何额外操作。漏提交的话本地好用、CI 挂掉,排查起来很费时间。

打完补丁建议删掉 node_modules 重装一次验证。因为 patch-commit 会顺带把补丁应用到当前的 node_modules,看起来是好的,但没验证过「从零安装时补丁能否正确打上」。重装一遍能提前发现 diff 有问题的情况。

包升级后补丁会失效。patchedDependencies 里绑的是具体版本号,升到 0.6.5 之后这条配置就匹配不上了,pnpm 会警告补丁未被使用,bug 又回来了。所以升级这个包时一定要检查:上游修好了就删补丁,没修好就重新对新版本走一遍 patch 流程。

如果补丁能打上但有冲突,pnpm 会报错中断安装。这时候先看错误里提示的是哪个文件哪一段对不上,通常是包的产物变了。重新生成补丁比手改 .patch 文件靠谱。

最后一点,这套机制对付「产物里的小 bug」很好用,但不适合做功能定制。改动一多就该考虑 fork 或者换方案了——补丁维护起来的心智负担是随改动量指数上升的。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEpnpm patch 给 npm 包打补丁修复 bug加载中