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.
- package/.cursorrules +23 -23
- package/CLAUDE.md +25 -25
- package/LICENSE +21 -0
- package/README.md +404 -295
- package/audit.md +224 -224
- package/bin/packwise.js +322 -155
- package/install.sh +123 -0
- package/package.json +32 -31
- package/skill.md +944 -719
- package/sub-skills/ai/local-llm.md +183 -183
- package/sub-skills/ai/python-ml.md +164 -164
- package/sub-skills/backend/go-server.md +184 -184
- package/sub-skills/backend/java-spring.md +241 -241
- package/sub-skills/backend/node-server.md +164 -164
- package/sub-skills/backend/php-laravel.md +175 -175
- package/sub-skills/backend/python-server.md +164 -164
- package/sub-skills/backend/rust-backend.md +118 -118
- package/sub-skills/cli/python-cli.md +236 -236
- package/sub-skills/cli/sdk-library.md +497 -497
- package/sub-skills/cloud/ci-cd-pipelines.md +350 -350
- package/sub-skills/cloud/docker.md +191 -191
- package/sub-skills/cloud/kubernetes.md +277 -277
- package/sub-skills/cloud/payment-integration.md +307 -307
- package/sub-skills/cross-platform/multiplatform.md +252 -252
- package/sub-skills/desktop/electron.md +783 -783
- package/sub-skills/desktop/game-dev.md +443 -443
- package/sub-skills/desktop/native-app.md +123 -123
- package/sub-skills/desktop/scenarios.md +443 -443
- package/sub-skills/desktop/smart-platforms.md +324 -324
- package/sub-skills/desktop/tauri.md +428 -428
- package/sub-skills/desktop/vr-ar.md +252 -252
- package/sub-skills/desktop/web-to-desktop.md +153 -153
- package/sub-skills/embedded/car-infotainment.md +129 -129
- package/sub-skills/embedded/esp32.md +184 -184
- package/sub-skills/embedded/ros.md +150 -150
- package/sub-skills/embedded/stm32.md +160 -160
- package/sub-skills/mobile/android.md +322 -322
- package/sub-skills/mobile/capacitor.md +232 -232
- package/sub-skills/mobile/flutter-mobile.md +138 -138
- package/sub-skills/mobile/harmonyos.md +150 -150
- package/sub-skills/mobile/ios.md +245 -245
- package/sub-skills/mobile/react-native.md +443 -443
- package/sub-skills/mobile/wearables.md +230 -230
- package/sub-skills/plugins/browser-extension.md +308 -308
- package/sub-skills/plugins/jetbrains-plugin.md +226 -226
- package/sub-skills/plugins/vscode-extension.md +204 -204
- package/sub-skills/security/security-tools.md +174 -174
- package/sub-skills/web/monorepo.md +274 -274
- package/sub-skills/web/pwa.md +220 -220
- package/sub-skills/web/serverless-edge.md +295 -295
- package/sub-skills/web/spa.md +266 -266
- package/sub-skills/web/ssr.md +228 -228
- 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 |
|