返回博客

npm install -g 报成功却找不到包目录——postinstall 原子回滚坑

装一个带浏览器下载逻辑的全局 CLI 工具,终端最后一行显示 added 1 package,没有任何报错。which 也能找到,一切看起来正常。但一运行就报 Cannot find moduleExecutable doesn't exist,进目录一看——lib/node_modules/<pkg> 根本不存在。

反复 npm install -g 毫无帮助,每次都重现完全相同的结果。

现象

$ npm install -g some-cli   # 提示 added 1 package,成功退出
$ some-cli --version        # 打印版本,看起来 OK
$ some-cli do-something
Error: Executable doesn't exist at
  ~/Library/Caches/ms-playwright/chromium_headless_shell-1217/...
 
$ ls ~/.nvm/.../lib/node_modules/some-cli
No such file or directory   # ← 包目录根本不在!
 
$ ls -la $(which some-cli)
lrwxr-xr-x ... -> ../lib/node_modules/some-cli/index.js   # 悬空软链

which 返回的是一个软链,指向一个已经不存在的目录。

根因

npm 全局安装有一个原子性保证:如果 postinstall(或 install 生命周期)脚本以非零退出,npm 会整体回滚 lib/node_modules/<pkg> 目录,就像这次安装从未发生一样。

但是 npm 只删了包目录,不会删 bin/ 下已经写好的软链。于是留下一个指向虚空的悬空软链(dangling symlink),让 which 骗过了你。

触发场景:很多 CLI 工具(Playwright、Puppeteer、Cypress 以及依赖它们的工具)会在 postinstall 里下载 Chromium 等浏览器二进制。下载过程中会写一个 __dirlock 锁文件。若上次安装因网络中断或被强制停止,锁文件残留,下次安装的 postinstall 一读到锁就抛异常 → npm 回滚 → 死循环。

坑:npm 回滚不删 bin 软链,让你误以为安装成功

which <pkg> 成功返回路径,但路径是悬空软链。npm install -g 最后一行 added N packages 是在回滚前写的日志,并不代表包最终落盘。真正的冒烟测试是 ls $(readlink -f $(which <pkg>)),文件存在才算装上。

诊断命令:

readlink $(which some-cli)      # 看软链指向
ls /path/to/lib/node_modules/some-cli   # 包目录存在?
npm install -g some-cli --verbose 2>&1 | grep "npm error"  # 看有无 postinstall 错误

正解

分两步走:把「包目录落盘」和「postinstall 副作用」解耦。

# 第一步:清理残留锁文件
rm -rf ~/Library/Caches/ms-playwright/__dirlock 2>/dev/null
# Puppeteer 在 ~/.cache/puppeteer,Cypress 在 ~/.cache/Cypress
 
# 第二步:跳过 scripts 安装,包目录正常落盘
npm install -g some-cli --ignore-scripts
 
# 第三步:确认包目录存在
ls ~/.nvm/versions/node/v20.14.0/lib/node_modules/some-cli
 
# 第四步:手动执行 postinstall 等价命令(完全可见)
cd ~/.nvm/versions/node/v20.14.0/lib/node_modules/some-cli
node node_modules/playwright-core/cli.js install chromium-headless-shell

查看该包的 postinstall 内容:

cat node_modules/some-cli/package.json | grep -A3 '"scripts"'

版本锁定陷阱

许多 CLI 会把 playwright-core固定版本打包进自己的 node_modules(例如 playwright-core@1.59.1 对应浏览器 revision 1217)。如果你用系统全局的 npx playwright install 去装浏览器,安装的是系统版本的浏览器(可能是 revision 1208),该 CLI 启动时会因版本不匹配拒绝使用。

始终用包内部的 playwright-core cli 安装浏览器:

cd /path/to/lib/node_modules/<pkg>
node node_modules/playwright-core/cli.js install chromium-headless-shell
# 不要用:npx playwright install chromium  ← 版本对不上
正解:--ignore-scripts 让包先落盘,再手动跑 postinstall

npm install -g <pkg> --ignore-scripts 绕开自动回滚,包目录稳定落盘。然后 cd 进去手动执行等价的下载命令,享有完整的错误输出,方便排查网络/锁文件/平台问题。yarn global addpnpm add -g 同样支持 --ignore-scripts 标志。

一句话外卖

npm install -g 成功但包消失 = postinstall 失败触发原子回滚,bin 软链成了幽灵;用 --ignore-scripts 先落盘,再手动跑副作用。