electron / forge

:electron: A complete tool for building and publishing Electron applications
https://electronforge.io
MIT License
6.33k stars 492 forks source link

local modules added with npm can break vite-based build #3624

Open noah10 opened 3 weeks ago

noah10 commented 3 weeks ago

Pre-flight checklist

Electron Forge version

7.4.0

Electron version

30.1.0

Operating system

macOS 13.6.3

Last known working Electron Forge version

No response

Expected behavior

When I add a local module (i.e. one that is in another directory in the same repo, rather than hosted in an npm repository) I should be able to use npm run make to successfully build the electron application that I have created from the vite template.

Actual behavior

The application build fails with no error message. It appears to fail on the 'Preparing native dependencies' step (though that's a red herring). Running DEBUG=electron-packager npm run make yields this error message:

An unhandled rejection has occurred inside Forge:
Error: ENOENT: no such file or directory, stat '/private/var/folders/_c/3rpnnjmd0cxb4wkz_k2pyf8c0000gn/T/electron-packager/tmp-J3b8pc/Electron.app/Contents/Resources/app/node_modules/sayhi'

Steps to reproduce

You can find a minimal reproduction at https://github.com/noah10/electron-forge-symlink-problem. After checking it out, do the following to reproduce the problem:

cd electron-forge-symlink-problem/forge-project
npm install
DEBUG=electron-packager npm run make

Additional information

The problem seems to happen when electron-forge copies the dependencies into a temporary directory. When we install the local module, npm creates a symlink for it in the node_modules directory:

ls -ld node_modules/sayhi           
lrwxr-xr-x  1  18 Jun 10 11:27 node_modules/sayhi -> ../../common/sayhi

The build process then copies this symlink itself, rather than the directory it points to, into the temp directory:

ls -l /private/var/folders/_c/3rpnnjmd0cxb4wkz_k2pyf8c0000gn/T/electron-packager/tmp-J3b8pc/Electron.app/Contents/Resources/app/node_modules/sayhi
lrwxr-xr-x  1  18 Jun 10 11:52 /private/var/folders/_c/3rpnnjmd0cxb4wkz_k2pyf8c0000gn/T/electron-packager/tmp-J3b8pc/Electron.app/Contents/Resources/app/node_modules/sayhi -> ../../common/sayhi

...but of course the directory that symlink points to does not exist. The ultimate error is triggered when the vite plugin is trying to copy dependencies: https://github.com/electron/forge/blob/38f64e4c9e2c79a3b9ff8447b1a23199415ede97/packages/plugin/vite/src/VitePlugin.ts#L125 .

I think that probably the right fix is for forge (or perhaps packager?) to copy the directory the symlink points to (rather than the symlink itself) into the temporary directory. At that point the vite plugin should no longer have a problem with it.

For anyone else who comes here and needs a temporary solution before the bug is fixed in forge/package/vite plugin, I was able to work around it by adding a packageAfterCopy hook to my forge.config.js that deletes the symlinks from the temp directory and then patching the vite plugin to handle symlinks correctly. If you're able to, I think that using yarn v1 and 'yarn link' (instead of npm) to install your local modules will also work, though I haven't completely tested that yet.

Here's my packageAfterCopy hook:

packageAfterCopy: async (_config, buildPath) => {
      const symlinkedModules = ['node_modules/logger', 'node_modules/pay-info'];

      const deleteFn = (symlinkMod) => {
        const target = path.join(buildPath, symlinkMod);
        try {
          fs.unlinkSync(target);
          fs.lstatSync(target);
          console.log('logger still exists');
        } catch (err) {
          if (err.code === 'ENOENT') {
            console.log(`${target} has been deleted`)
          } else {
            console.error(err);
          }
        }
      }

      symlinkedModules.forEach(deleteFn)

    }

And the patch to VitePlugin is to add {dereference: true} to the copy call from line 125 of the vite plugin (shown above): await fs.copy(dep.src, path.resolve(buildPath, dep.dest), {dereference: true});. This forces the fs-extra.copy call to use lstat rather than stat, causing the vite plugin to copy the directory the symlink points to.

noah10 commented 1 week ago

It looks like #3632 may be related to this.