An Electron starter with a strict main/renderer/shared split.
- Electron with a context-isolated, sandboxed renderer
- electron-vite (Vite) for dev server, HMR and builds
- TypeScript with project references
- React + React Router (hash routing)
- Fluent UI React v9 (
@fluentui/react-components+@fluentui/react-icons) for styled, accessible components — themed light/dark viaFluentProviderfollowing the OS preference. Page layout uses Fluent'smakeStyles(Griffel) with design tokens — there are no CSS files - Vitest for tests, oxlint for linting, oxfmt for formatting
- Home — a gallery of Fluent UI components (inputs, overlays, feedback) plus the Electron/Chromium/Node versions fetched from the main process over typed IPC
- Dashboard — an analytics-style example screen (stat cards, a dependency-free bar chart, progress bars, a data table) showing how a real app screen looks
- About — the stack and project structure, rendered with Fluent tabs
The top navigation is a Fluent TabList wired to React Router.
src/
main/ Electron main process (Node) → may import from shared only
preload/ contextBridge / typed IPC surface → may import from shared only
renderer/ React app → may import from shared only
shared/ types shared by all → imports from none of them
The boundaries are enforced twice:
- TypeScript project references —
tsconfig.shared.json,tsconfig.node.json(main + preload) andtsconfig.web.json(renderer) each have a strictincludelist and reference only the shared project. A forbidden import failsnpm run typecheckwithTS6307. - oxlint
no-restricted-imports— per-directory overrides in .oxlintrc.json reject forbidden imports at lint time with a descriptive message. The renderer and shared code are additionally barred from importingelectrondirectly.
The IPC contract lives in src/shared/ipc.ts: channel names and payload types are defined once and used by all sides. To add a channel:
- Add the channel name and types to
src/shared/ipc.ts(extendRendererApi). - Handle it in the main process (
ipcMain.handleinsrc/main/index.ts). - Forward it in the preload (
src/preload/index.ts) — theRendererApitype ensures preload and renderer stay in sync. - Call it in the renderer via
window.api.…(typed throughsrc/renderer/src/env.d.ts).
The preload script is built as CommonJS so the renderer can keep sandbox: true
(Electron does not support ESM preload scripts in sandboxed renderers).
| Script | Purpose |
|---|---|
npm run dev |
Start the app in dev mode with HMR |
npm run build |
Typecheck, then build main/preload/renderer |
npm run start |
Preview the production build in Electron |
npm run typecheck |
tsc -b over all project references |
npm run test |
Run Vitest |
npm run lint |
Run oxlint |
npm run format |
Format with oxfmt |
npm run format:check |
Check formatting (CI) |
- Packaging/distribution (electron-builder or Forge)
- Auto-updates, app icons, CI