pnpm安装Electron的坑,你遇到了吗?

MacOS系统里,使用pnpm安装Electron后,正常使用没有问题。但是,如果删除了node_modules,重新安装后,启动应用时会报错:

bash 复制代码
jw my-electron-app % pnpm start

> my-electron-app@1.0.0 start /Users/jw/wk/github/electron/my-electron-app
> electron .

dyld[38219]: Library not loaded: @rpath/Electron Framework.framework/Electron Framework
  Referenced from: <4C4C4421-5555-3144-A154-D5199CCDD1BE> /Users/jw/wk/github/electron/my-electron-app/node_modules/.pnpm/electron@27.0.4/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron
  Reason: tried: '/Users/jw/wk/github/electron/my-electron-app/node_modules/.pnpm/electron@27.0.4/node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Electron Framework' (no such file), '/Users/jw/wk/github/electron/my-electron-app/node_modules/.pnpm/electron@27.0.4/node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Electron Framework' (no such file), '/Library/Frameworks/Electron Framework.framework/Electron Framework' (no such file), '/System/Library/Frameworks/Electron Framework.framework/Electron Framework' (no such file, not in dyld cache)
/Users/jw/wk/github/electron/my-electron-app/node_modules/.pnpm/electron@27.0.4/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron exited with signal SIGABRT
 ELIFECYCLE  Command failed with exit code 1.

排查

我们进入到报错的文件的上级目录,看到确实是没有这个文件Electron Framework,只有一个文件夹Versions

bash 复制代码
jw my-electron-app % cd "/Users/jw/wk/github/electron/my-electron-app/node_modules/.pnpm/electron@27.0.4/node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/"
jw Electron Framework.framework % ls
Versions

从错误提示上看,应该是Electron Framework这个文件在安装时没有复制过来导致的,使用npmyarn安装并不会出现这个问题,锅显然是pnpm的。

我们知道,pnpm有别于npmyarn的核心卖点是,使用硬链接和符号链接的方式来管理node_modules目录,这种方式可以避免重复下载和存储相同的包,从而节省磁盘空间和提高安装速度。这个错误信息必然与这种链接方式有关。

我们在正常的版本中找到这个目录node_modules/.pnpm/electron@27.0.4/node_modules/electron/dist/Electron.app,看下里面有什么内容:

bash 复制代码
├── Electron Framework
├── Helpers
│   └── chrome_crashpad_handler
├── Libraries
│   ├── libEGL.dylib
│   ├── libGLESv2.dylib
│   ├── libffmpeg.dylib
│   ├── libvk_swiftshader.dylib
│   └── vk_swiftshader_icd.json
├── Resources
└── Versions

177 directories, 207 files

MacOS上看,Electron Framework是个可执行文件:

遇事不决问谷歌,很快找到了这条有关联的信息,是pnpm的GitHub的issue

这个问题在2023年10月就已经被提出了,顺便还给了一个临时解决方案:

也就是在preparehook里,先删除这个目录,再重新执行一次Electroninstall脚本:

json 复制代码
"scripts": {
	"prepare": "rimraf node_modules/electron/dist && node node_modules/electron/install.js"
}

由于只有Mac有这个问题,所以我写了一个简单的脚本install.cjs

javascript 复制代码
const os = require("os");
const fs = require("fs");

function main() {
  if (os.platform() !== "darwin") {
    return;
  }
  console.info("rm -rf node_modules/electron/dist");
  fs.rmSync("node_modules/electron/dist", { force: true, recursive: true });
  console.info("node node_modules/electron/install.js");
  require("electron/install.js");
}

main();

将上述script修改为:

json 复制代码
{
  "prepare": "node build/install.cjs"
}

以上是我在2024年1月发现这个问题的处理。

进展

时间进展到4月,我发现这个issue被关闭了:

这位大佬Zoltan Kochan是谁呢?到pnpm的贡献榜里一看:

排在第一位,哦,是pnpm之父,失敬:

另一个方案

言归正传。

大佬在2月24日提出:『我认为这个问题的临时解决方法是将side-effects-cache设置为 false』。

我试了下,修改.npmrc

bash 复制代码
side-effects-cache = false

果然生效。

这个配置是什么呢?它是pnpm的特有的配置项,在官方文档里是这样写的:

也就是说,side-effects-cache用于配置是否缓存包的安装副作用。

当你使用pnpm安装一个包时,pnpm会在node_modules目录下创建一个符号链接,指向一个全局的包存储位置。这样,如果多个项目使用同一个包,那么这个包只需要在全局存储位置存储一次,而不是在每个项目的node_modules目录下都存储一份。

然而,有些包在安装过程中会有副作用,比如编译本地代码或者生成某些文件。这些副作用通常是项目特定的,不能在多个项目之间共享。

这就是side-effects-cache配置项的作用。如果设置为truepnpm会在每个项目的node_modules目录下缓存这些副作用。这样,即使这个包在全局存储位置已经存在,pnpm也会重新执行这个包的安装过程,以生成这些副作用。

如果设置为falsepnpm则不会缓存这些副作用。这意味着,如果一个包在全局存储位置已经存在,pnpm就不会重新执行这个包的安装过程,即使这个过程可能会生成一些项目特定的副作用。

Bug修复解析

紧接着,这个Bug就被大佬修复了:

翻译过来是:

符号链接被解析为其真实路径并上传为真实文件。它们不会占用更多空间,因为硬链接最终指向同一文件。这可能令人惊讶,因为在首次安装时,用户会看到符号链接,但在后续安装中,软件包将使用"真实文件",因为它们将被链接到存储库(来自副作用缓存)。我认为理想情况下,在从副作用缓存链接软件包时,我们应该恢复符号链接,但这将需要对软件包索引文件进行破坏性更改,因为目前仅支持"真实文件"条目。

已经合并到main分支和v9 beta版本上了:

再看发版日志里,在v8.15.4里果然有了这条信息:

我们使用corepack切换pnpmv8.15.4

bash 复制代码
jw my-electron-app % corepack use pnpm@8.15.4
Installing pnpm@8.15.4 in the project...

Lockfile is up to date, resolution step is skipped
Packages: +78
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 78, reused 78, downloaded 0, added 78, done

devDependencies:
+ electron 27.0.4

Done in 540ms

这时发现pnpm start果然正常了。

下来,我们简单看下这个Bug的修复逻辑

大佬只是将代码顺序调整了一下,额外添加了一句stat.isDirectory的处理。

你可能会产生疑惑,什么情况下,在33fs.readdir判断过不是文件夹,到了47行又使用fs.stat判断是个文件夹的呢?

答案是pnpm的核心------符号链接。

我们做个测试,方便理解。随便找一个工程,使用ln -s创建一个文件夹的符号链接:

bash 复制代码
mkdir test
cd test
ln -s  ../node_modules aa

在IDE里能看到创建成功了:

再写段JS代码:

javascript 复制代码
const fs = require("fs");
const path = require("path");

const dir = "./test";
fs.readdirSync(dir, {
  withFileTypes: true,
}).forEach((file) => {
  console.log(
    "file:",
    file.name,
    "isDir:",
    file.isDirectory(),
    "isSymLink:",
    file.isSymbolicLink()
  );
  const fullPath = path.join(dir, file.name);
  let stat;
  try {
    stat = fs.statSync(fullPath);
    console.log("isDir:", stat.isDirectory());
  } catch (err) {
    if (err.code !== "ENOENT") {
      throw err;
    }
  }
});

// file: aa isDir: false isSymLink: true
// isDir: true

从打印结果就能看出,在fs.readdir里,如果返回的这个Dirent对象表示的是一个指向目录的符号链接,isDirectory方法会返回false,因为符号链接本身并不是目录。而fs.stat会"解引用"符号链接,也就是说,它会返回符号链接指向的目录或文件的状态,而不是符号链接本身的状态。

大佬的这次修改,考虑了文件夹为符号链接的情况,又考虑到file为符号链接(file.isSymbolicLink,原来判断是file.isFile)的情况,完美解决了这个Bug。

总结

MacOS中使用pnpm二次安装Electron后,可能会出现node_modules里缺失了二进制文件的情况,这是pnpm符号链接引发的一个Bug,不确定是具体哪个版本出的问题(8.0.0是好的),所以请大家升级到最新版本(v8.15.4以上),或者v9 beta版本。

相关推荐
诗书画唱3 分钟前
【前端面试题】JavaScript 核心知识点解析(第二十二题到第六十一题)
开发语言·前端·javascript
excel10 分钟前
前端必备:从能力检测到 UA-CH,浏览器客户端检测的完整指南
前端
前端小巷子17 分钟前
Vue 3全面提速剖析
前端·vue.js·面试
悟空聊架构23 分钟前
我的网站被攻击了,被干掉了 120G 流量,还在持续攻击中...
java·前端·架构
CodeSheep25 分钟前
国内 IT 公司时薪排行榜。
前端·后端·程序员
尖椒土豆sss29 分钟前
踩坑vue项目中使用 iframe 嵌套子系统无法登录,不报错问题!
前端·vue.js
遗悲风29 分钟前
html二次作业
前端·html
江城开朗的豌豆33 分钟前
React输入框优化:如何精准获取用户输入完成后的最终值?
前端·javascript·全栈
CF14年老兵33 分钟前
从卡顿到飞驰:我是如何用WebAssembly引爆React性能的
前端·react.js·trae
画月的亮36 分钟前
前端处理导出PDF。Vue导出pdf
前端·vue.js·pdf