medentem
medentem

Reputation: 31

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!

Upvotes: 2

Views: 292

Answers (1)

medentem
medentem

Reputation: 31

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;

Upvotes: 1

Related Questions