- TypeScript 96.7%
- CSS 2.6%
- HTML 0.4%
- JavaScript 0.3%
- Spread caller placement into default placement so individual properties like screen can be set without losing horizontal/top defaults - Add test verifying partial placement merge preserves defaults |
||
|---|---|---|
| .forgejo/workflows | ||
| .github/workflows | ||
| documentation | ||
| modules | ||
| types | ||
| .editorconfig | ||
| .gitignore | ||
| .node-version | ||
| .npmrc | ||
| electron-builder.config.js | ||
| eslint.config.js | ||
| License | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| project.config.ts | ||
| README.md | ||
| tsconfig.app.json | ||
| tsconfig.base.json | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| tsconfig.test.json | ||
| tsconfig.typecheck.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
vite-electron-starter
Introduction
This starter template provides a quick and easy way to build Electron apps with TypeScript. It supports Hot Module Replacement (HMR) for the Electron main process, the preload script, and the app itself. The entire setup uses Vite in a clear, transparent manner, no hidden "black magic" in third-party plugins.
Tech Stack
- Runtime: Electron 40 (bundled Node 24), Node 24+
- Build: Vite 7, TypeScript 5.9
- Package Manager: pnpm 10.30+
- Linting: ESLint 10 + @antfu/eslint-config 7
- Testing: Vitest 4
- Settings: smol-toml (TOML persistence)
- Distribution: electron-builder
Features
Multi-Page Support
Multiple windows with independent entry points, configured via project.config.ts. A custom Vite plugin resolves
virtual modules and injects template variables. Currently, it includes: the main window, a dialogue window, and the
display demo.
Window Management
The WindowController manages per-window state including pack mode (auto-sizing to content), placement within the
display work area, and display-awareness for multi-monitor setups. Windows in pack mode use a ResizeObserver in the
preload script to automatically report content size changes back to the main process, debounced at 50 ms.
The main window remembers its position, size, and maximized state across sessions, persisted as a TOML file in the user data directory. On startup, saved bounds are validated against the current display configuration: if the window ends up off-screen (e.g. after disconnecting an external monitor), it is automatically relocated to the centre of the primary display. The settings module is configurable (defaults, file name, directory), so customer projects can adapt it without changing the core code.
Target screen selection lets you choose which display a window or dialogue opens on: 'primary' (the OS primary
display), 'app' (the display of the main application window), or 'active' (the display under the mouse cursor).
Simply set placement.screen in any WindowConfiguration or DialogConfig.
A deferred whenWindowReady promise provides a clean lifecycle hook for post-load operations like pushing dialogue
configuration.
Dialog System
A built-in dialogue system replaces native Electron dialogues with fully styled, configurable windows. Dialogues support five visual types (confirm, error, info, success, warning), configurable buttons with variants (primary, secondary, danger), and flexible placement. The backend owns all close decisions; the renderer only sends intents.
Lifecycle hooks (onOpened, onShown, onAction, onClosed) allow callers to react to dialogue state transitions.
See the Dialog System API Reference for detailed usage and configuration options.
Type-Safe IPC
Four communication patterns over typed IpcChannels:
- Request-Response (
invoke/handleFromRenderer): Frontend asks, the main process answers - Fire-and-Forget (
send/onFromRenderer): Frontend sends it, the main process reacts - Broadcast (
broadcast): The main process sends it to all renderer windows - Targeted Send (
sendToRenderer): The main process sends it to a specific renderer window
The preload script exposes a minimal window.backend API via contextBridge, keeping the renderer fully sandboxed.
All IPC arguments are automatically sanitized via safeClone in the preload and main process layers, transparently
handling non-cloneable types (DOM nodes, Symbols, Errors, circular references, RegExp) so developers never have to
worry about serialization boundaries.
Security
Context Isolation, Sandbox, and strict CSP are enabled by default. Node Integration and Webview are disabled.
Logging
Production-ready logging powered by bit-log, with source map support for both
the main process and renderer. The preload script has its own logging pipeline that forwards events to the backend via a
shared BackendForwardingAppender.
Coloured console output with timestamps, log levels, logger names, and resolved TypeScript source locations:
The same information is written to a single log file per day, with backend and frontend events merged chronologically:
A custom PipelineAppender merges backend and frontend log events into a single, chronologically sorted log file.
Events are buffered and reordered by timestamp to account for IPC latency, then flushed to Console and File delegates
with origin prefixes (Backend : / Frontend:).
Inline source maps are deliberately included in production builds. The small size overhead is well worth it: log files show the original TypeScript file names and line numbers, making it much easier to locate and fix issues in deployed applications.
Distribution
Platform-specific builds are automated via CI workflows:
- Linux: AppImage, built on GitHub Actions
- macOS: DMG, built on GitHub Actions
- Windows: Squirrel installer, built on GitHub Actions
Workflows trigger on published releases (assets attached automatically) and on manual dispatch (artefacts retained for 30 days). All builds use electron-builder.
Debugging
The dev server (pnpm dev) launches Electron with --inspect, so the main process debug port (9229) is
always available.
Main process (backend): Start pnpm dev, then attach your debugger to port 9229.
Breakpoints in the TypeScript sources under modules/electron/ and modules/common/ work via source maps.
If you need to pause at startup before any application code runs, change --inspect to --inspect-brk in
vite.config.ts (the spawn call around line 272).
Electron will then wait for a debugger to attach before continuing.
- IntelliJ / WebStorm: Run > Edit Configurations > Add New (+) > "Attach to Node.js/Chrome".
- Set port to
9229and confirm. - Start
pnpm dev, then run the attach configuration. - Breakpoints in
.tsfiles are resolved automatically. - VS Code: Add an attach configuration to
.vscode/launch.json(not committed):
Start{ "type": "node", "request": "attach", "name": "Attach Electron Main", "port": 9229, "sourceMaps": true, "outFiles": ["${workspaceFolder}/dist-electron/**/*.js"] }pnpm dev, then launch the attach configuration (F5).
Renderer (frontend): The built-in Chromium DevTools open automatically in dev mode. You can set breakpoints
directly in the Sources tab; Vite serves the original TypeScript files via source maps. For IDE-based renderer
debugging, add --remote-debugging-port=9222 to the spawn arguments in vite.config.ts and create a "Chrome Remote"
attach configuration pointing to port 9222.
Core/Demo Separation
All demo code lives in demo/ subdirectories within each module and can be deleted for a clean starter. Core
functionality (window management, dialogue system, IPC, logging) is fully independent of the demo code.


