在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这个文件在安装时没有复制过来导致的,使用npm和yarn安装并不会出现这个问题,锅显然是pnpm的。

我们知道,pnpm有别于npm和yarn的核心卖点是,使用硬链接和符号链接的方式来管理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月就已经被提出了,顺便还给了一个临时解决方案:

也就是在prepare的hook里,先删除这个目录,再重新执行一次Electron的install脚本:
            
            
              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配置项的作用。如果设置为true,pnpm会在每个项目的node_modules目录下缓存这些副作用。这样,即使这个包在全局存储位置已经存在,pnpm也会重新执行这个包的安装过程,以生成这些副作用。
如果设置为false,pnpm则不会缓存这些副作用。这意味着,如果一个包在全局存储位置已经存在,pnpm就不会重新执行这个包的安装过程,即使这个过程可能会生成一些项目特定的副作用。
Bug修复解析
紧接着,这个Bug就被大佬修复了: 
翻译过来是:
符号链接被解析为其真实路径并上传为真实文件。它们不会占用更多空间,因为硬链接最终指向同一文件。这可能令人惊讶,因为在首次安装时,用户会看到符号链接,但在后续安装中,软件包将使用"真实文件",因为它们将被链接到存储库(来自副作用缓存)。我认为理想情况下,在从副作用缓存链接软件包时,我们应该恢复符号链接,但这将需要对软件包索引文件进行破坏性更改,因为目前仅支持"真实文件"条目。
已经合并到main分支和v9 beta版本上了:

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

我们使用corepack切换pnpm为v8.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的处理。
你可能会产生疑惑,什么情况下,在33行fs.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版本。