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!
After many many hours of trying and failing, I was able to get the right configuration to build the project.
Key learnings:
external
rollup options.base
path to resolve paths correctly.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;