Electron-Vite Configuration in Modly: How Preload and Renderer Processes Are Structured
The electron-vite configuration in lightningpixel/modly defines three separate Vite build targets—main, preload, and renderer—with distinct entry points, plugins, and aliases to isolate the privileged preload script from the sandboxed React UI.
This article examines the electron.vite.config.ts file in the modly repository to explain how electron-vite structures cross-process communication. Understanding this architecture is essential for developers building secure Electron applications with Vite and React.
Three-Process Architecture Overview
Modern Electron applications separate code into three contexts to maintain security boundaries. The electron-vite configuration in modly enforces this separation through independent build stanzas:
- Main process: Node.js environment with full system access
- Preload process: Privileged bridge running in an isolated context
- Renderer process: Sandbox-hosted UI with restricted capabilities
Each process receives tailored Vite configuration in electron.vite.config.ts to ensure correct bundling behavior and appropriate security restrictions.
Main Process Configuration
The main process stanza bundles the Electron entry script with externalized dependencies:
// electron.vite.config.ts (excerpt)
main: {
plugins: [externalizeDepsPlugin()],
build: {
lib: {
entry: resolve('electron/main/index.ts')
}
}
}
The externalizeDepsPlugin() is critical here—it prevents native Node.js and Electron modules from being bundled, avoiding runtime errors when Electron loads the main script. The entry point electron/main/index.ts initializes the application window and registers IPC handlers.
Preload Process Configuration
The preload stanza builds the security bridge between main and renderer:
// electron.vite.config.ts (excerpt)
preload: {
plugins: [externalizeDepsPlugin()],
build: {
lib: {
entry: resolve('electron/preload/index.ts')
}
}
}
Like the main process, preload uses externalizeDepsPlugin() to preserve Electron imports. The entry electron/preload/index.ts executes in an isolated world with access to both Node.js APIs (via ipcRenderer) and limited DOM context (via webFrame).
Exposing the Typed API
The preload script uses contextBridge.exposeInMainWorld to inject a controlled interface onto the window object:
// electron/preload/index.ts
import { contextBridge, ipcRenderer, webFrame } from 'electron';
import { createElectronApi } from './electron-api';
contextBridge.exposeInMainWorld('electron', createElectronApi(ipcRenderer, webFrame));
This creates the window.electron namespace that renderer code can access. The actual API implementation lives in electron/preload/electron-api.ts, which organizes functionality into logical groups:
// electron/preload/electron-api.ts (excerpt)
export function createElectronApi(ipcRenderer, webFrame) {
return {
// Window controls
window: {
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
},
// UI helpers
ui: {
setZoomFactor: (factor: number) => webFrame.setZoomFactor(factor),
},
// Python bridge for backend integration
python: {
start: () => ipcRenderer.invoke('python:start'),
stop: () => ipcRenderer.invoke('python:stop'),
status: () => ipcRenderer.invoke('python:status'),
},
// File system dialogs
fs: {
selectImage: () => ipcRenderer.invoke('fs:selectImage'),
saveFile: (data: string, filename: string) =>
ipcRenderer.invoke('fs:saveFile', data, filename),
},
// Application metadata
app: {
info: () => ipcRenderer.invoke('app:info'),
},
// Settings persistence
settings: {
get: (key: string) => ipcRenderer.invoke('settings:get', key),
set: (key: string, value: unknown) =>
ipcRenderer.invoke('settings:set', key, value),
},
// Additional groups: model, log, workspace, extensions...
};
}
Each method wraps ipcRenderer.send for fire-and-forget messages or ipcRenderer.invoke for request-response patterns with the main process.
Renderer Process Configuration
The renderer stanza configures the React-based UI layer with substantially different settings:
// electron.vite.config.ts (excerpt)
renderer: {
root: 'src',
build: {
rollupOptions: {
input: resolve('src/index.html')
}
},
resolve: {
alias: {
'@': resolve('src'),
'@areas': resolve('src/areas'),
'@shared': resolve('src/shared'),
'@styles': resolve('src/styles')
}
},
plugins: [react()]
}
Key differences from main/preload:
root: 'src': Treats thesrcdirectory as the project root, enabling clean importsrollupOptions.input: Specifiessrc/index.htmlas the build entry point rather than a JavaScript fileresolve.alias: Defines path aliases for maintainable imports across feature areasreact()plugin: Adds JSX/TSX transformation and React Fast Refresh
Consuming the Preload API in Renderer Code
Renderer components access the exposed API through the global window.electron object:
// src/App.tsx (simplified example)
import React, { useEffect, useState } from 'react';
export default function App() {
const [version, setVersion] = useState<string>('');
const [pythonStatus, setPythonStatus] = useState<string>('stopped');
useEffect(() => {
// Fetch application metadata via preload bridge
window.electron.app.info().then((info) => {
setVersion(info.version);
});
// Check Python backend status
window.electron.python.status().then((status) => {
setPythonStatus(status);
});
}, []);
const handleMinimize = () => {
window.electron.window.minimize();
};
return (
<div>
<header>
<span>Modly v{version}</span>
<button onClick={handleMinimize}>Minimize</button>
</header>
<main>
<p>Python backend: {pythonStatus}</p>
</main>
</div>
);
}
The TypeScript types for window.electron are typically declared in a .d.ts file to provide IntelliSense and compile-time validation.
Security Boundaries and Build Isolation
The electron-vite configuration enforces critical security properties:
| Aspect | Implementation |
|---|---|
| Context isolation | Preload runs in isolated context; renderer cannot directly require Node modules |
| API surface control | Only explicitly exposed methods on window.electron are accessible |
| No eval in renderer | Vite's default CSP-friendly build prevents unsafe execution |
| Separate bundles | Each process has independent output preventing accidental import leakage |
The externalizeDepsPlugin ensures that sensitive Electron APIs remain unavailable in the renderer bundle—even if malicious code executes in the UI layer, it cannot directly import ipcRenderer or other privileged modules.
Key Files and Responsibilities
| File | Purpose |
|---|---|
electron.vite.config.ts |
Central Vite configuration defining all three build targets |
electron/main/index.ts |
Main process entry; creates BrowserWindow with preload path |
electron/preload/index.ts |
Preload entry; initializes context bridge |
electron/preload/electron-api.ts |
API implementation with IPC wrappers |
src/index.html |
Renderer HTML shell loaded by BrowserWindow |
src/ (under root) |
React application source with path alias resolution |
Summary
- electron-vite in
lightningpixel/modlyconfigures three separate Vite builds viaelectron.vite.config.tsfor main, preload, and renderer processes - Preload process uses
externalizeDepsPluginwithlib.entrypointing toelectron/preload/index.ts, which exposes a typed API viacontextBridge.exposeInMainWorld - Renderer process configures
root: 'src', HTML entry point, path aliases (@,@areas,@shared,@styles), and React plugin - Cross-process communication flows through
window.electronmethods that wrapipcRenderer.invokeandipcRenderer.send, maintaining security boundaries
Frequently Asked Questions
What is the purpose of externalizeDepsPlugin in electron-vite?
externalizeDepsPlugin prevents native Node.js and Electron modules from being bundled into the output, keeping them as external require() calls. This is necessary because Electron's main and preload processes run in a Node.js context where these modules must be loaded at runtime from the Electron binary, not from a bundled artifact.
How does the preload script communicate with the renderer process?
The preload script does not directly communicate with the renderer—it exposes an API to it. Using contextBridge.exposeInMainWorld, the preload injects a window.electron object that the renderer can call. These calls are internally translated to IPC messages sent to the main process, which then handles the actual operation and returns results.
Why does the renderer configuration use root: 'src' instead of the project root?
Setting root: 'src' simplifies import paths within the React application and isolates the UI build from Electron-specific files. This allows clean aliases like import { Component } from '@shared/ui' rather than relative paths like ../../../../shared/ui, and ensures Vite's dev server and build process focus only on the frontend code.
Can renderer code directly import Electron modules?
No—direct imports of Electron modules in renderer code would fail or create security vulnerabilities. The renderer runs in a Chromium sandbox with contextIsolation: true (the default when using preload). All Electron functionality must be accessed through the explicitly exposed window.electron API, which the preload script constructs using ipcRenderer on behalf of the renderer.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →