From 879eb90438db92f94250a4030a96e8e68d0485c2 Mon Sep 17 00:00:00 2001 From: Matheus Messias Date: Fri, 13 Mar 2026 05:34:09 -0300 Subject: [PATCH] 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 --- CATALOG.md | 3 +- data/catalog.json | 24 + skills/electron-development/SKILL.md | 856 +++++++++++++++++++++++++++ skills_index.json | 10 + 4 files changed, 892 insertions(+), 1 deletion(-) create mode 100644 skills/electron-development/SKILL.md diff --git a/CATALOG.md b/CATALOG.md index 2f142a2e..26808311 100644 --- a/CATALOG.md +++ b/CATALOG.md @@ -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 | diff --git a/data/catalog.json b/data/catalog.json index 74e70ed0..831eed9f 100644 --- a/data/catalog.json +++ b/data/catalog.json @@ -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", diff --git a/skills/electron-development/SKILL.md b/skills/electron-development/SKILL.md new file mode 100644 index 00000000..c48430bb --- /dev/null +++ b/skills/electron-development/SKILL.md @@ -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; + 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(key: K): AppState[K] { + return this.data[key]; + } + + set(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 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 diff --git a/skills_index.json b/skills_index.json index 5a9ff10f..4d28c0c9 100644 --- a/skills_index.json +++ b/skills_index.json @@ -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",