184 lines
6.5 KiB
Markdown
184 lines
6.5 KiB
Markdown
# 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 |
|