docs: add documentation for desktop auto-updater, configuration, IPC architecture, and project overview

This commit is contained in:
Firman Ramdhani
2026-04-05 23:03:40 +07:00
parent a13feb1c51
commit 8cac18edf4
4 changed files with 910 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
# Configuration Guide
This document covers how to configure the Electron desktop wrapper, manage the target web application, and handle production SPA routing.
---
## Table of Contents
- [Target App Configuration](#target-app-configuration)
- [Environment Variables](#environment-variables)
- [Production SPA Routing (Custom `app://` Protocol)](#production-spa-routing-custom-app-protocol)
- [Break Glass: Reverting to `file://` + HashRouter](#break-glass-reverting-to-file--hashrouter)
---
## Target App Configuration
The desktop app wraps any web application in the monorepo. The target is configured via environment variables in `apps/desktop/.env`.
### `.env` File
```env
# The workspace name of the target web app to wrap.
# Must match a directory under apps/ (e.g., "web", "docs-dev", "admin").
DESKTOP_TARGET_APP=web
# The dev server URL for the target web app.
# This is the URL that Vite serves during development.
DESKTOP_DEV_SERVER_URL=http://localhost:5173
```
### Switching to a Different App
To wrap `apps/admin` instead of `apps/web`:
1. Update `.env`:
```env
DESKTOP_TARGET_APP=admin
DESKTOP_DEV_SERVER_URL=http://localhost:3001
```
2. Ensure the target app has a `build` script that outputs to `dist/`.
3. Run the desktop build:
```bash
pnpm build --filter=admin && cd apps/desktop && pnpm run build
```
The `scripts/copy-web-dist.ts` prebuild script reads `DESKTOP_TARGET_APP` and copies `apps/<target>/dist/` into `apps/desktop/web-dist/`, which is then bundled by electron-builder.
### How Path Resolution Works
```
Prebuild (copy-web-dist.ts):
monorepo-root/apps/<DESKTOP_TARGET_APP>/dist/ → apps/desktop/web-dist/
Development (main process):
__dirname (out/main/) → ../../ → apps/ → apps/<target>/dist/
Production (packaged app):
process.resourcesPath → Contents/Resources/web-dist/
```
---
## Environment Variables
| Variable | Default | Used By | Description |
|---|---|---|---|
| `DESKTOP_TARGET_APP` | `web` | `copy-web-dist.ts` | Workspace name of the web app to embed |
| `DESKTOP_DEV_SERVER_URL` | `http://localhost:5173` | `src/main/index.ts` | URL of the target app's Vite dev server |
| `GH_TOKEN` | — | `electron-builder` | GitHub token for publishing releases |
| `CSC_LINK` | — | `electron-builder` | Base64-encoded code signing certificate |
| `CSC_KEY_PASSWORD` | — | `electron-builder` | Password for the signing certificate |
---
## Production SPA Routing (Custom `app://` Protocol)
### The Problem
React apps using `BrowserRouter` rely on the server to always return `index.html` for any URL path (e.g., `/dashboard`, `/auth/login`). With Electron's `file://` protocol, requesting `file:///app/dashboard` looks for an actual file at that path — which doesn't exist — resulting in a blank screen or "file not found" error.
### The Solution
The main process registers a custom `app://` protocol with a handler that:
1. Receives a request like `app://-/dashboard`.
2. Strips the protocol and hostname to get the path: `dashboard`.
3. Checks if a real file exists at `web-dist/dashboard`.
4. **If yes** → serves the file with the correct MIME type.
5. **If no** → serves `web-dist/index.html` instead (SPA fallback).
This allows React Router to handle all client-side routing normally. Deep links, page refreshes, and direct URL entry all work because every unknown path falls back to `index.html`.
### Security Measures
- **Path traversal protection**: The resolved file path is validated to stay within `web-dist/` using `normalize()` + `startsWith()` check. Requests like `app://-/../../etc/passwd` return `403 Forbidden`.
- **CSP headers**: Content-Security-Policy headers are injected on every HTML response served by the protocol handler.
- **Scheme privileges**: The `app` scheme is registered with `standard: true`, `secure: true`, `supportFetchAPI: true`, and `corsEnabled: true` — making it behave like `https://` to the renderer process.
---
## Break Glass: Reverting to `file://` + HashRouter
If the custom `app://` protocol ever causes issues (e.g., a third-party library incompatibility), you can fall back to the standard `file://` protocol with `HashRouter`. This requires two changes.
### Step 1: Switch Router in the React App
In the target web app (e.g., `apps/web/src/apps/index.tsx`):
```diff
- import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
+ import { HashRouter, Navigate, Route, Routes } from 'react-router-dom';
export default function App() {
return (
<ThemeProvider colorScheme={colorScheme} density={density}>
- <BrowserRouter>
+ <HashRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{/* All route definitions remain unchanged */}
</Routes>
</Suspense>
- </BrowserRouter>
+ </HashRouter>
</ThemeProvider>
);
}
```
Routes will now use hash-based URLs: `#/app/dashboard`, `#/auth/login`, `#/showcase`.
### Step 2: Switch to `file://` in the Main Process
In `apps/desktop/src/main/index.ts`:
**a)** Remove the scheme registration at the top of the file:
```diff
- protocol.registerSchemesAsPrivileged([
- {
- scheme: 'app',
- privileges: { standard: true, secure: true, ... },
- },
- ]);
```
**b)** Remove the `registerAppProtocol()` function entirely.
**c)** Remove the `registerAppProtocol()` call in `app.whenReady()`.
**d)** Change the production content loading in `createWindow()`:
```diff
if (IS_DEV) {
mainWindow.loadURL(DEV_SERVER_URL);
mainWindow.webContents.openDevTools({ mode: 'detach' });
} else {
- mainWindow.loadURL('app://-/index.html');
+ const webDistPath = getWebDistPath();
+ mainWindow.loadFile(join(webDistPath, 'index.html'));
}
```
**e)** Add a CSP `<meta>` tag to the web app's `index.html` since there's no protocol handler to inject headers:
```html
<meta http-equiv="Content-Security-Policy"
content="default-src 'self' file:; script-src 'self' file:; style-src 'self' 'unsafe-inline' file:; connect-src 'self' https:; img-src 'self' file: data: https:; font-src 'self' file: data:;" />
```
### Comparison
| Aspect | Custom `app://` | `file://` + HashRouter |
|---|---|---|
| URL appearance | `/app/dashboard` | `#/app/dashboard` |
| React Router | `BrowserRouter` (no change) | Must use `HashRouter` |
| Deep linking | Full support | Hash-based |
| Implementation complexity | Higher | Lower |
| CSP delivery | Via response headers | Via `<meta>` tag |
| Third-party compatibility | Rare edge cases | Maximum compatibility |