chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:12 +08:00
commit d8dcd5f6d1
8604 changed files with 2479390 additions and 0 deletions
+277
View File
@@ -0,0 +1,277 @@
---
title: "Electron Desktop Guide"
version: 3.8.40
lastUpdated: 2026-06-28
---
# Electron Desktop Guide
> **Source of truth:** `electron/` workspace
> **Last updated:** 2026-06-28 — v3.8.40
OmniRoute ships a cross-platform desktop app (Windows / macOS / Linux) built on
**Electron 41** + **electron-builder 26.10**. The desktop app spawns the Next.js
standalone server as a child process, points a `BrowserWindow` at it, and adds a
system tray, auto-updater, IPC bridge, and zero-config secret bootstrap.
## Architecture
```
┌──────────────────────────────────────────────┐
│ Electron main process (electron/main.js) │
│ ├─ Single-instance lock │
│ ├─ Child process: Next.js standalone server │
│ │ (spawned with Electron's Node runtime) │
│ ├─ BrowserWindow → http://localhost:PORT │
│ ├─ System tray + context menu │
│ ├─ Auto-update via electron-updater │
│ ├─ Content Security Policy (session headers) │
│ └─ Secret bootstrap (JWT / API_KEY_SECRET) │
└──────────────────────────────────────────────┘
↕ IPC bridge (electron/preload.js)
┌──────────────────────────────────────────────┐
│ Renderer (Next.js dashboard) │
│ window.electronAPI.* (contextIsolation) │
└──────────────────────────────────────────────┘
```
## Versions
Confirmed from `electron/package.json`:
| Package | Version |
| ------------------ | -------------------------- |
| `electron` | `^41.5.1` |
| `electron-builder` | `^26.10.0` |
| `electron-updater` | `^6.8.5` |
| `better-sqlite3` | `^12.9.0` |
| App version | `3.8.0` |
| App id | `online.omniroute.desktop` |
| Product name | `OmniRoute` |
## Scripts (root `package.json`)
| Script | Purpose |
| --------------------------------- | -------------------------------------------------------------------------- |
| `npm run electron:dev` | Starts `npm run dev` + waits for `localhost:20128` + launches Electron |
| `npm run electron:build` | Builds Next.js then runs `electron-builder` for the current OS |
| `npm run electron:build:win` | Builds Windows NSIS installer + portable (x64) |
| `npm run electron:build:mac` | Builds macOS DMG (Intel + Apple Silicon) |
| `npm run electron:build:linux` | Builds Linux AppImage + DEB (x64 + arm64) |
| `npm run electron:smoke:packaged` | Launches packaged binary and probes `/login` for HTTP 200, then shuts down |
The `electron/` workspace also exposes:
- `npm run prepare:bundle` — runs `scripts/build/prepare-electron-standalone.mjs`
- `npm run build:mac-x64` / `build:mac-arm64` — single-arch macOS builds
- `npm run pack` — directory-only build for local testing (no installer)
## Directory Layout
```
electron/
├── package.json # Electron deps + electron-builder config
├── main.js # Main process (24 KB — see annotations below)
├── preload.js # contextBridge IPC bridge
├── types.d.ts # AppInfo / ServerStatus / ElectronAPI types
├── README.md # In-workspace notes
├── assets/ # icon.png, icon.ico, icon.icns, tray-icon.png
└── dist-electron/ # electron-builder output (gitignored)
scripts/
├── build/
│ └── prepare-electron-standalone.mjs # Stages .next/electron-standalone bundle
└── dev/
└── smoke-electron-packaged.mjs # Post-build smoke test
```
Both `main.js` and `preload.js` are **CommonJS `.js` files**, not TypeScript. The
renderer-side typings live in `electron/types.d.ts`.
## IPC Bridge (`preload.js`)
The preload exposes a whitelisted API on `window.electronAPI` using `contextBridge`
with `contextIsolation: true` and `nodeIntegration: false`.
```javascript
const VALID_CHANNELS = {
invoke: [
"get-app-info",
"open-external",
"get-data-dir",
"restart-server",
"check-for-updates",
"download-update",
"install-update",
"get-app-version",
],
send: ["window-minimize", "window-maximize", "window-close"],
receive: ["server-status", "port-changed", "update-status"],
};
```
Exposed methods:
| Renderer call | Type |
| ----------------------------------------------------------------- | -------------------------- |
| `getAppInfo()``{ name, version, platform, isDev, port }` | invoke |
| `openExternal(url)` | invoke |
| `getDataDir()` | invoke |
| `restartServer()` | invoke |
| `getAppVersion()` | invoke |
| `checkForUpdates()` / `downloadUpdate()` / `installUpdate()` | invoke |
| `minimizeWindow()` / `maximizeWindow()` / `closeWindow()` | send |
| `onServerStatus(cb)` / `onPortChanged(cb)` / `onUpdateStatus(cb)` | receive (returns disposer) |
The receive helpers return a **disposer function** rather than relying on
`removeAllListeners` — this prevents listener accumulation when React components
remount.
## Server Lifecycle
`main.js` spawns the Next.js standalone bundle directly with the Electron Node
runtime to avoid native-module ABI mismatch with system Node:
```js
spawn(process.execPath, [serverScript], {
cwd: NEXT_SERVER_PATH,
env: { ...serverEnv, PORT, NODE_ENV: "production", ELECTRON_RUN_AS_NODE: "1", NODE_PATH },
stdio: "pipe",
});
```
Highlights:
- `waitForServer()` polls the URL up to 30 s before showing the window (no blank screen on cold start).
- `stdio: "pipe"` captures stdout/stderr; ready phrases (`Ready` / `listening`) emit `server-status: running` over IPC.
- `before-quit` waits up to 5 s for graceful SIGTERM (WAL checkpoint) then sends SIGKILL.
- Port switcher in the tray (`20128`, `3000`, `8080`) stops and restarts the server, then reloads the BrowserWindow.
## Zero-config Secret Bootstrap
On first launch, the main process auto-generates and persists missing secrets:
| Secret | Source |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `JWT_SECRET` | `crypto.randomBytes(64).toString("hex")` |
| `STORAGE_ENCRYPTION_KEY` | `crypto.randomBytes(32).toString("hex")` (refuses if encrypted creds already exist) |
| `API_KEY_SECRET` | `crypto.randomBytes(32).toString("hex")` |
Persisted to `<DATA_DIR>/server.env`. `DATA_DIR` resolves to:
- Windows: `%APPDATA%\omniroute`
- Linux: `$XDG_CONFIG_HOME/omniroute` or `~/.omniroute`
- macOS: `~/.omniroute`
## Window & Tray
- `BrowserWindow`: 1400×900 (min 1024×700), `backgroundColor: "#0a0a0a"`.
- macOS: `titleBarStyle: "hiddenInset"`, traffic-light at `{ x: 16, y: 16 }`.
- Windows/Linux: native title bar.
- Close button minimizes to tray; the tray menu has **Open OmniRoute**, **Open Dashboard** (external browser), **Server Port** submenu, **Check for Updates**, **Quit**.
## Content Security Policy
Set via `session.defaultSession.webRequest.onHeadersReceived`. Notable directives:
- `frame-ancestors 'none'`, `object-src 'none'`, `child-src 'none'`
- `connect-src 'self' http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https://*.omniroute.online https://*.omniroute.dev`
- Dev mode adds `'unsafe-eval'` to `script-src` only
## Auto-update
Uses `electron-updater` with the GitHub provider (`diegosouzapw/OmniRoute`).
- `autoDownload = false`, `autoInstallOnAppQuit = true`
- Events forwarded to renderer via `update-status` IPC:
`checking`, `available`, `not-available`, `downloading` (with `percent`), `downloaded`, `error`
- `installUpdate()` kills the server then calls `autoUpdater.quitAndInstall()`
- Skipped in dev mode (`!app.isPackaged`)
## Build Pipeline
1. `npm run build` → Next.js standalone in `.next/standalone`.
2. `prepare-electron-standalone.mjs` → re-stages into `.next/electron-standalone` and rewrites absolute paths inside `server.js` + `required-server-files.json` so the bundle is relocatable.
3. `electron-builder` packages `main.js`, `preload.js`, `node_modules`, and `extraResources: { ../.next/electron-standalone → app }`.
### Build targets
| OS | Targets |
| ------- | ----------------------------------------- |
| Windows | NSIS installer + portable (x64) |
| macOS | DMG (Intel + arm64, drag-to-Applications) |
| Linux | AppImage + DEB (x64 + arm64) |
NSIS settings: `oneClick: false`, lets the user choose the install directory, creates Desktop and Start-Menu shortcuts.
## Smoke Testing Packaged Build
```bash
npm run electron:smoke:packaged
```
`scripts/dev/smoke-electron-packaged.mjs`:
- Auto-discovers the packaged binary in `electron/dist-electron/` for the current platform.
- Launches with isolated `HOME`/`APPDATA`/`XDG_*` directories so it doesn't touch developer data.
- Polls `http://127.0.0.1:20128/login` for HTTP 200 within 45 s.
- Watches stderr/stdout for fatal patterns (`Cannot find module`, `MODULE_NOT_FOUND`, `ERR_DLOPEN_FAILED`, `Failed to start server`, etc.).
- Waits 2 s of stable runtime after readiness, then issues SIGTERM and waits for the port to free.
- In CI, automatically passes `--no-sandbox --disable-gpu` (and `--disable-dev-shm-usage` on Linux).
Env overrides: `ELECTRON_SMOKE_APP_EXECUTABLE`, `ELECTRON_SMOKE_URL`, `ELECTRON_SMOKE_TIMEOUT_MS`, `ELECTRON_SMOKE_SETTLE_MS`, `ELECTRON_SMOKE_DATA_DIR`, `ELECTRON_SMOKE_KEEP_DATA`, `ELECTRON_SMOKE_STREAM_LOGS`.
## Code Signing
`electron/package.json` does **not** wire signing credentials directly. Pass them via env vars to `electron-builder`:
### macOS
```bash
export APPLE_ID=<email>
export APPLE_APP_SPECIFIC_PASSWORD=<password>
export APPLE_TEAM_ID=<id>
export CSC_LINK=path/to/cert.p12
export CSC_KEY_PASSWORD=<cert-password>
npm run electron:build:mac
```
### Windows
```bash
export CSC_LINK=path/to/cert.pfx
export CSC_KEY_PASSWORD=<cert-password>
npm run electron:build:win
```
### Linux
AppImage signing is optional — set `LINUX_GPG_KEY` if signing.
## Distribution
Artifacts land in `electron/dist-electron/`:
- `OmniRoute Setup X.Y.Z.exe`, `OmniRoute-X.Y.Z-portable.exe` (Windows)
- `OmniRoute-X.Y.Z-mac.dmg`, `OmniRoute-X.Y.Z-arm64-mac.dmg` (macOS)
- `OmniRoute-X.Y.Z.AppImage`, `omniroute-desktop_X.Y.Z_amd64.deb` (Linux)
Releases are published to GitHub Releases (`diegosouzapw/OmniRoute`), which is also where `electron-updater` checks for new versions.
## Troubleshooting
| Symptom | Fix |
| --------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `Cannot find module 'better-sqlite3'` after Electron major bump | `cd electron && npm rebuild` |
| `ERR_DLOPEN_FAILED` for native module | Re-run `prepare:bundle` and verify ABI matches Electron's Node |
| Window appears blank on Linux | Confirm Next.js server actually bound to PORT (check `[Server]` logs) |
| macOS notarization stalls | Ensure `APPLE_*` vars are exported, not just in `.env` |
| Windows SmartScreen warning | Sign with EV cert, or users right-click → "Run anyway" |
| Smoke test fails with port-in-use | Stop any local dev server on 20128 before running `electron:smoke:packaged` |
## See Also
- [SETUP_GUIDE.md](./SETUP_GUIDE.md)
- [RELEASE_CHECKLIST.md](../ops/RELEASE_CHECKLIST.md)
- Source: `electron/main.js`, `electron/preload.js`, `electron/package.json`
- Helpers: `scripts/build/prepare-electron-standalone.mjs`, `scripts/dev/smoke-electron-packaged.mjs`