Electron is a framework that lets you build desktop applications using the same three technologies you already use on the web: HTML, CSS and JavaScript. It bundles Chromium (for rendering the interface) and Node.js (for the system-level work), so a single codebase can run as a native-feeling app on Windows, macOS and Linux. Visual Studio Code, Slack and Discord are all built this way.
Writing the app is the easy half. The half that usually trips people up is packaging: turning a folder of JavaScript files into an .exe installer, a .dmg disk image and an .AppImage or .deb package. That is what we are doing here. We will build a tiny demo app first, and then compile it for all three platforms with electron-builder.
Getting Started:
You only need Node.js (version 18 or newer) and npm. Check what you have:
bash
node -v
npm -v
Create the project folder and initialize it:
bash
mkdir electron-demo
cd electron-demo
npm init -y
Now install Electron and the packaging tool. Both are development dependencies — they are used to build the app, not shipped as part of it:
bash
npm install --save-dev electron electron-builder
This is the structure we are going to end up with:
An Electron app has two kinds of processes. The main process is Node.js: it creates windows and talks to the operating system. The renderer process is the web page inside the window. They are isolated from each other, and a preload script is the bridge that exposes a controlled set of functions from one side to the other.
main.js
This creates the window and answers one message coming from the interface:
javascript
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 720,
height: 480,
title: 'Electron Demo',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
win.loadFile(path.join(__dirname, 'renderer', 'index.html'));
}
// Handler for the message sent by the renderer.
ipcMain.handle('get-info', () => {
return {
appVersion: app.getVersion(),
electron: process.versions.electron,
node: process.versions.node,
chrome: process.versions.chrome,
platform: process.platform,
arch: process.arch
};
});
app.whenReady().then(() => {
createWindow();
// On macOS, re-create the window when the dock icon is clicked.
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
// On Windows and Linux, closing every window quits the app.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
preload.js
Instead of giving the web page full access to Node, we expose exactly one function:
electron-builder will happily build without custom icons, but you will get the default Electron logo. Drop three files into build/ and it picks them up automatically:
File
Platform
Requirements
build/icon.ico
Windows
Multi-size ICO, at least 256×256
build/icon.icns
macOS
ICNS, from a 1024×1024 source
build/icon.png
Linux
PNG, 512×512 or 1024×1024
If you only have a PNG, the easiest path is to keep a 1024×1024 build/icon.png; electron-builder generates the missing formats for Linux and Windows from it. For macOS, providing the .icns yourself is the safer option.
Building for Windows:
Run this on a Windows machine:
bash
npm run dist:win
You end up with two artifacts inside dist/:
Electron Demo Setup 1.0.0.exe — an NSIS installer with a wizard, Start Menu entry and uninstaller.
Electron Demo 1.0.0.exe — the portable build, a single executable that runs with no installation.
Because the mac block above lists both architectures, you get Intel and Apple Silicon versions:
Electron Demo-1.0.0.dmg — Intel (x64) disk image.
Electron Demo-1.0.0-arm64.dmg — Apple Silicon disk image.
.zip versions of both, which is the format auto-update uses on macOS.
You can also produce a single binary that runs natively on both chips:
bash
npx electron-builder --mac --universal
Heads-up on macOSAn unsigned app will be blocked by Gatekeeper on somebody else's Mac. For personal use that is fine — right-click the app and chooseOpen. To distribute it, you need an Apple Developer account, a Developer ID certificate, and notarization.
With a certificate installed and an app-specific password created, signing and notarizing is a matter of environment variables plus one flag:
bash
export APPLE_ID="you@example.com"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
export APPLE_TEAM_ID="ABCDE12345"
npm run dist:mac
Other formats are just one more entry in the target list — rpm for Fedora and RHEL, snap for the Snap Store, tar.gz for a plain archive:
bash
npx electron-builder --linux AppImage deb rpm tar.gz
Cross-Compiling:
The command npm run dist:all (electron-builder -mwl) is real, but it only works from the right host. This is the part of the process worth memorizing:
You are on
Windows target
macOS target
Linux target
macOS
Yes (needs Wine)
Yes
Yes
Linux
Yes (needs Wine)
No
Yes
Windows
Yes
No
Partially — use WSL or Docker
The short version: macOS builds require a Mac, because Apple's code-signing and DMG tooling does not exist anywhere else. Everything else can be arranged. On Linux you can build Windows and Linux artifacts inside the official Docker image without installing Wine yourself:
The clean answer to the Mac problem is to let a build server do it. This GitHub Actions workflow runs the same build on three runners in parallel and uploads every artifact:
Push a tag such as v1.0.0 and a few minutes later you have installers for the three platforms sitting in the workflow's artifacts, all built from the same commit.
The dist Folder:
dist/
├─ Electron Demo Setup 1.0.0.exe <- Windows installer
├─ Electron Demo 1.0.0.exe <- Windows portable
├─ Electron Demo-1.0.0.dmg <- macOS Intel
├─ Electron Demo-1.0.0-arm64.dmg <- macOS Apple Silicon
├─ Electron Demo-1.0.0.AppImage <- Linux universal
├─ electron-demo_1.0.0_amd64.deb <- Debian / Ubuntu
├─ latest.yml / latest-mac.yml <- auto-update metadata
└─ win-unpacked/ mac/ linux-unpacked/ <- raw application folders
Expect a big downloadEvery Electron app ships its own copy of Chromium, so even this demo lands somewhere around 70–100 MB per installer. That is normal, and there is no way around it — it is the price of the framework.
Common Problems:
A blank white window. Almost always a wrong path in win.loadFile(). Always build the path with path.join(__dirname, ...), never a relative string.
Works with npm start, fails when packaged. A file is missing from the files array in the build config. Anything the app loads at runtime has to be listed there.
window.api is undefined. The preload path is wrong, or contextIsolation was turned off.
“Cannot find module” after packaging. The package was installed with --save-dev but the app needs it at runtime. Move it to dependencies.
The build hangs on the first run. electron-builder is downloading the Electron binaries and the NSIS/Wine helpers into its cache. Let it finish; later builds are much faster.
Files:
The complete demo — main.js, preload.js, the renderer folder and the full package.json — is available in the repository:
Post a Comment