最近 PicGo 2.5.3 发出去没多久,就有人来报了个很怪的问题:应用能装上,但一启动就直接报错,核心信息是 Cannot find module 'esprima'。

最开始我还以为这事大概率是 macOS arm64 打包出了岔子。毕竟最早的反馈来自 mac 用户,而且 2.5.2 明明还是好的,2.5.3 看起来也没怎么动依赖。结果没过多久,我自己在 Windows 上也复现了。到这一步就知道,事情没那么简单了。
先说结论:这次不是某个依赖“没装上”,也不是代码里手滑删了 esprima。真正的问题出在发布链路上:GitHub Actions 里 pnpm/action-setup 用的是浮动的 version: 10,PicGo 2.5.2 和 2.5.3 实际用到的 pnpm 小版本并不一样。偏偏当前 electron-builder 收集生产依赖的方式,又刚好会吃这个差异。
问题是怎么暴露出来的
这次的用户反馈在这个 issue:
报错现象很直接,就是安装后启动失败,提示缺少 esprima。
一开始我有两个很自然的判断:
- 这是不是某个平台特有的问题?
- 这是不是
2.5.3里某个依赖偷偷变了?
结果这两个判断最后都只对了一半。
平台特有这条很快就被排除了,因为 Windows 也中了。至于“依赖偷偷变了”,从 package.json 表面看,确实没有什么特别大的变化,至少不是那种一眼就能看出会把安装包搞炸的改动。
更迷惑的是,我本地把 node_modules 删掉,重新安装依赖,再重新构建,打出来的包居然是正常的。
这就很烦。因为这意味着:
- 代码仓库本身看起来没问题
- 本地 fresh install 看起来也没问题
- 线上发布出来的安装包却有问题
这种情况我以前也遇到过一次,所以我第一反应不是业务代码,而是打包链路或者依赖问题。
为什么这次特别难查
真正让我卡住的,不是报错本身,而是“本地正常,远端不正常”。
我本机最开始用的是 pnpm 10.25.0,而 GitHub Actions workflow 里写的是:1
2
3- uses: pnpm/action-setup@v4
with:
version: 10
这个写法看起来像“固定到了 pnpm 10”,实际上并没有固定到具体小版本。它只保证你拿到的是某个 10.x,至于到底是 10.25.0、10.29.2 还是 10.30.3,要看当时 action 解析到了什么版本。
我当时已经隐约觉得,八成和 pnpm 版本差异有关。只是这个判断很模糊,我只能感觉“版本可能不一样”,但具体不一样在哪里、为什么会影响到最终安装包,我其实说不上来。
后来我索性让 AI 帮我一起排查这条链路:一边对比本地和线上 release 产物,一边翻 GitHub Actions 日志,再顺着 electron-builder 的依赖收集逻辑继续往下查。也是从这时候开始,我不再盯着源码和 lockfile 打转,而是直接去看最终产物。
第一步:先看最终产物
后面我让 AI 做了一件很朴素的事:直接去看打出来的安装包内容。
本地构建出来的 app.asar 里,comment-json、esprima、array-timsort 都在。
但已经发布出去的 2.5.3 安装包里,情况就不一样了:
comment-json在picgo在esprima不在array-timsort也不在
到这里,问题就清楚很多了:不是程序运行时“找错路径”,而是这些依赖在打包阶段就根本没有被带进最终产物。
也就是说,报错只是结果。真正出问题的是依赖收集。
第二步:依赖明明装了,为什么还是没进包
这部分是这次踩坑里最关键的一点。
我一开始也有点想不通:既然 pnpm install 都成功了,node_modules 里也确实能看到 esprima,那为什么 electron-builder 还会漏掉它?
后面 AI 自己定位问题,翻了 electron-builder 的实现才发现,它并不是傻乎乎把磁盘上的 node_modules 整个搬进去。
对于 pnpm 项目,它会先跑一条命令去拿“生产依赖树”:1
pnpm list --prod --json --depth Infinity
然后基于这棵树去决定哪些依赖应该进 app.asar。
前面看 app.asar,只是确认“哪些依赖没被打进去”;真正把原因讲明白,还得继续往下查。后面我让 AI 帮我把 app.asar、pnpm list 输出、Actions 日志和 electron-builder 源码串起来之后,问题才真正坐实。
我把 pnpm 10.29.2 和 pnpm 10.30.3 的输出拿来对比,结果非常扎眼:
在 10.29.2 下,comment-json 节点下面能看到这些依赖:1
2
3array-timsort
core-util-is
esprima
但在 10.30.3 下,同样的命令,同样的仓库,同样的 lockfile,comment-json 返回出来的依赖列表是空的。
注意,这不代表这些包没被安装。
磁盘上它们还在,node_modules/esprima 也能找到。只是 pnpm list --prod --json --depth Infinity 给 electron-builder 的那棵树里,已经不再把它们挂到 comment-json 下面了。electron-builder 又正好是按这棵树来拷贝依赖,于是最后这些包就没进安装包。
第三步:把本地和远端的版本差异对上
这也是这次最容易让人误判的地方。
后面去翻 release run 日志,才发现:
v2.5.2的线上构建实际使用的是pnpm 10.29.2v2.5.3的线上构建实际使用的是pnpm 10.30.3- 而我本地构建使用的是
pnpm 10.25.0
到这里整个链路终于对上了:
- 本地因为不是
10.30.3,所以打包正常 2.5.2线上还没漂到有问题的小版本,所以正常2.5.3线上漂到了10.30.3,刚好触发了这个问题
我一开始真没往 pnpm 小版本漂移上想。毕竟大家平时更警惕的是大版本升级,小版本总让人下意识觉得“应该没事”。结果这次偏偏就是小版本把我绊了一下。
更巧的是,后面我又去翻了一圈,发现 pnpm 官方那边也已经有人提了类似问题:
看到这个 issue 的时候,我心里反而踏实了一点。至少可以确认,这不是我仓库里某个奇怪配置独有的玄学问题,而是确实有人在真实项目里踩到了同一类坑。
最后的修复方式
最后的处理其实不复杂,核心就一条:不要再让 CI 里的 pnpm 用大版本了。
我把 workflow 里的 pnpm 版本从浮动的 10 改成了明确的 10.29.2。之所以选这个版本,一方面,2.5.2 的线上构建已经验证过它是正常的;另一方面,我后来去看 pnpm 那个 issue 时,也看到有人提到 10.29.2 是最后一个还能稳定构建的版本。所以最后把它锁在 10.29.2,算是一个相对稳妥的选择。
本来 AI 给我一开始的建议是让我显式补充 esprima 这个依赖,但我觉得这只是治标不治本。还好最后让 AI 继续深挖,才把真正的根因找出来了。之前其实 PicGo 也有过一次这种依赖问题,但是当时试了好久就是没找到根因,后来换了不知道多少依赖才构建成功。现在想想,如果当时能直接锁定 pnpm 版本,可能就不至于绕那么远了。
顺手我还给 workflow 加了一个 release_tag 手动参数。这样如果以后要从 dev 分支重新出一版某个已有 tag 的产物,就不用为了重新发包再去硬 bump 一个版本号。
这次踩坑给我的几个提醒
1. CI 里的包管理器版本,最好别写成浮动值
如果这个项目的发布链路依赖 pnpm、npm、yarn 或者别的工具输出的依赖树,那我现在的建议很明确:能锁就锁,别只写大版本。
本地正常不代表线上正常,尤其是这种会牵扯到打包器、符号链接、依赖图解析的链路。
2. “依赖已经安装成功”不等于“依赖一定会进最终产物”
这次算是把这个坑踩得很彻底。
以前我更习惯从“磁盘上有没有这个包”去判断问题。但对 Electron 这类桌面应用来说,最终交付给用户的是安装包,不是仓库里的 node_modules。中间还隔着一层打包器的筛选逻辑。
只要这层逻辑吃的是“依赖树描述”而不是“磁盘实际状态”,那就有可能出现这种很别扭的情况:包明明装了,但最后还是没进去。
3. 出问题时,早点去看产物本身
我这次真正开始接近答案,是从直接检查 app.asar 开始的。
很多时候我们会先盯着源码、锁文件、安装日志来回看,结果越看越乱。可一旦把注意力放到“最终产物里到底有什么”,问题范围反而收得很快。
这个办法虽然土,但挺有效。
结语
这次 2.5.3 的问题,说大不大,说小也不小。它不是功能 bug,而是发布事故。更麻烦的是,它看起来还很像“偶发环境问题”,很容易让人绕远路。
不过也因为这次把链路追到底了,后面再看类似问题,心里会更有数一些:先别急着怀疑代码,有时候真是工具链在背后捅刀子。
如果你也在用 pnpm + electron-builder 这一套,尤其是 workflow 里还写着浮动版本,那我建议你现在就去看一眼。别等到包发出去了,用户替你做集成测试。


