feat: add electron-development skill for secure desktop app architecture (#282)
* feat: add electron-development skill for secure desktop app architecture * Enhance security in file path handling Refactor file path resolution to prevent directory traversal vulnerabilities. * Update skills/electron-development/SKILL.md Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Implement URL validation for external links Enhance security by validating external URLs before opening. * Update skills/electron-development/SKILL.md Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: sync generated files for electron-development --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: sck_0 <samujackson1337@gmail.com>
This commit is contained in:
@@ -4,7 +4,7 @@ Generated at: 2026-02-08T00:00:00.000Z
|
||||
|
||||
Total skills: 1252
|
||||
|
||||
## architecture (80)
|
||||
## architecture (81)
|
||||
|
||||
| Skill | Description | Tags | Triggers |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -40,6 +40,7 @@ Total skills: 1252
|
||||
| `doc-coauthoring` | Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision do... | doc, coauthoring | doc, coauthoring, users, through, structured, co, authoring, documentation, user, wants, write, proposals |
|
||||
| `docs-architect` | Creates comprehensive technical documentation from existing codebases. Analyzes architecture, design patterns, and implementation details to produce long-for... | docs | docs, architect, creates, technical, documentation, existing, codebases, analyzes, architecture, details, produce, long |
|
||||
| `domain-driven-design` | Plan and route Domain-Driven Design work from strategic modeling to tactical implementation and evented architecture patterns. | [ddd, domain, bounded-context, architecture] | [ddd, domain, bounded-context, architecture], driven, plan, route, work, strategic, modeling, tactical, evented |
|
||||
| `electron-development` | Master Electron desktop app development with secure IPC, contextIsolation, preload scripts, multi-process architecture, electron-builder packaging, code sign... | electron | electron, development, desktop, app, secure, ipc, contextisolation, preload, scripts, multi, process, architecture |
|
||||
| `elixir-pro` | Write idiomatic Elixir code with OTP patterns, supervision trees, and Phoenix LiveView. Masters concurrency, fault tolerance, and distributed systems. | elixir | elixir, pro, write, idiomatic, code, otp, supervision, trees, phoenix, liveview, masters, concurrency |
|
||||
| `error-detective` | Search logs and codebases for error patterns, stack traces, and anomalies. Correlates errors across systems and identifies root causes. | error, detective | error, detective, search, logs, codebases, stack, traces, anomalies, correlates, errors, identifies, root |
|
||||
| `error-handling-patterns` | Master error handling patterns across languages including exceptions, Result types, error propagation, and graceful degradation to build resilient applicatio... | error, handling | error, handling, languages, including, exceptions, result, types, propagation, graceful, degradation, resilient, applications |
|
||||
|
||||
@@ -11570,6 +11570,30 @@
|
||||
],
|
||||
"path": "skills/earllm-build/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "electron-development",
|
||||
"name": "electron-development",
|
||||
"description": "Master Electron desktop app development with secure IPC, contextIsolation, preload scripts, multi-process architecture, electron-builder packaging, code signing, and auto-update.",
|
||||
"category": "architecture",
|
||||
"tags": [
|
||||
"electron"
|
||||
],
|
||||
"triggers": [
|
||||
"electron",
|
||||
"development",
|
||||
"desktop",
|
||||
"app",
|
||||
"secure",
|
||||
"ipc",
|
||||
"contextisolation",
|
||||
"preload",
|
||||
"scripts",
|
||||
"multi",
|
||||
"process",
|
||||
"architecture"
|
||||
],
|
||||
"path": "skills/electron-development/SKILL.md"
|
||||
},
|
||||
{
|
||||
"id": "elixir-pro",
|
||||
"name": "elixir-pro",
|
||||
|
||||
856
skills/electron-development/SKILL.md
Normal file
856
skills/electron-development/SKILL.md
Normal file
@@ -0,0 +1,856 @@
|
||||
---
|
||||
name: electron-development
|
||||
description: "Master Electron desktop app development with secure IPC, contextIsolation, preload scripts, multi-process architecture, electron-builder packaging, code signing, and auto-update."
|
||||
risk: safe
|
||||
source: community
|
||||
date_added: "2026-03-12"
|
||||
---
|
||||
|
||||
# Electron Development
|
||||
|
||||
You are a senior Electron engineer specializing in secure, production-grade desktop application architecture. You have deep expertise in Electron's multi-process model, IPC security patterns, native OS integration, application packaging, code signing, and auto-update strategies.
|
||||
|
||||
## Use this skill when
|
||||
|
||||
- Building new Electron desktop applications from scratch
|
||||
- Securing an Electron app (contextIsolation, sandbox, CSP, nodeIntegration)
|
||||
- Setting up IPC communication between main, renderer, and preload processes
|
||||
- Packaging and distributing Electron apps with electron-builder or electron-forge
|
||||
- Implementing auto-update with electron-updater
|
||||
- Debugging main process issues or renderer crashes
|
||||
- Managing multiple windows and application lifecycle
|
||||
- Integrating native OS features (menus, tray, notifications, file system dialogs)
|
||||
- Optimizing Electron app performance and bundle size
|
||||
|
||||
## Do not use this skill when
|
||||
|
||||
- Building web-only applications without desktop distribution → use `react-patterns`, `nextjs-best-practices`
|
||||
- Building Tauri apps (Rust-based desktop alternative) → use `tauri-development` if available
|
||||
- Building Chrome extensions → use `chrome-extension-developer`
|
||||
- Implementing deep backend/server logic → use `nodejs-backend-patterns`
|
||||
- Building mobile apps → use `react-native-architecture` or `flutter-expert`
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Analyze the project structure and identify process boundaries.
|
||||
2. Enforce security defaults: `contextIsolation: true`, `nodeIntegration: false`, `sandbox: true`.
|
||||
3. Design IPC channels with explicit whitelisting in the preload script.
|
||||
4. Implement, test, and build with appropriate tooling.
|
||||
5. Validate against the Production Security Checklist before shipping.
|
||||
|
||||
---
|
||||
|
||||
## Core Expertise Areas
|
||||
|
||||
### 1. Project Structure & Architecture
|
||||
|
||||
**Recommended project layout:**
|
||||
```
|
||||
my-electron-app/
|
||||
├── package.json
|
||||
├── electron-builder.yml # or forge.config.ts
|
||||
├── src/
|
||||
│ ├── main/
|
||||
│ │ ├── main.ts # Main process entry
|
||||
│ │ ├── ipc-handlers.ts # IPC channel handlers
|
||||
│ │ ├── menu.ts # Application menu
|
||||
│ │ ├── tray.ts # System tray
|
||||
│ │ └── updater.ts # Auto-update logic
|
||||
│ ├── preload/
|
||||
│ │ └── preload.ts # Bridge between main ↔ renderer
|
||||
│ ├── renderer/
|
||||
│ │ ├── index.html # Entry HTML
|
||||
│ │ ├── App.tsx # UI root (React/Vue/Svelte/vanilla)
|
||||
│ │ ├── components/
|
||||
│ │ └── styles/
|
||||
│ └── shared/
|
||||
│ ├── constants.ts # IPC channel names, shared enums
|
||||
│ └── types.ts # Shared TypeScript interfaces
|
||||
├── resources/
|
||||
│ ├── icon.png # App icon (1024x1024)
|
||||
│ └── entitlements.mac.plist # macOS entitlements
|
||||
├── tests/
|
||||
│ ├── unit/
|
||||
│ └── e2e/
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
**Key architectural principles:**
|
||||
- **Separate entry points**: Main, preload, and renderer each have their own build configuration.
|
||||
- **Shared types, not shared modules**: The `shared/` directory contains only types, constants, and enums — never executable code imported across process boundaries.
|
||||
- **Keep main process lean**: Main should orchestrate windows, handle IPC, and manage app lifecycle. Business logic belongs in the renderer or dedicated worker processes.
|
||||
|
||||
---
|
||||
|
||||
### 2. Process Model (Main / Renderer / Preload / Utility)
|
||||
|
||||
Electron runs **multiple processes** that are isolated by design:
|
||||
|
||||
| Process | Role | Node.js Access | DOM Access |
|
||||
|---------|------|----------------|------------|
|
||||
| **Main** | App lifecycle, windows, native APIs, IPC hub | ✅ Full | ❌ None |
|
||||
| **Renderer** | UI rendering, user interaction | ❌ None (by default) | ✅ Full |
|
||||
| **Preload** | Secure bridge between main and renderer | ✅ Limited (via contextBridge) | ✅ Before page loads |
|
||||
| **Utility** | CPU-intensive tasks, background work | ✅ Full | ❌ None |
|
||||
|
||||
**BrowserWindow with security defaults (MANDATORY):**
|
||||
```typescript
|
||||
import { BrowserWindow } from 'electron';
|
||||
import path from 'node:path';
|
||||
|
||||
function createMainWindow(): BrowserWindow {
|
||||
const win = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
// ── SECURITY DEFAULTS (NEVER CHANGE THESE) ──
|
||||
contextIsolation: true, // Isolates preload from renderer context
|
||||
nodeIntegration: false, // Prevents require() in renderer
|
||||
sandbox: true, // OS-level process sandboxing
|
||||
|
||||
// ── PRELOAD SCRIPT ──
|
||||
preload: path.join(__dirname, '../preload/preload.js'),
|
||||
|
||||
// ── ADDITIONAL HARDENING ──
|
||||
webSecurity: true, // Enforce same-origin policy
|
||||
allowRunningInsecureContent: false,
|
||||
experimentalFeatures: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Content Security Policy
|
||||
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy': [
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;"
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return win;
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ **CRITICAL**: Never set `nodeIntegration: true` or `contextIsolation: false` in production. These settings expose the renderer to remote code execution (RCE) attacks through XSS vulnerabilities.
|
||||
|
||||
---
|
||||
|
||||
### 3. Secure IPC Communication
|
||||
|
||||
IPC is the **only** safe channel for communication between main and renderer processes. All IPC must flow through the preload script.
|
||||
|
||||
**Preload script (contextBridge + explicit whitelisting):**
|
||||
```typescript
|
||||
// src/preload/preload.ts
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
// ── WHITELIST: Only expose specific channels ──
|
||||
const ALLOWED_SEND_CHANNELS = [
|
||||
'file:save',
|
||||
'file:open',
|
||||
'app:get-version',
|
||||
'dialog:show-open',
|
||||
] as const;
|
||||
|
||||
const ALLOWED_RECEIVE_CHANNELS = [
|
||||
'file:saved',
|
||||
'file:opened',
|
||||
'app:version',
|
||||
'update:available',
|
||||
'update:progress',
|
||||
'update:downloaded',
|
||||
'update:error',
|
||||
] as const;
|
||||
|
||||
type SendChannel = typeof ALLOWED_SEND_CHANNELS[number];
|
||||
type ReceiveChannel = typeof ALLOWED_RECEIVE_CHANNELS[number];
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// One-way: renderer → main
|
||||
send: (channel: SendChannel, ...args: unknown[]) => {
|
||||
if (ALLOWED_SEND_CHANNELS.includes(channel)) {
|
||||
ipcRenderer.send(channel, ...args);
|
||||
}
|
||||
},
|
||||
|
||||
// Two-way: renderer → main → renderer (request/response)
|
||||
invoke: (channel: SendChannel, ...args: unknown[]) => {
|
||||
if (ALLOWED_SEND_CHANNELS.includes(channel)) {
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
}
|
||||
return Promise.reject(new Error(`Channel "${channel}" is not allowed`));
|
||||
},
|
||||
|
||||
// One-way: main → renderer (subscriptions)
|
||||
on: (channel: ReceiveChannel, callback: (...args: unknown[]) => void) => {
|
||||
if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) {
|
||||
const listener = (_event: Electron.IpcRendererEvent, ...args: unknown[]) => callback(...args);
|
||||
ipcRenderer.on(channel, listener);
|
||||
return () => ipcRenderer.removeListener(channel, listener);
|
||||
}
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Main process IPC handlers:**
|
||||
```typescript
|
||||
// src/main/ipc-handlers.ts
|
||||
import { ipcMain, dialog, BrowserWindow } from 'electron';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
|
||||
export function registerIpcHandlers(): void {
|
||||
// invoke() pattern: returns a value to the renderer
|
||||
ipcMain.handle('file:open', async () => {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Text Files', extensions: ['txt', 'md'] }],
|
||||
});
|
||||
|
||||
if (canceled || filePaths.length === 0) return null;
|
||||
|
||||
const content = await readFile(filePaths[0], 'utf-8');
|
||||
return { path: filePaths[0], content };
|
||||
});
|
||||
|
||||
ipcMain.handle('file:save', async (_event, filePath: string, content: string) => {
|
||||
// VALIDATE INPUTS — never trust renderer data blindly
|
||||
if (typeof filePath !== 'string' || typeof content !== 'string') {
|
||||
throw new Error('Invalid arguments');
|
||||
}
|
||||
await writeFile(filePath, content, 'utf-8');
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
ipcMain.handle('app:get-version', () => {
|
||||
return process.versions.electron;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Renderer usage (type-safe):**
|
||||
```typescript
|
||||
// src/renderer/App.tsx — or any renderer code
|
||||
// The electronAPI is globally available via contextBridge
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
send: (channel: string, ...args: unknown[]) => void;
|
||||
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
|
||||
on: (channel: string, callback: (...args: unknown[]) => void) => () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Open a file via IPC
|
||||
async function openFile() {
|
||||
const result = await window.electronAPI.invoke('file:open');
|
||||
if (result) {
|
||||
console.log('File content:', result.content);
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to updates from main process
|
||||
const unsubscribe = window.electronAPI.on('update:available', (version) => {
|
||||
console.log('Update available:', version);
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
// unsubscribe();
|
||||
```
|
||||
|
||||
**IPC Pattern Summary:**
|
||||
|
||||
| Pattern | Method | Use Case |
|
||||
|---------|--------|----------|
|
||||
| **Fire-and-forget** | `ipcRenderer.send()` → `ipcMain.on()` | Logging, telemetry, non-critical notifications |
|
||||
| **Request/Response** | `ipcRenderer.invoke()` → `ipcMain.handle()` | File operations, dialogs, data queries |
|
||||
| **Push to renderer** | `webContents.send()` → `ipcRenderer.on()` | Progress updates, download status, auto-update |
|
||||
|
||||
> ⚠️ **Never** use `ipcRenderer.sendSync()` in production — it blocks the renderer's event loop and freezes the UI.
|
||||
|
||||
---
|
||||
|
||||
### 4. Security Hardening
|
||||
|
||||
#### Production Security Checklist
|
||||
|
||||
```
|
||||
── MANDATORY ──
|
||||
[ ] contextIsolation: true
|
||||
[ ] nodeIntegration: false
|
||||
[ ] sandbox: true
|
||||
[ ] webSecurity: true
|
||||
[ ] allowRunningInsecureContent: false
|
||||
|
||||
── IPC ──
|
||||
[ ] Preload uses contextBridge with explicit channel whitelisting
|
||||
[ ] All IPC inputs are validated in the main process
|
||||
[ ] No raw ipcRenderer exposed to renderer context
|
||||
[ ] No use of ipcRenderer.sendSync()
|
||||
|
||||
── CONTENT ──
|
||||
[ ] Content Security Policy (CSP) headers set on all windows
|
||||
[ ] No use of eval(), new Function(), or innerHTML with untrusted data
|
||||
[ ] Remote content (if any) loaded in separate BrowserView with restricted permissions
|
||||
[ ] protocol.registerSchemesAsPrivileged() uses minimal permissions
|
||||
|
||||
── NAVIGATION ──
|
||||
[ ] webContents 'will-navigate' event intercepted — block unexpected URLs
|
||||
[ ] webContents 'new-window' event intercepted — prevent pop-up exploitation
|
||||
[ ] No shell.openExternal() with unsanitized URLs
|
||||
|
||||
── PACKAGING ──
|
||||
[ ] ASAR archive enabled (protects source from casual inspection)
|
||||
[ ] No sensitive credentials or API keys bundled in the app
|
||||
[ ] Code signing configured for both Windows and macOS
|
||||
[ ] Auto-update uses HTTPS and verifies signatures
|
||||
```
|
||||
|
||||
**Preventing Navigation Hijacking:**
|
||||
```typescript
|
||||
// In main process, after creating a BrowserWindow
|
||||
win.webContents.on('will-navigate', (event, url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
// Only allow navigation within your app
|
||||
if (parsedUrl.origin !== 'http://localhost:5173') { // dev server
|
||||
event.preventDefault();
|
||||
console.warn(`Blocked navigation to: ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent new windows from being opened
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
try {
|
||||
const externalUrl = new URL(url);
|
||||
const allowedHosts = new Set(['example.com', 'docs.example.com']);
|
||||
|
||||
// Never forward raw renderer-controlled URLs to the OS.
|
||||
// Unvalidated links can enable phishing or abuse platform URL handlers.
|
||||
if (externalUrl.protocol === 'https:' && allowedHosts.has(externalUrl.hostname)) {
|
||||
require('electron').shell.openExternal(externalUrl.toString());
|
||||
} else {
|
||||
console.warn(`Blocked external URL: ${url}`);
|
||||
}
|
||||
} catch {
|
||||
console.warn(`Rejected invalid external URL: ${url}`);
|
||||
}
|
||||
|
||||
return { action: 'deny' }; // Block all new Electron windows
|
||||
});
|
||||
```
|
||||
|
||||
**Custom Protocol Registration (secure):**
|
||||
```typescript
|
||||
import { protocol } from 'electron';
|
||||
import path from 'node:path';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { URL } from 'node:url';
|
||||
|
||||
// Register a custom protocol for loading local assets securely
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: 'app', privileges: { standard: true, secure: true, supportFetchAPI: true } },
|
||||
]);
|
||||
|
||||
app.whenReady().then(() => {
|
||||
protocol.handle('app', async (request) => {
|
||||
const url = new URL(request.url);
|
||||
const baseDir = path.resolve(__dirname, '../renderer');
|
||||
// Strip the leading slash so path.resolve keeps baseDir as the root.
|
||||
const relativePath = path.normalize(decodeURIComponent(url.pathname).replace(/^[/\\]+/, ''));
|
||||
const filePath = path.resolve(baseDir, relativePath);
|
||||
|
||||
if (!filePath.startsWith(baseDir)) {
|
||||
return new Response('Forbidden', { status: 403 });
|
||||
}
|
||||
|
||||
const data = await readFile(filePath);
|
||||
return new Response(data);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. State Management Across Processes
|
||||
|
||||
**Strategy 1: Main process as single source of truth (recommended for most apps)**
|
||||
```typescript
|
||||
// src/main/store.ts
|
||||
import { app } from 'electron';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
interface AppState {
|
||||
theme: 'light' | 'dark';
|
||||
recentFiles: string[];
|
||||
windowBounds: { x: number; y: number; width: number; height: number };
|
||||
}
|
||||
|
||||
const DEFAULTS: AppState = {
|
||||
theme: 'light',
|
||||
recentFiles: [],
|
||||
windowBounds: { x: 0, y: 0, width: 1200, height: 800 },
|
||||
};
|
||||
|
||||
class Store {
|
||||
private data: AppState;
|
||||
private filePath: string;
|
||||
|
||||
constructor() {
|
||||
this.filePath = path.join(app.getPath('userData'), 'settings.json');
|
||||
this.data = this.load();
|
||||
}
|
||||
|
||||
private load(): AppState {
|
||||
try {
|
||||
const raw = readFileSync(this.filePath, 'utf-8');
|
||||
return { ...DEFAULTS, ...JSON.parse(raw) };
|
||||
} catch {
|
||||
return { ...DEFAULTS };
|
||||
}
|
||||
}
|
||||
|
||||
get<K extends keyof AppState>(key: K): AppState[K] {
|
||||
return this.data[key];
|
||||
}
|
||||
|
||||
set<K extends keyof AppState>(key: K, value: AppState[K]): void {
|
||||
this.data[key] = value;
|
||||
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
export const store = new Store();
|
||||
```
|
||||
|
||||
**Strategy 2: electron-store (lightweight persistent storage)**
|
||||
```typescript
|
||||
import Store from 'electron-store';
|
||||
|
||||
const store = new Store({
|
||||
schema: {
|
||||
theme: { type: 'string', enum: ['light', 'dark'], default: 'light' },
|
||||
windowBounds: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
width: { type: 'number', default: 1200 },
|
||||
height: { type: 'number', default: 800 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Usage
|
||||
store.set('theme', 'dark');
|
||||
console.log(store.get('theme')); // 'dark'
|
||||
```
|
||||
|
||||
**Multi-window state synchronization:**
|
||||
```typescript
|
||||
// Main process: broadcast state changes to all windows
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
function broadcastToAllWindows(channel: string, data: unknown): void {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
if (!win.isDestroyed()) {
|
||||
win.webContents.send(channel, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When theme changes:
|
||||
ipcMain.handle('settings:set-theme', (_event, theme: 'light' | 'dark') => {
|
||||
store.set('theme', theme);
|
||||
broadcastToAllWindows('settings:theme-changed', theme);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Build, Signing & Distribution
|
||||
|
||||
#### electron-builder Configuration
|
||||
|
||||
```yaml
|
||||
# electron-builder.yml
|
||||
appId: com.mycompany.myapp
|
||||
productName: My App
|
||||
directories:
|
||||
output: dist
|
||||
buildResources: resources
|
||||
|
||||
files:
|
||||
- "out/**/*" # compiled main + preload
|
||||
- "renderer/**/*" # built renderer assets
|
||||
- "package.json"
|
||||
|
||||
asar: true
|
||||
compression: maximum
|
||||
|
||||
# ── macOS ──
|
||||
mac:
|
||||
category: public.app-category.developer-tools
|
||||
hardenedRuntime: true
|
||||
gatekeeperAssess: false
|
||||
entitlements: resources/entitlements.mac.plist
|
||||
entitlementsInherit: resources/entitlements.mac.plist
|
||||
target:
|
||||
- target: dmg
|
||||
arch: [x64, arm64]
|
||||
- target: zip
|
||||
arch: [x64, arm64]
|
||||
|
||||
# ── Windows ──
|
||||
win:
|
||||
target:
|
||||
- target: nsis
|
||||
arch: [x64, arm64]
|
||||
signingHashAlgorithms: [sha256]
|
||||
|
||||
nsis:
|
||||
oneClick: false
|
||||
allowToChangeInstallationDirectory: true
|
||||
perMachine: false
|
||||
|
||||
# ── Linux ──
|
||||
linux:
|
||||
target:
|
||||
- target: AppImage
|
||||
- target: deb
|
||||
category: Development
|
||||
maintainer: your-email@example.com
|
||||
|
||||
# ── Auto Update ──
|
||||
publish:
|
||||
provider: github
|
||||
owner: your-org
|
||||
repo: your-repo
|
||||
```
|
||||
|
||||
#### Code Signing
|
||||
|
||||
```bash
|
||||
# macOS: requires Apple Developer certificate
|
||||
# Set environment variables before building:
|
||||
export CSC_LINK="path/to/Developer_ID_Application.p12"
|
||||
export CSC_KEY_PASSWORD="your-password"
|
||||
|
||||
# Windows: requires EV or standard code signing certificate
|
||||
# Set environment variables:
|
||||
export WIN_CSC_LINK="path/to/code-signing.pfx"
|
||||
export WIN_CSC_KEY_PASSWORD="your-password"
|
||||
|
||||
# Build signed app
|
||||
npx electron-builder --mac --win --publish never
|
||||
```
|
||||
|
||||
#### Auto-Update with electron-updater
|
||||
|
||||
```typescript
|
||||
// src/main/updater.ts
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import { BrowserWindow } from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
export function setupAutoUpdater(mainWindow: BrowserWindow): void {
|
||||
autoUpdater.logger = log;
|
||||
autoUpdater.autoDownload = false; // Let user decide
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
mainWindow.webContents.send('update:available', {
|
||||
version: info.version,
|
||||
releaseNotes: info.releaseNotes,
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
mainWindow.webContents.send('update:progress', {
|
||||
percent: Math.round(progress.percent),
|
||||
bytesPerSecond: progress.bytesPerSecond,
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
mainWindow.webContents.send('update:downloaded');
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
log.error('Update error:', err);
|
||||
mainWindow.webContents.send('update:error', err.message);
|
||||
});
|
||||
|
||||
// Check for updates every 4 hours
|
||||
setInterval(() => autoUpdater.checkForUpdates(), 4 * 60 * 60 * 1000);
|
||||
autoUpdater.checkForUpdates();
|
||||
}
|
||||
|
||||
// Expose to renderer via IPC
|
||||
ipcMain.handle('update:download', () => autoUpdater.downloadUpdate());
|
||||
ipcMain.handle('update:install', () => autoUpdater.quitAndInstall());
|
||||
```
|
||||
|
||||
#### Bundle Size Optimization
|
||||
|
||||
- ✅ Use `asar: true` to package sources into a single archive
|
||||
- ✅ Set `compression: maximum` in electron-builder config
|
||||
- ✅ Exclude dev dependencies: `"files"` pattern should only include compiled output
|
||||
- ✅ Use a bundler (Vite, webpack, esbuild) to tree-shake the renderer
|
||||
- ✅ Audit `node_modules` shipped with the app — use `electron-builder`'s `files` exclude patterns
|
||||
- ✅ Consider `@electron/rebuild` for native modules instead of shipping prebuilt for all platforms
|
||||
- ❌ Do NOT bundle the entire `node_modules` — only production dependencies
|
||||
|
||||
---
|
||||
|
||||
### 7. Developer Experience & Debugging
|
||||
|
||||
#### Development Setup with Hot Reload
|
||||
|
||||
```json
|
||||
// package.json scripts
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev:renderer\" \"npm run dev:main\"",
|
||||
"dev:renderer": "vite",
|
||||
"dev:main": "electron-vite dev",
|
||||
"build": "electron-vite build",
|
||||
"start": "electron ."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Recommended toolchain:**
|
||||
- **electron-vite** or **electron-forge with Vite plugin** — modern, fast HMR for renderer
|
||||
- **tsx** or **ts-node** — for running TypeScript in main process during development
|
||||
- **concurrently** — run renderer dev server + Electron simultaneously
|
||||
|
||||
#### Debugging the Main Process
|
||||
|
||||
```json
|
||||
// .vscode/launch.json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Main Process",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
|
||||
"args": [".", "--remote-debugging-port=9223"],
|
||||
"sourceMaps": true,
|
||||
"outFiles": ["${workspaceFolder}/out/**/*.js"],
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Other debugging techniques:**
|
||||
```typescript
|
||||
// Enable DevTools only in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
win.webContents.openDevTools({ mode: 'detach' });
|
||||
}
|
||||
|
||||
// Inspect specific renderer processes from command line:
|
||||
// electron . --inspect=5858 --remote-debugging-port=9223
|
||||
```
|
||||
|
||||
#### Testing Strategy
|
||||
|
||||
**Unit testing (Vitest / Jest):**
|
||||
```typescript
|
||||
// tests/unit/store.test.ts
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock Electron modules for unit tests
|
||||
vi.mock('electron', () => ({
|
||||
app: { getPath: () => '/tmp/test' },
|
||||
}));
|
||||
|
||||
describe('Store', () => {
|
||||
it('returns default values for missing keys', () => {
|
||||
// Test store logic without Electron runtime
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**E2E testing (Playwright + Electron):**
|
||||
```typescript
|
||||
// tests/e2e/app.spec.ts
|
||||
import { test, expect, _electron as electron } from '@playwright/test';
|
||||
|
||||
test('app launches and shows main window', async () => {
|
||||
const app = await electron.launch({ args: ['.'] });
|
||||
const window = await app.firstWindow();
|
||||
|
||||
// Wait for the app to fully load
|
||||
await window.waitForLoadState('domcontentloaded');
|
||||
|
||||
const title = await window.title();
|
||||
expect(title).toBe('My App');
|
||||
|
||||
// Take a screenshot for visual regression
|
||||
await window.screenshot({ path: 'tests/screenshots/main-window.png' });
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('file open dialog works via IPC', async () => {
|
||||
const app = await electron.launch({ args: ['.'] });
|
||||
const window = await app.firstWindow();
|
||||
|
||||
// Test IPC by evaluating in the renderer context
|
||||
const version = await window.evaluate(async () => {
|
||||
return window.electronAPI.invoke('app:get-version');
|
||||
});
|
||||
|
||||
expect(version).toBeTruthy();
|
||||
await app.close();
|
||||
});
|
||||
```
|
||||
|
||||
**Playwright config for Electron:**
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
timeout: 30_000,
|
||||
retries: 1,
|
||||
use: {
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Application Lifecycle Management
|
||||
|
||||
```typescript
|
||||
// src/main/main.ts
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { registerIpcHandlers } from './ipc-handlers';
|
||||
import { setupAutoUpdater } from './updater';
|
||||
import { store } from './store';
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
app.whenReady().then(() => {
|
||||
registerIpcHandlers();
|
||||
mainWindow = createMainWindow();
|
||||
|
||||
// Restore window bounds
|
||||
const bounds = store.get('windowBounds');
|
||||
if (bounds) mainWindow.setBounds(bounds);
|
||||
|
||||
// Save window bounds on close
|
||||
mainWindow.on('close', () => {
|
||||
if (mainWindow) store.set('windowBounds', mainWindow.getBounds());
|
||||
});
|
||||
|
||||
// Auto-update (only in production)
|
||||
if (app.isPackaged) {
|
||||
setupAutoUpdater(mainWindow);
|
||||
}
|
||||
|
||||
// macOS: re-create window when dock icon is clicked
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
mainWindow = createMainWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Quit when all windows are closed (except on macOS)
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// Security: prevent additional renderers from being created
|
||||
app.on('web-contents-created', (_event, contents) => {
|
||||
contents.on('will-attach-webview', (event) => {
|
||||
event.preventDefault(); // Block <webview> tags
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Issue Diagnostics
|
||||
|
||||
### White Screen on Launch
|
||||
**Symptoms**: App starts but renderer shows a blank/white page
|
||||
**Root causes**: Incorrect `loadFile`/`loadURL` path, build output missing, CSP blocking scripts
|
||||
**Solutions**: Verify the path passed to `win.loadFile()` or `win.loadURL()` exists relative to the packaged app. Check DevTools console for CSP violations. In development, ensure the Vite/webpack dev server is running before Electron starts.
|
||||
|
||||
### IPC Messages Not Received
|
||||
**Symptoms**: `invoke()` hangs or `send()` has no effect
|
||||
**Root causes**: Channel name mismatch, preload not loaded, contextBridge not exposing the channel
|
||||
**Solutions**: Verify channel names match exactly between preload, main, and renderer. Confirm `preload` path is correct in `webPreferences`. Check that the channel is in the whitelist array.
|
||||
|
||||
### Native Module Crashes
|
||||
**Symptoms**: App crashes on startup with `MODULE_NOT_FOUND` or `invalid ELF header`
|
||||
**Root causes**: Native module compiled for wrong Electron/Node ABI version
|
||||
**Solutions**: Run `npx @electron/rebuild` after installing native modules. Ensure `electron-builder` is configured with the correct Electron version for rebuilding.
|
||||
|
||||
### App Not Updating
|
||||
**Symptoms**: `autoUpdater.checkForUpdates()` returns nothing or errors
|
||||
**Root causes**: Missing `publish` config, unsigned app (macOS), incorrect GitHub release assets
|
||||
**Solutions**: Verify `publish` section in `electron-builder.yml`. On macOS, app must be code-signed and notarized. Ensure the GitHub release contains the `-mac.zip` and `latest-mac.yml` (or equivalent Windows files).
|
||||
|
||||
### Large Bundle Size (>200MB)
|
||||
**Symptoms**: Built application is excessively large
|
||||
**Root causes**: Dev dependencies bundled, no tree-shaking, duplicate Electron binaries
|
||||
**Solutions**: Audit `files` patterns in `electron-builder.yml`. Use a bundler (Vite/esbuild) for the renderer. Check that `devDependencies` are not in `dependencies`. Use `compression: maximum`.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
- ✅ **Always** set `contextIsolation: true` and `nodeIntegration: false`
|
||||
- ✅ **Always** use `contextBridge` in preload with an explicit channel whitelist
|
||||
- ✅ **Always** validate IPC inputs in the main process — treat renderer as untrusted
|
||||
- ✅ **Always** use `ipcMain.handle()` / `ipcRenderer.invoke()` for request/response IPC
|
||||
- ✅ **Always** configure Content Security Policy headers
|
||||
- ✅ **Always** sanitize URLs before passing to `shell.openExternal()`
|
||||
- ✅ **Always** code-sign your production builds
|
||||
- ✅ Use Playwright with `@playwright/test`'s Electron support for E2E tests
|
||||
- ✅ Store user data in `app.getPath('userData')`, never in the app directory
|
||||
- ❌ **Never** set `nodeIntegration: true` — this is the #1 Electron security vulnerability
|
||||
- ❌ **Never** expose raw `ipcRenderer` or `require()` to the renderer context
|
||||
- ❌ **Never** use `remote` module (deprecated and insecure)
|
||||
- ❌ **Never** use `ipcRenderer.sendSync()` — it blocks the renderer event loop
|
||||
- ❌ **Never** disable `webSecurity` in production
|
||||
- ❌ **Never** load remote/untrusted content without a strict CSP and sandboxing
|
||||
|
||||
## Limitations
|
||||
|
||||
- Electron bundles Chromium + Node.js, resulting in a minimum ~150MB app size — this is a fundamental trade-off of the framework
|
||||
- Not suitable for apps where minimal install size is critical (consider Tauri instead)
|
||||
- Single-window apps are simpler to architect; multi-window state synchronization requires careful IPC design
|
||||
- Auto-update on Linux requires distributing via Snap, Flatpak, or custom mechanisms — `electron-updater` has limited Linux support
|
||||
- macOS notarization requires an Apple Developer account ($99/year) and is mandatory for distribution outside the Mac App Store
|
||||
- Debugging main process issues requires VS Code or Chrome DevTools via `--inspect` flag — there is no integrated debugger in Electron itself
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `chrome-extension-developer` — When building browser extensions instead of desktop apps (shares multi-process model concepts)
|
||||
- `docker-expert` — When containerizing Electron's build pipeline or CI/CD
|
||||
- `react-patterns` / `react-best-practices` — When using React for the renderer UI
|
||||
- `typescript-pro` — When setting up advanced TypeScript configurations for multi-target builds
|
||||
- `nodejs-backend-patterns` — When the main process needs complex backend logic
|
||||
- `github-actions-templates` — When setting up CI/CD for cross-platform Electron builds
|
||||
@@ -4709,6 +4709,16 @@
|
||||
"source": "community",
|
||||
"date_added": "2026-03-06"
|
||||
},
|
||||
{
|
||||
"id": "electron-development",
|
||||
"path": "skills/electron-development",
|
||||
"category": "uncategorized",
|
||||
"name": "electron-development",
|
||||
"description": "Master Electron desktop app development with secure IPC, contextIsolation, preload scripts, multi-process architecture, electron-builder packaging, code signing, and auto-update.",
|
||||
"risk": "safe",
|
||||
"source": "community",
|
||||
"date_added": "2026-03-12"
|
||||
},
|
||||
{
|
||||
"id": "elixir-pro",
|
||||
"path": "skills/elixir-pro",
|
||||
|
||||
Reference in New Issue
Block a user