electronviteelectron-builderelectron-forge

Electron Forge + SerialPort native dependency packaging not working


I am working on an open source firmware update app for Meshtastic (https://github.com/medentem/electron-flasher/). The app is a ReactJS/TS app bundled with Vite for Electron. When running locally on OSX (electron-forge start), it functions perfectly and the native dependencies (serialport and drivelist) seem to be referenced properly. However, after bundling the OSX app for distribution (electron-forge make), the resulting app launches with this error:

Uncaught Exception:
Error: Cannot find module 'serialport'
Require stack:
- /Users/medentem/Dev/electron-flasher/out/electron-flasher-darwin-arm64/electron-flasher.app/Contents/Resources/app.asar/.vite/build/main.js
- 
at Module._resolveFilename (node:internal/modules/cjs/loader:1232:15)
at s._resolveFilename (node:electron/js2c/browser_init:2:124038)
at Module._load (node:internal/modules/cjs/loader:1058:27)
at c._load (node:electron/js2c/node_init:2:17025)
at Module.require (node:internal/modules/cjs/loader:1318:19)
at require (node:internal/modules/helpers:179:18)
at Object.<anonymous> (/Users/medentem/Dev/electron-flasher/out/electron-flasher-darwin-arm64/electron-flasher.app/Contents/Resources/app.asar/.vite/build/main.js:1:214)
at Module._compile (node:internal/modules/cjs/loader:1484:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1564:10)
at Module.load (node:internal/modules/cjs/loader:1295:32)

I confirmed that the serialport and drivelist packages are configured in vite as external (see https://github.com/medentem/electron-flasher/blob/main/vite.main.config.ts), and that the electron-forge configuration AutoUnpackNativesPlugin to ensure both packages are rebuilt and included outside of the asar bundle. But that does not seem to work.

I've also tried to use electron-builder to generate the OSX app, but in that case, the app will not even launch.

Thank you in advance for any help!


Solution

  • After many many hours of trying and failing, I was able to get the right configuration to build the project.

    Key learnings:

    1. When using forge and vite, the build output must be in a .vite folder.
    2. electron itself should be listed in the external rollup options.
    3. The renderer config needs a base path to resolve paths correctly.
    4. Using the emptyOutDir config value is ... dicey because it depends on what order forge builds the package. I found that turning that off in the vite.main.config.ts config works, otherwise the new subdirectories get blown away right after they were built.

    Here is a snapshot of the config - for the latest, view the repository linked in the question.

    vite.main.config.ts

    import { defineConfig } from "vite";
    import { builtinModules } from "node:module";
    
    export default defineConfig({
      build: {
        sourcemap: true,
        outDir: ".vite", // Output directory set to .vite
        lib: {
          entry: "src/main.ts",
          formats: ["cjs"],
        },
        rollupOptions: {
          external: ["electron", ...builtinModules, "serialport", "drivelist"],
        },
      },
    });
    

    vite.preload.config.ts

    import { defineConfig } from "vite";
    import { builtinModules } from "node:module";
    
    export default defineConfig({
      build: {
        sourcemap: true,
        outDir: ".vite/preload", // Output directory set to .vite
        emptyOutDir: true,
        lib: {
          entry: "src/preload.ts",
          formats: ["cjs"],
        },
        rollupOptions: {
          external: ["electron", ...builtinModules],
          output: {
            entryFileNames: "[name].js",
          },
        },
      },
    });
    

    vite.renderer.config.ts

    import { defineConfig } from "vite";
    import react from "@vitejs/plugin-react";
    
    export default defineConfig({
      plugins: [react()],
      base: "./",
      build: {
        sourcemap: true,
        outDir: ".vite/renderer", // Output directory set to .vite
        emptyOutDir: true,
      },
    });
    

    forge.config.ts

    import type { ForgeConfig } from "@electron-forge/shared-types";
    import { AutoUnpackNativesPlugin } from "@electron-forge/plugin-auto-unpack-natives";
    import { MakerSquirrel } from "@electron-forge/maker-squirrel";
    import { MakerZIP } from "@electron-forge/maker-zip";
    import { MakerDeb } from "@electron-forge/maker-deb";
    import { MakerDMG } from "@electron-forge/maker-dmg";
    import { MakerRpm } from "@electron-forge/maker-rpm";
    import { VitePlugin } from "@electron-forge/plugin-vite";
    import { FusesPlugin } from "@electron-forge/plugin-fuses";
    import { FuseV1Options, FuseVersion } from "@electron/fuses";
    
    const config: ForgeConfig = {
      packagerConfig: {
        asar: true,
        ignore: [/\/\.(?!vite)/],
      },
      makers: [
        new MakerSquirrel({}),
        new MakerZIP({}, ["darwin"]),
        new MakerRpm({}),
        new MakerDeb({}),
        new MakerDMG(),
      ],
      rebuildConfig: {
        force: true,
        onlyModules: ["serialport", "drivelist"],
      },
      plugins: [
        new AutoUnpackNativesPlugin({}),
        new VitePlugin({
          // `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
          // If you are familiar with Vite configuration, it will look really familiar.
          build: [
            {
              // `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
              entry: "src/main.ts",
              config: "vite.main.config.ts",
              target: "main",
            },
            {
              entry: "src/preload.ts",
              config: "vite.preload.config.ts",
              target: "preload",
            },
          ],
          renderer: [
            {
              name: "main_window",
              config: "vite.renderer.config.ts",
            },
          ],
        }),
        // Fuses are used to enable/disable various Electron functionality
        // at package time, before code signing the application
        new FusesPlugin({
          version: FuseVersion.V1,
          [FuseV1Options.RunAsNode]: false,
          [FuseV1Options.EnableCookieEncryption]: true,
          [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
          [FuseV1Options.EnableNodeCliInspectArguments]: false,
          [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
          [FuseV1Options.OnlyLoadAppFromAsar]: true,
        }),
      ],
    };
    
    export default config;