docs: add documentation for desktop auto-updater, configuration, IPC architecture, and project overview
This commit is contained in:
@@ -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 |
|
||||
Reference in New Issue
Block a user