packwise-skills 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.cursorrules +23 -23
  2. package/CLAUDE.md +25 -25
  3. package/LICENSE +21 -0
  4. package/README.md +404 -295
  5. package/audit.md +224 -224
  6. package/bin/packwise.js +322 -155
  7. package/install.sh +123 -0
  8. package/package.json +32 -31
  9. package/skill.md +944 -719
  10. package/sub-skills/ai/local-llm.md +183 -183
  11. package/sub-skills/ai/python-ml.md +164 -164
  12. package/sub-skills/backend/go-server.md +184 -184
  13. package/sub-skills/backend/java-spring.md +241 -241
  14. package/sub-skills/backend/node-server.md +164 -164
  15. package/sub-skills/backend/php-laravel.md +175 -175
  16. package/sub-skills/backend/python-server.md +164 -164
  17. package/sub-skills/backend/rust-backend.md +118 -118
  18. package/sub-skills/cli/python-cli.md +236 -236
  19. package/sub-skills/cli/sdk-library.md +497 -497
  20. package/sub-skills/cloud/ci-cd-pipelines.md +350 -350
  21. package/sub-skills/cloud/docker.md +191 -191
  22. package/sub-skills/cloud/kubernetes.md +277 -277
  23. package/sub-skills/cloud/payment-integration.md +307 -307
  24. package/sub-skills/cross-platform/multiplatform.md +252 -252
  25. package/sub-skills/desktop/electron.md +783 -783
  26. package/sub-skills/desktop/game-dev.md +443 -443
  27. package/sub-skills/desktop/native-app.md +123 -123
  28. package/sub-skills/desktop/scenarios.md +443 -443
  29. package/sub-skills/desktop/smart-platforms.md +324 -324
  30. package/sub-skills/desktop/tauri.md +428 -428
  31. package/sub-skills/desktop/vr-ar.md +252 -252
  32. package/sub-skills/desktop/web-to-desktop.md +153 -153
  33. package/sub-skills/embedded/car-infotainment.md +129 -129
  34. package/sub-skills/embedded/esp32.md +184 -184
  35. package/sub-skills/embedded/ros.md +150 -150
  36. package/sub-skills/embedded/stm32.md +160 -160
  37. package/sub-skills/mobile/android.md +322 -322
  38. package/sub-skills/mobile/capacitor.md +232 -232
  39. package/sub-skills/mobile/flutter-mobile.md +138 -138
  40. package/sub-skills/mobile/harmonyos.md +150 -150
  41. package/sub-skills/mobile/ios.md +245 -245
  42. package/sub-skills/mobile/react-native.md +443 -443
  43. package/sub-skills/mobile/wearables.md +230 -230
  44. package/sub-skills/plugins/browser-extension.md +308 -308
  45. package/sub-skills/plugins/jetbrains-plugin.md +226 -226
  46. package/sub-skills/plugins/vscode-extension.md +204 -204
  47. package/sub-skills/security/security-tools.md +174 -174
  48. package/sub-skills/web/monorepo.md +274 -274
  49. package/sub-skills/web/pwa.md +220 -220
  50. package/sub-skills/web/serverless-edge.md +295 -295
  51. package/sub-skills/web/spa.md +266 -266
  52. package/sub-skills/web/ssr.md +228 -228
  53. package/sub-skills/web/wasm.md +243 -243
@@ -1,783 +1,783 @@
1
- # Electron Build Sub-Skill
2
-
3
- Package Web frontend + Node.js backend as a desktop application. Suitable for L1–L3 complexity projects.
4
-
5
- **Current version**: Electron 43.x / electron-builder 26.x / electron-forge 7.x / electron-updater 6.x (2025-2026)
6
-
7
- > **Breaking changes since Electron 35** (docs were written for 35.x):
8
- > - **Node.js 22 -> 24** (Electron 40+): Native module ABI changed. `bytenode` bytecode must be recompiled. CI must use `node-version: '24'`.
9
- > - **`electron` npm package** (Electron 42+): No longer downloads binary via `postinstall`; downloads lazily on first run. `ELECTRON_SKIP_BINARY_DOWNLOAD` removed.
10
- > - **macOS notifications** (Electron 42+): Use `UNNotification` API, **require code signing** to display.
11
- > - **Clipboard in renderer** (Electron 40 deprecated, v44 removed): Must use `contextBridge` preload, NOT direct renderer access.
12
- > - **32-bit platforms** (Electron 44 removes): `win32-ia32` and `linux-armv7l` no longer published.
13
- > - **Linux Wayland default** (Electron 38+): Runs as native Wayland app. Force X11 with `--ozone-platform=x11` if needed.
14
- > - **ASAR Integrity stable** (Electron 39+): Runtime validation of `app.asar` tampering. Recommended for production.
15
- > - See full list at [electron.org/docs/latest/breaking-changes](https://www.electronjs.org/docs/latest/breaking-changes).
16
-
17
- ## When to Use
18
-
19
- - Full Node.js runtime required (Express, native modules, SQLite)
20
- - Existing Web frontend (React/Vue/Svelte/Vanilla)
21
- - File system read/write, database, local storage needed
22
- - Cross-platform support required (Windows/macOS/Linux)
23
-
24
- ## Comparison with Alternatives
25
-
26
- | Feature | Electron 43 | Tauri 2.11 | Neutralinojs |
27
- |---------|------------|-----------|-------------|
28
- | Size | 130–180MB | 3–10MB | ~2MB |
29
- | Backend | Node.js (full) | Rust | C++ WebSocket |
30
- | Native modules | Excellent | Rust crates | Limited |
31
- | Ecosystem | Most mature | Rapidly growing | Smaller |
32
- | Learning curve | Low | High (Rust) | Low |
33
- | Best for | Complex full-stack | Lightweight tools | Minimal wrappers |
34
-
35
- ---
36
-
37
- ## 1. Security Requirements (Non-Negotiable)
38
-
39
- ```javascript
40
- // electron/main.cjs
41
- const { app, BrowserWindow, session, ipcMain, shell } = require('electron');
42
- const path = require('path');
43
-
44
- const win = new BrowserWindow({
45
- webPreferences: {
46
- // === MANDATORY ===
47
- nodeIntegration: false, // NEVER true in production
48
- contextIsolation: true, // ALWAYS true
49
- sandbox: true, // Chromium sandbox
50
- webSecurity: true, // Same-origin policy
51
- allowRunningInsecureContent: false,
52
- // === HARDENING ===
53
- webviewTag: false, // Use BrowserView instead
54
- enableWebSQL: false, // Deprecated
55
- safeDialogs: true, // Prevent dialog spoofing
56
- preload: path.join(__dirname, 'preload.cjs'),
57
- },
58
- });
59
- ```
60
-
61
- ### Content Security Policy (CSP)
62
-
63
- ```javascript
64
- session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
65
- callback({
66
- responseHeaders: {
67
- ...details.responseHeaders,
68
- 'Content-Security-Policy': [
69
- "default-src 'self'; " +
70
- "script-src 'self'; " +
71
- "style-src 'self' 'unsafe-inline'; " +
72
- "img-src 'self' data: https:; " +
73
- "connect-src 'self' https://api.your-app.com; " +
74
- "object-src 'none'; " +
75
- "base-uri 'self'; " +
76
- "form-action 'self'; " +
77
- "frame-ancestors 'none'"
78
- ],
79
- },
80
- });
81
- });
82
- ```
83
-
84
- ### Navigation & Window Guards
85
-
86
- ```javascript
87
- // Block navigation to external URLs
88
- win.webContents.on('will-navigate', (event, url) => {
89
- const parsed = new URL(url);
90
- if (parsed.protocol !== 'file:' && parsed.origin !== 'null') {
91
- event.preventDefault();
92
- }
93
- });
94
-
95
- // Open external links in system browser, not in app
96
- win.webContents.setWindowOpenHandler(({ url }) => {
97
- if (url.startsWith('https:') || url.startsWith('http:')) {
98
- shell.openExternal(url);
99
- }
100
- return { action: 'deny' };
101
- });
102
- ```
103
-
104
- ### Secure IPC (Preload)
105
-
106
- ```javascript
107
- // preload.cjs — Whitelist specific functions, NEVER expose raw ipcRenderer
108
- const { contextBridge, ipcRenderer } = require('electron');
109
-
110
- contextBridge.exposeInMainWorld('electronAPI', {
111
- saveData: (data) => ipcRenderer.invoke('save-data', data),
112
- getVersion: () => ipcRenderer.invoke('get-version'),
113
- onProgress: (callback) => {
114
- const handler = (_event, value) => callback(value);
115
- ipcRenderer.on('progress-update', handler);
116
- return () => ipcRenderer.removeListener('progress-update', handler);
117
- },
118
- // ❌ NEVER do this:
119
- // ipc: ipcRenderer,
120
- // send: (channel, ...args) => ipcRenderer.send(channel, ...args),
121
- });
122
- ```
123
-
124
- ### Permission Handler
125
-
126
- ```javascript
127
- session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
128
- const allowedPermissions = ['clipboard-read'];
129
- callback(allowedPermissions.includes(permission));
130
- });
131
- ```
132
-
133
- ### Security Checklist
134
-
135
- | Item | Required Setting |
136
- |------|-----------------|
137
- | `nodeIntegration` | `false` |
138
- | `contextIsolation` | `true` |
139
- | `sandbox` | `true` |
140
- | CSP header | Restrict `default-src`, `script-src` |
141
- | IPC channels | Whitelist specific channels via `contextBridge` |
142
- | `webviewTag` | `false` (use `BrowserView` instead) |
143
- | `remote` module | Removed in Electron 14+ (never use `@electron/remote`) |
144
- | Navigation | Validate all `will-navigate` / `new-window` events |
145
- | External links | Open in system browser, not in app window |
146
- | Clipboard (v44+) | Access only via `contextBridge` preload, NOT in renderer directly |
147
- | Permissions | Whitelist only needed permissions via `setPermissionRequestHandler` |
148
-
149
- ---
150
-
151
- ## 2. Path Resolution (Critical)
152
-
153
- **Problem**: After packaging, `process.cwd()` points to the install directory. `__dirname` is unavailable in ESM. Database/config must write to `userData` (`%APPDATA%`).
154
-
155
- **Principle**: Separate writable paths (userData) from read-only paths (asar).
156
-
157
- ```typescript
158
- // server/paths.ts
159
- import path from 'path';
160
- import fs from 'fs';
161
-
162
- function resolveAppRoot(): string {
163
- if (process.env.APP_USER_DATA) return process.env.APP_USER_DATA;
164
- const appData = process.env.APPDATA || '';
165
- if (appData) {
166
- const p = path.join(appData, '<APP_NAME>', '<APP_NAME>');
167
- if (fs.existsSync(p)) return p;
168
- }
169
- return process.cwd();
170
- }
171
-
172
- const appRoot = resolveAppRoot();
173
- const asarRoot = __dirname.includes('app.asar')
174
- ? path.dirname(__dirname)
175
- : process.cwd();
176
-
177
- export const CONFIG_DIR = path.join(appRoot, '.<app-name>');
178
- export const DB_PATH = path.join(CONFIG_DIR, 'data.db');
179
- export const SAVES_DIR = path.join(CONFIG_DIR, 'saves');
180
- export const DIST_DIR = path.join(asarRoot, 'dist');
181
- export const PUBLIC_DIR = path.join(asarRoot, 'public');
182
-
183
- export function ensureDirectories(): void {
184
- for (const dir of [CONFIG_DIR, SAVES_DIR]) {
185
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
186
- }
187
- }
188
- ```
189
-
190
- ### Common Path Mistakes
191
-
192
- | Mistake | Consequence | Fix |
193
- |---------|-------------|-----|
194
- | Use `__dirname` to detect Electron | dev mode falls back to cwd, DB in project dir | Use `process.env.APP_USER_DATA` first |
195
- | Use `import.meta.url` in paths.ts | Empty in esbuild CJS output | Use `__dirname` only |
196
- | Use `process.cwd()` as data path | Points to install directory | Never use cwd |
197
- | `process.cwd()` loads HTML | `file://${process.cwd()}/dist/index.html` fails | Use `app.isPackaged` + `process.resourcesPath` |
198
-
199
- ### Correct Asset Path Resolution
200
-
201
- ```javascript
202
- // ❌ WRONG: process.cwd() points to install directory in packaged app
203
- win.loadURL(`file://${process.cwd()}/dist/index.html`);
204
-
205
- // ✅ CORRECT: Use app.isPackaged + process.resourcesPath
206
- const { app } = require('electron');
207
-
208
- function getAssetPath(...segments) {
209
- const basePath = app.isPackaged
210
- ? process.resourcesPath
211
- : path.join(__dirname, '..');
212
- return path.join(basePath, ...segments);
213
- }
214
-
215
- win.loadFile(getAssetPath('dist', 'index.html'));
216
- ```
217
-
218
- ---
219
-
220
- ## 3. Frontend Image Paths
221
-
222
- **Problem**: Vite does not process string literals like `/src/assets/images/...`. Images disappear after build.
223
-
224
- **Recommended: base64 embedding** (anti-theft + guaranteed availability)
225
-
226
- ```typescript
227
- // vite.config.ts
228
- export default defineConfig({
229
- build: { assetsInlineLimit: 1024 * 1024 * 2 },
230
- });
231
-
232
- // Component
233
- const imgs = import.meta.glob<{ default: string }>('/src/assets/*.jpg', { eager: true });
234
- const bg = imgs['/src/assets/bg.jpg']?.default || '';
235
-
236
- // Requires: src/vite-env.d.ts → /// <reference types="vite/client" />
237
- ```
238
-
239
- ---
240
-
241
- ## 4. Server Startup
242
-
243
- **Problem**: `fork()` extracts server.cjs to a temp directory. Native modules (better-sqlite3) live in `app.asar.unpacked/`, not in the temp dir → module resolution fails. **This is the #1 cause of black screen after packaging.**
244
-
245
- **Solution**: `require()` in main process (no fork).
246
-
247
- ```javascript
248
- // electron/main.cjs
249
- const { app } = require('electron');
250
-
251
- function startServer(port, userDataDir) {
252
- return new Promise((resolve, reject) => {
253
- process.env.PORT = String(port);
254
- process.env.NODE_ENV = 'production';
255
- process.env.APP_USER_DATA = userDataDir;
256
-
257
- const serverPath = app.isPackaged
258
- ? path.join(process.resourcesPath, 'app.asar', 'dist', 'server.cjs')
259
- : path.join(__dirname, '..', 'dist', 'server.cjs');
260
-
261
- try {
262
- const server = require(serverPath);
263
- // Poll until server is ready
264
- const http = require('http');
265
- let attempts = 0;
266
- const check = () => {
267
- attempts++;
268
- const req = http.get(`http://127.0.0.1:${port}`, (res) => {
269
- res.resume();
270
- resolve(server);
271
- });
272
- req.on('error', () => {
273
- if (attempts > 30) reject(new Error('Server failed to start'));
274
- else setTimeout(check, 100);
275
- });
276
- req.end();
277
- };
278
- setTimeout(check, 200);
279
- } catch (err) {
280
- reject(err);
281
- }
282
- });
283
- }
284
- ```
285
-
286
- ### Preload Script Path (Must Be Absolute)
287
-
288
- ```javascript
289
- // ❌ WRONG: relative path breaks in packaged app
290
- const win = new BrowserWindow({
291
- webPreferences: { preload: 'preload.js' }
292
- });
293
-
294
- // ✅ CORRECT: absolute path using __dirname (works in main process CJS)
295
- const win = new BrowserWindow({
296
- webPreferences: {
297
- preload: path.join(__dirname, 'preload.cjs'),
298
- }
299
- });
300
- ```
301
-
302
- ---
303
-
304
- ## 5. electron-builder Configuration
305
-
306
- ```yaml
307
- appId: com.<author>.<appname>
308
- productName: <App Name>
309
- copyright: "Copyright (c) <YEAR> <AUTHOR>"
310
-
311
- directories:
312
- output: release
313
- buildResources: build
314
-
315
- asar: true
316
- asarUnpack:
317
- - "node_modules/better-sqlite3/**/*"
318
- - "node_modules/sharp/**/*"
319
- - "node_modules/@img/**/*" # sharp's dependencies
320
- - "**/*.node" # Catch-all for any native addon
321
-
322
- files:
323
- - electron/**/*.cjs
324
- - dist/**/*
325
- - package.json
326
- - "!**/node_modules/*/{CHANGELOG.md,README.md,LICENSE,test,tests,example,examples}"
327
- - "!**/node_modules/.cache/**"
328
- - "!**/*.map"
329
- - "!**/*.tsbuildinfo"
330
-
331
- extraResources:
332
- - from: data-encrypted
333
- to: data
334
- - from: public
335
- to: public
336
-
337
- win:
338
- target: [{ target: nsis, arch: [x64] }] # ⚠️ ia32 (32-bit) removed in Electron 44
339
- icon: build/icon.ico
340
- signingHashAlgorithms: [sha256]
341
- artifactName: "<App>-Setup-${version}.${ext}"
342
-
343
- nsis:
344
- oneClick: false
345
- allowToChangeInstallationDirectory: true
346
- createDesktopShortcut: true
347
- shortcutName: "<App Name>"
348
- license: build/license.txt
349
-
350
- mac:
351
- target: [{ target: dmg, arch: [x64, arm64] }]
352
- icon: build/icon.icns
353
- identity: "Developer ID Application: Your Name (TEAM_ID)"
354
- hardenedRuntime: true
355
- entitlements: build/entitlements.mac.plist
356
- entitlementsInherit: build/entitlements.mac.plist
357
- afterSign: build/notarize.js
358
-
359
- linux:
360
- target: [AppImage, deb]
361
- icon: build/icon.png
362
- ```
363
-
364
- ### Windows Packaging Formats
365
-
366
- | Format | Description | Best For |
367
- |--------|-------------|---------|
368
- | **NSIS** (.exe) | Custom installer wizard | Most universal |
369
- | **MSI** (.msi) | Windows Installer | Enterprise, Group Policy |
370
- | **MSIX** (.msix) | Modern format, containerized | Windows Store, enterprise, clean install/uninstall |
371
- | **Portable** (.zip) | No install needed | Portable apps |
372
- | **Squirrel** | Auto-update framework | Electron native support |
373
-
374
- ### Linux Packaging Formats
375
-
376
- | Format | Description | Limitations |
377
- |--------|-------------|------------|
378
- | **AppImage** | Single-file, no install | Requires FUSE (issues on newer distros without FUSE2) |
379
- | **.deb** | Debian/Ubuntu | Distribution-specific |
380
- | **.rpm** | Fedora/RHEL | Distribution-specific |
381
- | **Flatpak** | Sandboxed, cross-distro | Better for Flathub distribution |
382
- | **Snap** | Ubuntu/Canonical | Centralized Snap Store |
383
-
384
- ---
385
-
386
- ## 6. Source Code Protection (Layered Strategy)
387
-
388
- | Layer | Protection Level | Tool | What It Protects |
389
- |-------|-----------------|------|-----------------|
390
- | ASAR packaging | Low | electron-builder `asar: true` | Casual browsing (trivially extractable) |
391
- | ASAR integrity | Medium | Electron 39+ built-in | Detects tampering |
392
- | JavaScript obfuscation | Medium-High | javascript-obfuscator | Understanding server logic |
393
- | V8 bytecode compilation | High | bytenode | Full source code exposure |
394
- | Config encryption | High (for data at rest) | AES-256-CBC | API keys, secrets |
395
- | Native addon (C/C++) | Very High | Move critical code to .node | Algorithm protection |
396
-
397
- ### Content Encryption (AES-256-CBC)
398
-
399
- ```javascript
400
- // encrypt-content.cjs
401
- const crypto = require('crypto');
402
- const fs = require('fs');
403
-
404
- function encryptFile(inputPath, outputPath, password) {
405
- const data = fs.readFileSync(inputPath, 'utf-8');
406
- const key = crypto.scryptSync(password, 'fixed-salt-v1', 32);
407
- const iv = crypto.randomBytes(16);
408
- const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
409
- const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
410
- fs.writeFileSync(outputPath, iv.toString('hex') + ':' + encrypted.toString('hex'));
411
- }
412
-
413
- function decryptFile(encryptedPath, password) {
414
- const data = fs.readFileSync(encryptedPath, 'utf-8');
415
- const [ivHex, encryptedHex] = data.split(':');
416
- const key = crypto.scryptSync(password, 'fixed-salt-v1', 32);
417
- const decipher = crypto.createDecipheriv('aes-256-cbc', key, Buffer.from(ivHex, 'hex'));
418
- return Buffer.concat([decipher.update(Buffer.from(encryptedHex, 'hex')), decipher.final()]).toString('utf8');
419
- }
420
- ```
421
-
422
- ### Server Obfuscation
423
-
424
- ```javascript
425
- // obfuscate-server.cjs
426
- const JavaScriptObfuscator = require('javascript-obfuscator');
427
- const fs = require('fs');
428
-
429
- function obfuscateFile(filePath) {
430
- const code = fs.readFileSync(filePath, 'utf-8');
431
- let cleaned = code.replace(/\/\/# sourceMappingURL=.+$/gm, '');
432
-
433
- const obfuscated = JavaScriptObfuscator.obfuscate(cleaned, {
434
- compact: true,
435
- controlFlowFlattening: true,
436
- controlFlowFlatteningThreshold: 0.5,
437
- deadCodeInjection: true,
438
- deadCodeInjectionThreshold: 0.2,
439
- stringArray: true,
440
- stringArrayEncoding: ['rc4'],
441
- stringArrayThreshold: 0.75,
442
- transformObjectKeys: true,
443
- // Do NOT enable selfDefending or debugProtection for server code
444
- }).getObfuscatedCode();
445
-
446
- fs.writeFileSync(filePath, obfuscated, 'utf-8');
447
- }
448
- ```
449
-
450
- ### V8 Bytecode Compilation (Strongest JS Protection)
451
-
452
- ```javascript
453
- // compile-bytecode.cjs
454
- const bytenode = require('bytenode');
455
- const fs = require('fs');
456
- const path = require('path');
457
-
458
- // Compile to .jsc (V8 bytecode) — must match Electron's Node.js version
459
- bytenode.compileFile({
460
- filename: path.resolve('dist/server.cjs'),
461
- output: path.resolve('dist/server.jsc'),
462
- compileAsModule: true,
463
- });
464
-
465
- // Replace original with tiny loader
466
- fs.writeFileSync('dist/server.cjs', `
467
- const bytenode = require('bytenode');
468
- const path = require('path');
469
- module.exports = bytenode.require(path.join(__dirname, 'server.jsc'));
470
- `);
471
- ```
472
-
473
- ---
474
-
475
- ## 7. Code Signing
476
-
477
- ### Windows
478
-
479
- | Certificate Type | Cost | SmartScreen Trust | Best For |
480
- |-----------------|------|-------------------|---------|
481
- | EV (Extended Validation) | $200-500/year | Instant trust | Production apps |
482
- | OV (Organization Validation) | $60-200/year | Gradual trust (after many installs) | Small teams |
483
- | Self-signed | Free | Big red warning | Development only |
484
-
485
- ### macOS (Mandatory for Gatekeeper)
486
-
487
- ```javascript
488
- // build/notarize.js
489
- const { notarize } = require('@electron/notarize');
490
-
491
- exports.default = async function (context) {
492
- if (context.electronPlatformName !== 'darwin') return;
493
- const appName = context.packager.appInfo.productFilename;
494
- await notarize({
495
- appBundleId: 'com.yourcompany.yourapp',
496
- appPath: `${context.appOutDir}/${appName}.app`,
497
- appleId: process.env.APPLE_ID,
498
- appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
499
- teamId: process.env.APPLE_TEAM_ID,
500
- tool: 'notarytool',
501
- });
502
- };
503
- ```
504
-
505
- ```xml
506
- <!-- build/entitlements.mac.plist -->
507
- <plist version="1.0">
508
- <dict>
509
- <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
510
- <true/>
511
- <key>com.apple.security.cs.allow-jit</key>
512
- <true/>
513
- <key>com.apple.security.cs.disable-library-validation</key>
514
- <true/>
515
- <key>com.apple.security.network.client</key>
516
- <true/>
517
- </dict>
518
- </plist>
519
- ```
520
-
521
- ### Linux
522
-
523
- ```bash
524
- # GPG signing for AppImage
525
- export GPG_KEY_NAME="your-key-id"
526
- npx electron-builder --linux AppImage
527
- ```
528
-
529
- ---
530
-
531
- ## 8. Auto-Update
532
-
533
- ```javascript
534
- // electron/auto-update.cjs
535
- const { autoUpdater } = require('electron-updater');
536
- const { app, dialog, BrowserWindow } = require('electron');
537
- const log = require('electron-log');
538
-
539
- autoUpdater.logger = log;
540
- autoUpdater.autoDownload = false; // Prompt user first
541
- autoUpdater.autoInstallOnAppQuit = true;
542
-
543
- function setupAutoUpdate() {
544
- autoUpdater.on('update-available', (info) => {
545
- dialog.showMessageBox({
546
- type: 'info',
547
- title: 'Update Available',
548
- message: `Version ${info.version} is available. Download now?`,
549
- buttons: ['Download', 'Later'],
550
- defaultId: 0,
551
- }).then(({ response }) => {
552
- if (response === 0) autoUpdater.downloadUpdate();
553
- });
554
- });
555
-
556
- autoUpdater.on('download-progress', (progress) => {
557
- const win = BrowserWindow.getAllWindows()[0];
558
- if (win) win.setProgressBar(progress.percent / 100);
559
- });
560
-
561
- autoUpdater.on('update-downloaded', (info) => {
562
- dialog.showMessageBox({
563
- type: 'info',
564
- title: 'Update Ready',
565
- message: `Version ${info.version} downloaded. Restart to apply?`,
566
- buttons: ['Restart', 'Later'],
567
- }).then(({ response }) => {
568
- if (response === 0) autoUpdater.quitAndInstall(false, true);
569
- });
570
- });
571
-
572
- autoUpdater.on('error', (err) => {
573
- log.error('Auto-update error:', err);
574
- // Don't show error dialogs — update failure is not critical
575
- });
576
-
577
- if (app.isPackaged) {
578
- setTimeout(() => autoUpdater.checkForUpdates(), 5000);
579
- setInterval(() => autoUpdater.checkForUpdates(), 4 * 60 * 60 * 1000);
580
- }
581
- }
582
-
583
- module.exports = { setupAutoUpdate };
584
- ```
585
-
586
- ```yaml
587
- # electron-builder.yml
588
- publish:
589
- provider: github
590
- owner: your-org
591
- repo: your-app
592
- ```
593
-
594
- **Code signing requirements for auto-update:**
595
-
596
- | Platform | Requirement | Why |
597
- |----------|------------|-----|
598
- | macOS | **Mandatory** — Developer ID + notarization | autoUpdater throws without valid signature |
599
- | Windows | OV/EV certificate (recommended) | Without: SmartScreen warning |
600
- | Linux | GPG signing (optional) | AppImage updates work with GPG |
601
-
602
- ---
603
-
604
- ## 9. Native Module Packaging
605
-
606
- ```yaml
607
- # electron-builder.yml
608
- asar: true
609
- asarUnpack:
610
- - "node_modules/better-sqlite3/**/*"
611
- - "node_modules/sharp/**/*"
612
- - "node_modules/@img/**/*"
613
- - "**/*.node"
614
- ```
615
-
616
- ```bash
617
- # ALWAYS rebuild native modules against Electron's Node.js version
618
- npx electron-rebuild -f -w better-sqlite3,sharp
619
- # Or use postinstall hook:
620
- # "postinstall": "electron-rebuild" in package.json
621
- ```
622
-
623
- ```javascript
624
- // Native module path resolution in packaged app
625
- const Database = require(
626
- app.isPackaged
627
- ? path.join(process.resourcesPath, 'app.asar.unpacked', 'node_modules', 'better-sqlite3')
628
- : path.join(__dirname, '..', 'node_modules', 'better-sqlite3')
629
- );
630
- ```
631
-
632
- ---
633
-
634
- ## 10. Memory Optimization
635
-
636
- ### Use utilityProcess Instead of Hidden BrowserWindow
637
-
638
- ```javascript
639
- // ❌ WRONG: Hidden BrowserWindow for background work (~30-60MB per window)
640
- const bgWin = new BrowserWindow({ show: false });
641
-
642
- // ✅ CORRECT: Utility process (no renderer, no GPU, minimal memory)
643
- const { utilityProcess } = require('electron');
644
- const worker = utilityProcess.fork(path.join(__dirname, 'workers/processor.cjs'));
645
- ```
646
-
647
- ### Singleton Pattern for Windows
648
-
649
- ```javascript
650
- let settingsWindow = null;
651
- function openSettings() {
652
- if (settingsWindow && !settingsWindow.isDestroyed()) {
653
- settingsWindow.focus();
654
- return;
655
- }
656
- settingsWindow = new BrowserWindow({ /* ... */ });
657
- settingsWindow.loadFile('settings.html');
658
- settingsWindow.on('closed', () => { settingsWindow = null; });
659
- }
660
- ```
661
-
662
- ### Chromium Memory Flags
663
-
664
- ```javascript
665
- app.commandLine.appendSwitch('js-flags', '--max-old-space-size=512');
666
- app.commandLine.appendSwitch('disk-cache-size', String(50 * 1024 * 1024)); // 50MB
667
- app.commandLine.appendSwitch('max-active-webgl-contexts', '2');
668
- app.commandLine.appendSwitch('renderer-process-limit', '4');
669
- ```
670
-
671
- ---
672
-
673
- ## 11. Cross-Platform CI/CD
674
-
675
- ```yaml
676
- # .github/workflows/release.yml
677
- name: Release
678
- on:
679
- push:
680
- tags: ['v*']
681
- jobs:
682
- build:
683
- strategy:
684
- matrix:
685
- include:
686
- - os: windows-latest
687
- platform: win
688
- - os: macos-latest
689
- platform: mac
690
- - os: ubuntu-latest
691
- platform: linux
692
- runs-on: ${{ matrix.os }}
693
- steps:
694
- - uses: actions/checkout@v4
695
- - uses: actions/setup-node@v4
696
- with:
697
- node-version: '24'
698
- - run: npm ci
699
- - run: npm run build
700
- - name: macOS signing
701
- if: matrix.platform == 'mac'
702
- uses: apple-actions/import-codesign-certs@v3
703
- with:
704
- p12-file-base64: ${{ secrets.MAC_CERTIFICATE_P12 }}
705
- p12-password: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
706
- - name: Build & Publish
707
- run: npx electron-builder --${{ matrix.platform }} --publish always
708
- env:
709
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
710
- APPLE_ID: ${{ secrets.APPLE_ID }}
711
- APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
712
- APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
713
- CSC_LINK: ${{ secrets.WIN_CERTIFICATE }}
714
- CSC_KEY_PASSWORD: ${{ secrets.WIN_CERTIFICATE_PASSWORD }}
715
- ```
716
-
717
- ---
718
-
719
- ## 12. Complete Build Pipeline
720
-
721
- ```bash
722
- # 1. TypeScript check
723
- npx tsc --noEmit
724
-
725
- # 2. Encrypt sensitive content (if user chose encryption)
726
- node encrypt-content.cjs
727
-
728
- # 3. Build frontend
729
- npx vite build
730
-
731
- # 4. Bundle server
732
- npx esbuild server/index.ts \
733
- --bundle --platform=node --format=cjs \
734
- --packages=external --sourcemap --outfile=dist/server.cjs
735
-
736
- # 5. Obfuscate server code (if user chose obfuscation)
737
- node obfuscate-server.cjs
738
-
739
- # 5b. Delete source maps (SECURITY: prevents unobfuscated source leak)
740
- rm -f dist/*.map
741
-
742
- # 6. Compile to bytecode (if user chose bytecode protection)
743
- node compile-bytecode.cjs
744
-
745
- # 7. Validate syntax
746
- node -c dist/server.cjs
747
-
748
- # 8. Credential leak check
749
- grep -c "sk-\|token\|secret\|password" dist/server.cjs dist/assets/*.js
750
-
751
- # 9. Rebuild native modules for Electron
752
- npx electron-rebuild
753
-
754
- # 10. Build Electron
755
- npx electron-builder --win --publish never
756
-
757
- # 11. Test the packaged app (CRITICAL — always test on clean machine)
758
- ```
759
-
760
- ---
761
-
762
- ## 13. Common Pitfalls
763
-
764
- | Issue | Cause | Fix |
765
- |-------|-------|-----|
766
- | **Black screen** | Wrong path resolution or fork() + ASAR | Use `app.isPackaged` + `process.resourcesPath`; use `require()` not `fork()` |
767
- | `NODE_MODULE_VERSION` mismatch | Native modules built for system Node, not Electron | Run `electron-rebuild` |
768
- | SmartScreen warning | No code signing or self-signed | Use EV certificate |
769
- | macOS "damaged" error | Not notarized | Add `afterSign: build/notarize.js` |
770
- | Auto-update not working (macOS) | No code signing | macOS requires Developer ID + notarization |
771
- | Memory usage too high | Hidden BrowserWindows | Use `utilityProcess` |
772
- | ASAR integrity error | Modified ASAR after signing | Don't modify ASAR after build |
773
- | Source map leak | .map files in package | `rm -f dist/*.map` after obfuscation |
774
- | `Unexpected token ','` | Obfuscator regex breaks template literals | Don't regex-remove console.log |
775
- | `selfDefending` infinite loop | javascript-obfuscator feature breaks on file move | Never enable `selfDefending` for server code |
776
- | Database "not created" | paths.ts falls back to cwd | Use `process.env.APP_USER_DATA` |
777
- | `__dirname not defined` | ESM module | Use `resolveAppRoot()` with env vars |
778
- | `import.meta` CJS warning | esbuild CJS output | Don't use import.meta in paths.ts |
779
- | Images not displaying | Vite doesn't process string literal paths | Use `import.meta.glob` + embed |
780
- | Windows EPERM | win-unpacked locked | Build to temp directory |
781
- | Clipboard not working (v44+) | clipboard module removed from renderer | Use `contextBridge` in preload |
782
- | macOS notifications missing (v42+) | UNNotification API requires code signing | Sign app with Developer ID + notarize |
783
- | `electron` not installing (v42+) | No longer auto-downloads via postinstall | Run `npx electron install` manually |
1
+ # Electron Build Sub-Skill
2
+
3
+ Package Web frontend + Node.js backend as a desktop application. Suitable for L1–L3 complexity projects.
4
+
5
+ **Current version**: Electron 43.x / electron-builder 26.x / electron-forge 7.x / electron-updater 6.x (2025-2026)
6
+
7
+ > **Breaking changes since Electron 35** (docs were written for 35.x):
8
+ > - **Node.js 22 -> 24** (Electron 40+): Native module ABI changed. `bytenode` bytecode must be recompiled. CI must use `node-version: '24'`.
9
+ > - **`electron` npm package** (Electron 42+): No longer downloads binary via `postinstall`; downloads lazily on first run. `ELECTRON_SKIP_BINARY_DOWNLOAD` removed.
10
+ > - **macOS notifications** (Electron 42+): Use `UNNotification` API, **require code signing** to display.
11
+ > - **Clipboard in renderer** (Electron 40 deprecated, v44 removed): Must use `contextBridge` preload, NOT direct renderer access.
12
+ > - **32-bit platforms** (Electron 44 removes): `win32-ia32` and `linux-armv7l` no longer published.
13
+ > - **Linux Wayland default** (Electron 38+): Runs as native Wayland app. Force X11 with `--ozone-platform=x11` if needed.
14
+ > - **ASAR Integrity stable** (Electron 39+): Runtime validation of `app.asar` tampering. Recommended for production.
15
+ > - See full list at [electron.org/docs/latest/breaking-changes](https://www.electronjs.org/docs/latest/breaking-changes).
16
+
17
+ ## When to Use
18
+
19
+ - Full Node.js runtime required (Express, native modules, SQLite)
20
+ - Existing Web frontend (React/Vue/Svelte/Vanilla)
21
+ - File system read/write, database, local storage needed
22
+ - Cross-platform support required (Windows/macOS/Linux)
23
+
24
+ ## Comparison with Alternatives
25
+
26
+ | Feature | Electron 43 | Tauri 2.11 | Neutralinojs |
27
+ |---------|------------|-----------|-------------|
28
+ | Size | 130–180MB | 3–10MB | ~2MB |
29
+ | Backend | Node.js (full) | Rust | C++ WebSocket |
30
+ | Native modules | Excellent | Rust crates | Limited |
31
+ | Ecosystem | Most mature | Rapidly growing | Smaller |
32
+ | Learning curve | Low | High (Rust) | Low |
33
+ | Best for | Complex full-stack | Lightweight tools | Minimal wrappers |
34
+
35
+ ---
36
+
37
+ ## 1. Security Requirements (Non-Negotiable)
38
+
39
+ ```javascript
40
+ // electron/main.cjs
41
+ const { app, BrowserWindow, session, ipcMain, shell } = require('electron');
42
+ const path = require('path');
43
+
44
+ const win = new BrowserWindow({
45
+ webPreferences: {
46
+ // === MANDATORY ===
47
+ nodeIntegration: false, // NEVER true in production
48
+ contextIsolation: true, // ALWAYS true
49
+ sandbox: true, // Chromium sandbox
50
+ webSecurity: true, // Same-origin policy
51
+ allowRunningInsecureContent: false,
52
+ // === HARDENING ===
53
+ webviewTag: false, // Use BrowserView instead
54
+ enableWebSQL: false, // Deprecated
55
+ safeDialogs: true, // Prevent dialog spoofing
56
+ preload: path.join(__dirname, 'preload.cjs'),
57
+ },
58
+ });
59
+ ```
60
+
61
+ ### Content Security Policy (CSP)
62
+
63
+ ```javascript
64
+ session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
65
+ callback({
66
+ responseHeaders: {
67
+ ...details.responseHeaders,
68
+ 'Content-Security-Policy': [
69
+ "default-src 'self'; " +
70
+ "script-src 'self'; " +
71
+ "style-src 'self' 'unsafe-inline'; " +
72
+ "img-src 'self' data: https:; " +
73
+ "connect-src 'self' https://api.your-app.com; " +
74
+ "object-src 'none'; " +
75
+ "base-uri 'self'; " +
76
+ "form-action 'self'; " +
77
+ "frame-ancestors 'none'"
78
+ ],
79
+ },
80
+ });
81
+ });
82
+ ```
83
+
84
+ ### Navigation & Window Guards
85
+
86
+ ```javascript
87
+ // Block navigation to external URLs
88
+ win.webContents.on('will-navigate', (event, url) => {
89
+ const parsed = new URL(url);
90
+ if (parsed.protocol !== 'file:' && parsed.origin !== 'null') {
91
+ event.preventDefault();
92
+ }
93
+ });
94
+
95
+ // Open external links in system browser, not in app
96
+ win.webContents.setWindowOpenHandler(({ url }) => {
97
+ if (url.startsWith('https:') || url.startsWith('http:')) {
98
+ shell.openExternal(url);
99
+ }
100
+ return { action: 'deny' };
101
+ });
102
+ ```
103
+
104
+ ### Secure IPC (Preload)
105
+
106
+ ```javascript
107
+ // preload.cjs — Whitelist specific functions, NEVER expose raw ipcRenderer
108
+ const { contextBridge, ipcRenderer } = require('electron');
109
+
110
+ contextBridge.exposeInMainWorld('electronAPI', {
111
+ saveData: (data) => ipcRenderer.invoke('save-data', data),
112
+ getVersion: () => ipcRenderer.invoke('get-version'),
113
+ onProgress: (callback) => {
114
+ const handler = (_event, value) => callback(value);
115
+ ipcRenderer.on('progress-update', handler);
116
+ return () => ipcRenderer.removeListener('progress-update', handler);
117
+ },
118
+ // ❌ NEVER do this:
119
+ // ipc: ipcRenderer,
120
+ // send: (channel, ...args) => ipcRenderer.send(channel, ...args),
121
+ });
122
+ ```
123
+
124
+ ### Permission Handler
125
+
126
+ ```javascript
127
+ session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
128
+ const allowedPermissions = ['clipboard-read'];
129
+ callback(allowedPermissions.includes(permission));
130
+ });
131
+ ```
132
+
133
+ ### Security Checklist
134
+
135
+ | Item | Required Setting |
136
+ |------|-----------------|
137
+ | `nodeIntegration` | `false` |
138
+ | `contextIsolation` | `true` |
139
+ | `sandbox` | `true` |
140
+ | CSP header | Restrict `default-src`, `script-src` |
141
+ | IPC channels | Whitelist specific channels via `contextBridge` |
142
+ | `webviewTag` | `false` (use `BrowserView` instead) |
143
+ | `remote` module | Removed in Electron 14+ (never use `@electron/remote`) |
144
+ | Navigation | Validate all `will-navigate` / `new-window` events |
145
+ | External links | Open in system browser, not in app window |
146
+ | Clipboard (v44+) | Access only via `contextBridge` preload, NOT in renderer directly |
147
+ | Permissions | Whitelist only needed permissions via `setPermissionRequestHandler` |
148
+
149
+ ---
150
+
151
+ ## 2. Path Resolution (Critical)
152
+
153
+ **Problem**: After packaging, `process.cwd()` points to the install directory. `__dirname` is unavailable in ESM. Database/config must write to `userData` (`%APPDATA%`).
154
+
155
+ **Principle**: Separate writable paths (userData) from read-only paths (asar).
156
+
157
+ ```typescript
158
+ // server/paths.ts
159
+ import path from 'path';
160
+ import fs from 'fs';
161
+
162
+ function resolveAppRoot(): string {
163
+ if (process.env.APP_USER_DATA) return process.env.APP_USER_DATA;
164
+ const appData = process.env.APPDATA || '';
165
+ if (appData) {
166
+ const p = path.join(appData, '<APP_NAME>', '<APP_NAME>');
167
+ if (fs.existsSync(p)) return p;
168
+ }
169
+ return process.cwd();
170
+ }
171
+
172
+ const appRoot = resolveAppRoot();
173
+ const asarRoot = __dirname.includes('app.asar')
174
+ ? path.dirname(__dirname)
175
+ : process.cwd();
176
+
177
+ export const CONFIG_DIR = path.join(appRoot, '.<app-name>');
178
+ export const DB_PATH = path.join(CONFIG_DIR, 'data.db');
179
+ export const SAVES_DIR = path.join(CONFIG_DIR, 'saves');
180
+ export const DIST_DIR = path.join(asarRoot, 'dist');
181
+ export const PUBLIC_DIR = path.join(asarRoot, 'public');
182
+
183
+ export function ensureDirectories(): void {
184
+ for (const dir of [CONFIG_DIR, SAVES_DIR]) {
185
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
186
+ }
187
+ }
188
+ ```
189
+
190
+ ### Common Path Mistakes
191
+
192
+ | Mistake | Consequence | Fix |
193
+ |---------|-------------|-----|
194
+ | Use `__dirname` to detect Electron | dev mode falls back to cwd, DB in project dir | Use `process.env.APP_USER_DATA` first |
195
+ | Use `import.meta.url` in paths.ts | Empty in esbuild CJS output | Use `__dirname` only |
196
+ | Use `process.cwd()` as data path | Points to install directory | Never use cwd |
197
+ | `process.cwd()` loads HTML | `file://${process.cwd()}/dist/index.html` fails | Use `app.isPackaged` + `process.resourcesPath` |
198
+
199
+ ### Correct Asset Path Resolution
200
+
201
+ ```javascript
202
+ // ❌ WRONG: process.cwd() points to install directory in packaged app
203
+ win.loadURL(`file://${process.cwd()}/dist/index.html`);
204
+
205
+ // ✅ CORRECT: Use app.isPackaged + process.resourcesPath
206
+ const { app } = require('electron');
207
+
208
+ function getAssetPath(...segments) {
209
+ const basePath = app.isPackaged
210
+ ? process.resourcesPath
211
+ : path.join(__dirname, '..');
212
+ return path.join(basePath, ...segments);
213
+ }
214
+
215
+ win.loadFile(getAssetPath('dist', 'index.html'));
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 3. Frontend Image Paths
221
+
222
+ **Problem**: Vite does not process string literals like `/src/assets/images/...`. Images disappear after build.
223
+
224
+ **Recommended: base64 embedding** (anti-theft + guaranteed availability)
225
+
226
+ ```typescript
227
+ // vite.config.ts
228
+ export default defineConfig({
229
+ build: { assetsInlineLimit: 1024 * 1024 * 2 },
230
+ });
231
+
232
+ // Component
233
+ const imgs = import.meta.glob<{ default: string }>('/src/assets/*.jpg', { eager: true });
234
+ const bg = imgs['/src/assets/bg.jpg']?.default || '';
235
+
236
+ // Requires: src/vite-env.d.ts → /// <reference types="vite/client" />
237
+ ```
238
+
239
+ ---
240
+
241
+ ## 4. Server Startup
242
+
243
+ **Problem**: `fork()` extracts server.cjs to a temp directory. Native modules (better-sqlite3) live in `app.asar.unpacked/`, not in the temp dir → module resolution fails. **This is the #1 cause of black screen after packaging.**
244
+
245
+ **Solution**: `require()` in main process (no fork).
246
+
247
+ ```javascript
248
+ // electron/main.cjs
249
+ const { app } = require('electron');
250
+
251
+ function startServer(port, userDataDir) {
252
+ return new Promise((resolve, reject) => {
253
+ process.env.PORT = String(port);
254
+ process.env.NODE_ENV = 'production';
255
+ process.env.APP_USER_DATA = userDataDir;
256
+
257
+ const serverPath = app.isPackaged
258
+ ? path.join(process.resourcesPath, 'app.asar', 'dist', 'server.cjs')
259
+ : path.join(__dirname, '..', 'dist', 'server.cjs');
260
+
261
+ try {
262
+ const server = require(serverPath);
263
+ // Poll until server is ready
264
+ const http = require('http');
265
+ let attempts = 0;
266
+ const check = () => {
267
+ attempts++;
268
+ const req = http.get(`http://127.0.0.1:${port}`, (res) => {
269
+ res.resume();
270
+ resolve(server);
271
+ });
272
+ req.on('error', () => {
273
+ if (attempts > 30) reject(new Error('Server failed to start'));
274
+ else setTimeout(check, 100);
275
+ });
276
+ req.end();
277
+ };
278
+ setTimeout(check, 200);
279
+ } catch (err) {
280
+ reject(err);
281
+ }
282
+ });
283
+ }
284
+ ```
285
+
286
+ ### Preload Script Path (Must Be Absolute)
287
+
288
+ ```javascript
289
+ // ❌ WRONG: relative path breaks in packaged app
290
+ const win = new BrowserWindow({
291
+ webPreferences: { preload: 'preload.js' }
292
+ });
293
+
294
+ // ✅ CORRECT: absolute path using __dirname (works in main process CJS)
295
+ const win = new BrowserWindow({
296
+ webPreferences: {
297
+ preload: path.join(__dirname, 'preload.cjs'),
298
+ }
299
+ });
300
+ ```
301
+
302
+ ---
303
+
304
+ ## 5. electron-builder Configuration
305
+
306
+ ```yaml
307
+ appId: com.<author>.<appname>
308
+ productName: <App Name>
309
+ copyright: "Copyright (c) <YEAR> <AUTHOR>"
310
+
311
+ directories:
312
+ output: release
313
+ buildResources: build
314
+
315
+ asar: true
316
+ asarUnpack:
317
+ - "node_modules/better-sqlite3/**/*"
318
+ - "node_modules/sharp/**/*"
319
+ - "node_modules/@img/**/*" # sharp's dependencies
320
+ - "**/*.node" # Catch-all for any native addon
321
+
322
+ files:
323
+ - electron/**/*.cjs
324
+ - dist/**/*
325
+ - package.json
326
+ - "!**/node_modules/*/{CHANGELOG.md,README.md,LICENSE,test,tests,example,examples}"
327
+ - "!**/node_modules/.cache/**"
328
+ - "!**/*.map"
329
+ - "!**/*.tsbuildinfo"
330
+
331
+ extraResources:
332
+ - from: data-encrypted
333
+ to: data
334
+ - from: public
335
+ to: public
336
+
337
+ win:
338
+ target: [{ target: nsis, arch: [x64] }] # ⚠️ ia32 (32-bit) removed in Electron 44
339
+ icon: build/icon.ico
340
+ signingHashAlgorithms: [sha256]
341
+ artifactName: "<App>-Setup-${version}.${ext}"
342
+
343
+ nsis:
344
+ oneClick: false
345
+ allowToChangeInstallationDirectory: true
346
+ createDesktopShortcut: true
347
+ shortcutName: "<App Name>"
348
+ license: build/license.txt
349
+
350
+ mac:
351
+ target: [{ target: dmg, arch: [x64, arm64] }]
352
+ icon: build/icon.icns
353
+ identity: "Developer ID Application: Your Name (TEAM_ID)"
354
+ hardenedRuntime: true
355
+ entitlements: build/entitlements.mac.plist
356
+ entitlementsInherit: build/entitlements.mac.plist
357
+ afterSign: build/notarize.js
358
+
359
+ linux:
360
+ target: [AppImage, deb]
361
+ icon: build/icon.png
362
+ ```
363
+
364
+ ### Windows Packaging Formats
365
+
366
+ | Format | Description | Best For |
367
+ |--------|-------------|---------|
368
+ | **NSIS** (.exe) | Custom installer wizard | Most universal |
369
+ | **MSI** (.msi) | Windows Installer | Enterprise, Group Policy |
370
+ | **MSIX** (.msix) | Modern format, containerized | Windows Store, enterprise, clean install/uninstall |
371
+ | **Portable** (.zip) | No install needed | Portable apps |
372
+ | **Squirrel** | Auto-update framework | Electron native support |
373
+
374
+ ### Linux Packaging Formats
375
+
376
+ | Format | Description | Limitations |
377
+ |--------|-------------|------------|
378
+ | **AppImage** | Single-file, no install | Requires FUSE (issues on newer distros without FUSE2) |
379
+ | **.deb** | Debian/Ubuntu | Distribution-specific |
380
+ | **.rpm** | Fedora/RHEL | Distribution-specific |
381
+ | **Flatpak** | Sandboxed, cross-distro | Better for Flathub distribution |
382
+ | **Snap** | Ubuntu/Canonical | Centralized Snap Store |
383
+
384
+ ---
385
+
386
+ ## 6. Source Code Protection (Layered Strategy)
387
+
388
+ | Layer | Protection Level | Tool | What It Protects |
389
+ |-------|-----------------|------|-----------------|
390
+ | ASAR packaging | Low | electron-builder `asar: true` | Casual browsing (trivially extractable) |
391
+ | ASAR integrity | Medium | Electron 39+ built-in | Detects tampering |
392
+ | JavaScript obfuscation | Medium-High | javascript-obfuscator | Understanding server logic |
393
+ | V8 bytecode compilation | High | bytenode | Full source code exposure |
394
+ | Config encryption | High (for data at rest) | AES-256-CBC | API keys, secrets |
395
+ | Native addon (C/C++) | Very High | Move critical code to .node | Algorithm protection |
396
+
397
+ ### Content Encryption (AES-256-CBC)
398
+
399
+ ```javascript
400
+ // encrypt-content.cjs
401
+ const crypto = require('crypto');
402
+ const fs = require('fs');
403
+
404
+ function encryptFile(inputPath, outputPath, password) {
405
+ const data = fs.readFileSync(inputPath, 'utf-8');
406
+ const key = crypto.scryptSync(password, 'fixed-salt-v1', 32);
407
+ const iv = crypto.randomBytes(16);
408
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
409
+ const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
410
+ fs.writeFileSync(outputPath, iv.toString('hex') + ':' + encrypted.toString('hex'));
411
+ }
412
+
413
+ function decryptFile(encryptedPath, password) {
414
+ const data = fs.readFileSync(encryptedPath, 'utf-8');
415
+ const [ivHex, encryptedHex] = data.split(':');
416
+ const key = crypto.scryptSync(password, 'fixed-salt-v1', 32);
417
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key, Buffer.from(ivHex, 'hex'));
418
+ return Buffer.concat([decipher.update(Buffer.from(encryptedHex, 'hex')), decipher.final()]).toString('utf8');
419
+ }
420
+ ```
421
+
422
+ ### Server Obfuscation
423
+
424
+ ```javascript
425
+ // obfuscate-server.cjs
426
+ const JavaScriptObfuscator = require('javascript-obfuscator');
427
+ const fs = require('fs');
428
+
429
+ function obfuscateFile(filePath) {
430
+ const code = fs.readFileSync(filePath, 'utf-8');
431
+ let cleaned = code.replace(/\/\/# sourceMappingURL=.+$/gm, '');
432
+
433
+ const obfuscated = JavaScriptObfuscator.obfuscate(cleaned, {
434
+ compact: true,
435
+ controlFlowFlattening: true,
436
+ controlFlowFlatteningThreshold: 0.5,
437
+ deadCodeInjection: true,
438
+ deadCodeInjectionThreshold: 0.2,
439
+ stringArray: true,
440
+ stringArrayEncoding: ['rc4'],
441
+ stringArrayThreshold: 0.75,
442
+ transformObjectKeys: true,
443
+ // Do NOT enable selfDefending or debugProtection for server code
444
+ }).getObfuscatedCode();
445
+
446
+ fs.writeFileSync(filePath, obfuscated, 'utf-8');
447
+ }
448
+ ```
449
+
450
+ ### V8 Bytecode Compilation (Strongest JS Protection)
451
+
452
+ ```javascript
453
+ // compile-bytecode.cjs
454
+ const bytenode = require('bytenode');
455
+ const fs = require('fs');
456
+ const path = require('path');
457
+
458
+ // Compile to .jsc (V8 bytecode) — must match Electron's Node.js version
459
+ bytenode.compileFile({
460
+ filename: path.resolve('dist/server.cjs'),
461
+ output: path.resolve('dist/server.jsc'),
462
+ compileAsModule: true,
463
+ });
464
+
465
+ // Replace original with tiny loader
466
+ fs.writeFileSync('dist/server.cjs', `
467
+ const bytenode = require('bytenode');
468
+ const path = require('path');
469
+ module.exports = bytenode.require(path.join(__dirname, 'server.jsc'));
470
+ `);
471
+ ```
472
+
473
+ ---
474
+
475
+ ## 7. Code Signing
476
+
477
+ ### Windows
478
+
479
+ | Certificate Type | Cost | SmartScreen Trust | Best For |
480
+ |-----------------|------|-------------------|---------|
481
+ | EV (Extended Validation) | $200-500/year | Instant trust | Production apps |
482
+ | OV (Organization Validation) | $60-200/year | Gradual trust (after many installs) | Small teams |
483
+ | Self-signed | Free | Big red warning | Development only |
484
+
485
+ ### macOS (Mandatory for Gatekeeper)
486
+
487
+ ```javascript
488
+ // build/notarize.js
489
+ const { notarize } = require('@electron/notarize');
490
+
491
+ exports.default = async function (context) {
492
+ if (context.electronPlatformName !== 'darwin') return;
493
+ const appName = context.packager.appInfo.productFilename;
494
+ await notarize({
495
+ appBundleId: 'com.yourcompany.yourapp',
496
+ appPath: `${context.appOutDir}/${appName}.app`,
497
+ appleId: process.env.APPLE_ID,
498
+ appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
499
+ teamId: process.env.APPLE_TEAM_ID,
500
+ tool: 'notarytool',
501
+ });
502
+ };
503
+ ```
504
+
505
+ ```xml
506
+ <!-- build/entitlements.mac.plist -->
507
+ <plist version="1.0">
508
+ <dict>
509
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
510
+ <true/>
511
+ <key>com.apple.security.cs.allow-jit</key>
512
+ <true/>
513
+ <key>com.apple.security.cs.disable-library-validation</key>
514
+ <true/>
515
+ <key>com.apple.security.network.client</key>
516
+ <true/>
517
+ </dict>
518
+ </plist>
519
+ ```
520
+
521
+ ### Linux
522
+
523
+ ```bash
524
+ # GPG signing for AppImage
525
+ export GPG_KEY_NAME="your-key-id"
526
+ npx electron-builder --linux AppImage
527
+ ```
528
+
529
+ ---
530
+
531
+ ## 8. Auto-Update
532
+
533
+ ```javascript
534
+ // electron/auto-update.cjs
535
+ const { autoUpdater } = require('electron-updater');
536
+ const { app, dialog, BrowserWindow } = require('electron');
537
+ const log = require('electron-log');
538
+
539
+ autoUpdater.logger = log;
540
+ autoUpdater.autoDownload = false; // Prompt user first
541
+ autoUpdater.autoInstallOnAppQuit = true;
542
+
543
+ function setupAutoUpdate() {
544
+ autoUpdater.on('update-available', (info) => {
545
+ dialog.showMessageBox({
546
+ type: 'info',
547
+ title: 'Update Available',
548
+ message: `Version ${info.version} is available. Download now?`,
549
+ buttons: ['Download', 'Later'],
550
+ defaultId: 0,
551
+ }).then(({ response }) => {
552
+ if (response === 0) autoUpdater.downloadUpdate();
553
+ });
554
+ });
555
+
556
+ autoUpdater.on('download-progress', (progress) => {
557
+ const win = BrowserWindow.getAllWindows()[0];
558
+ if (win) win.setProgressBar(progress.percent / 100);
559
+ });
560
+
561
+ autoUpdater.on('update-downloaded', (info) => {
562
+ dialog.showMessageBox({
563
+ type: 'info',
564
+ title: 'Update Ready',
565
+ message: `Version ${info.version} downloaded. Restart to apply?`,
566
+ buttons: ['Restart', 'Later'],
567
+ }).then(({ response }) => {
568
+ if (response === 0) autoUpdater.quitAndInstall(false, true);
569
+ });
570
+ });
571
+
572
+ autoUpdater.on('error', (err) => {
573
+ log.error('Auto-update error:', err);
574
+ // Don't show error dialogs — update failure is not critical
575
+ });
576
+
577
+ if (app.isPackaged) {
578
+ setTimeout(() => autoUpdater.checkForUpdates(), 5000);
579
+ setInterval(() => autoUpdater.checkForUpdates(), 4 * 60 * 60 * 1000);
580
+ }
581
+ }
582
+
583
+ module.exports = { setupAutoUpdate };
584
+ ```
585
+
586
+ ```yaml
587
+ # electron-builder.yml
588
+ publish:
589
+ provider: github
590
+ owner: your-org
591
+ repo: your-app
592
+ ```
593
+
594
+ **Code signing requirements for auto-update:**
595
+
596
+ | Platform | Requirement | Why |
597
+ |----------|------------|-----|
598
+ | macOS | **Mandatory** — Developer ID + notarization | autoUpdater throws without valid signature |
599
+ | Windows | OV/EV certificate (recommended) | Without: SmartScreen warning |
600
+ | Linux | GPG signing (optional) | AppImage updates work with GPG |
601
+
602
+ ---
603
+
604
+ ## 9. Native Module Packaging
605
+
606
+ ```yaml
607
+ # electron-builder.yml
608
+ asar: true
609
+ asarUnpack:
610
+ - "node_modules/better-sqlite3/**/*"
611
+ - "node_modules/sharp/**/*"
612
+ - "node_modules/@img/**/*"
613
+ - "**/*.node"
614
+ ```
615
+
616
+ ```bash
617
+ # ALWAYS rebuild native modules against Electron's Node.js version
618
+ npx electron-rebuild -f -w better-sqlite3,sharp
619
+ # Or use postinstall hook:
620
+ # "postinstall": "electron-rebuild" in package.json
621
+ ```
622
+
623
+ ```javascript
624
+ // Native module path resolution in packaged app
625
+ const Database = require(
626
+ app.isPackaged
627
+ ? path.join(process.resourcesPath, 'app.asar.unpacked', 'node_modules', 'better-sqlite3')
628
+ : path.join(__dirname, '..', 'node_modules', 'better-sqlite3')
629
+ );
630
+ ```
631
+
632
+ ---
633
+
634
+ ## 10. Memory Optimization
635
+
636
+ ### Use utilityProcess Instead of Hidden BrowserWindow
637
+
638
+ ```javascript
639
+ // ❌ WRONG: Hidden BrowserWindow for background work (~30-60MB per window)
640
+ const bgWin = new BrowserWindow({ show: false });
641
+
642
+ // ✅ CORRECT: Utility process (no renderer, no GPU, minimal memory)
643
+ const { utilityProcess } = require('electron');
644
+ const worker = utilityProcess.fork(path.join(__dirname, 'workers/processor.cjs'));
645
+ ```
646
+
647
+ ### Singleton Pattern for Windows
648
+
649
+ ```javascript
650
+ let settingsWindow = null;
651
+ function openSettings() {
652
+ if (settingsWindow && !settingsWindow.isDestroyed()) {
653
+ settingsWindow.focus();
654
+ return;
655
+ }
656
+ settingsWindow = new BrowserWindow({ /* ... */ });
657
+ settingsWindow.loadFile('settings.html');
658
+ settingsWindow.on('closed', () => { settingsWindow = null; });
659
+ }
660
+ ```
661
+
662
+ ### Chromium Memory Flags
663
+
664
+ ```javascript
665
+ app.commandLine.appendSwitch('js-flags', '--max-old-space-size=512');
666
+ app.commandLine.appendSwitch('disk-cache-size', String(50 * 1024 * 1024)); // 50MB
667
+ app.commandLine.appendSwitch('max-active-webgl-contexts', '2');
668
+ app.commandLine.appendSwitch('renderer-process-limit', '4');
669
+ ```
670
+
671
+ ---
672
+
673
+ ## 11. Cross-Platform CI/CD
674
+
675
+ ```yaml
676
+ # .github/workflows/release.yml
677
+ name: Release
678
+ on:
679
+ push:
680
+ tags: ['v*']
681
+ jobs:
682
+ build:
683
+ strategy:
684
+ matrix:
685
+ include:
686
+ - os: windows-latest
687
+ platform: win
688
+ - os: macos-latest
689
+ platform: mac
690
+ - os: ubuntu-latest
691
+ platform: linux
692
+ runs-on: ${{ matrix.os }}
693
+ steps:
694
+ - uses: actions/checkout@v4
695
+ - uses: actions/setup-node@v4
696
+ with:
697
+ node-version: '24'
698
+ - run: npm ci
699
+ - run: npm run build
700
+ - name: macOS signing
701
+ if: matrix.platform == 'mac'
702
+ uses: apple-actions/import-codesign-certs@v3
703
+ with:
704
+ p12-file-base64: ${{ secrets.MAC_CERTIFICATE_P12 }}
705
+ p12-password: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
706
+ - name: Build & Publish
707
+ run: npx electron-builder --${{ matrix.platform }} --publish always
708
+ env:
709
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
710
+ APPLE_ID: ${{ secrets.APPLE_ID }}
711
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
712
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
713
+ CSC_LINK: ${{ secrets.WIN_CERTIFICATE }}
714
+ CSC_KEY_PASSWORD: ${{ secrets.WIN_CERTIFICATE_PASSWORD }}
715
+ ```
716
+
717
+ ---
718
+
719
+ ## 12. Complete Build Pipeline
720
+
721
+ ```bash
722
+ # 1. TypeScript check
723
+ npx tsc --noEmit
724
+
725
+ # 2. Encrypt sensitive content (if user chose encryption)
726
+ node encrypt-content.cjs
727
+
728
+ # 3. Build frontend
729
+ npx vite build
730
+
731
+ # 4. Bundle server
732
+ npx esbuild server/index.ts \
733
+ --bundle --platform=node --format=cjs \
734
+ --packages=external --sourcemap --outfile=dist/server.cjs
735
+
736
+ # 5. Obfuscate server code (if user chose obfuscation)
737
+ node obfuscate-server.cjs
738
+
739
+ # 5b. Delete source maps (SECURITY: prevents unobfuscated source leak)
740
+ rm -f dist/*.map
741
+
742
+ # 6. Compile to bytecode (if user chose bytecode protection)
743
+ node compile-bytecode.cjs
744
+
745
+ # 7. Validate syntax
746
+ node -c dist/server.cjs
747
+
748
+ # 8. Credential leak check
749
+ grep -c "sk-\|token\|secret\|password" dist/server.cjs dist/assets/*.js
750
+
751
+ # 9. Rebuild native modules for Electron
752
+ npx electron-rebuild
753
+
754
+ # 10. Build Electron
755
+ npx electron-builder --win --publish never
756
+
757
+ # 11. Test the packaged app (CRITICAL — always test on clean machine)
758
+ ```
759
+
760
+ ---
761
+
762
+ ## 13. Common Pitfalls
763
+
764
+ | Issue | Cause | Fix |
765
+ |-------|-------|-----|
766
+ | **Black screen** | Wrong path resolution or fork() + ASAR | Use `app.isPackaged` + `process.resourcesPath`; use `require()` not `fork()` |
767
+ | `NODE_MODULE_VERSION` mismatch | Native modules built for system Node, not Electron | Run `electron-rebuild` |
768
+ | SmartScreen warning | No code signing or self-signed | Use EV certificate |
769
+ | macOS "damaged" error | Not notarized | Add `afterSign: build/notarize.js` |
770
+ | Auto-update not working (macOS) | No code signing | macOS requires Developer ID + notarization |
771
+ | Memory usage too high | Hidden BrowserWindows | Use `utilityProcess` |
772
+ | ASAR integrity error | Modified ASAR after signing | Don't modify ASAR after build |
773
+ | Source map leak | .map files in package | `rm -f dist/*.map` after obfuscation |
774
+ | `Unexpected token ','` | Obfuscator regex breaks template literals | Don't regex-remove console.log |
775
+ | `selfDefending` infinite loop | javascript-obfuscator feature breaks on file move | Never enable `selfDefending` for server code |
776
+ | Database "not created" | paths.ts falls back to cwd | Use `process.env.APP_USER_DATA` |
777
+ | `__dirname not defined` | ESM module | Use `resolveAppRoot()` with env vars |
778
+ | `import.meta` CJS warning | esbuild CJS output | Don't use import.meta in paths.ts |
779
+ | Images not displaying | Vite doesn't process string literal paths | Use `import.meta.glob` + embed |
780
+ | Windows EPERM | win-unpacked locked | Build to temp directory |
781
+ | Clipboard not working (v44+) | clipboard module removed from renderer | Use `contextBridge` in preload |
782
+ | macOS notifications missing (v42+) | UNNotification API requires code signing | Sign app with Developer ID + notarize |
783
+ | `electron` not installing (v42+) | No longer auto-downloads via postinstall | Run `npx electron install` manually |