# Electron-Vite Configuration in Modly: How Preload and Renderer Processes Are Structured

> Learn how electron-vite configuration in Modly structures main, preload, and renderer processes with separate build targets and entry points to isolate the preload script from the React UI.

- Repository: [lightningpixel/modly](https://github.com/lightningpixel/modly)
- Tags: internals
- Published: 2026-08-15

---

**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`](https://github.com/lightningpixel/modly/blob/main/electron.vite.config.ts) file in the [modly repository](https://github.com/lightningpixel/modly) 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`](https://github.com/lightningpixel/modly/blob/main/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:

```typescript
// 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`](https://github.com/lightningpixel/modly/blob/main/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:

```typescript
// 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`](https://github.com/lightningpixel/modly/blob/main/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:

```typescript
// 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`](https://github.com/lightningpixel/modly/blob/main/electron/preload/electron-api.ts), which organizes functionality into logical groups:

```typescript
// 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:

```typescript
// 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 the `src` directory as the project root, enabling clean imports
- **`rollupOptions.input`**: Specifies [`src/index.html`](https://github.com/lightningpixel/modly/blob/main/src/index.html) as the build entry point rather than a JavaScript file
- **`resolve.alias`**: Defines path aliases for maintainable imports across feature areas
- **`react()` 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:

```tsx
// 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`](https://github.com/lightningpixel/modly/blob/main/.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`](https://github.com/lightningpixel/modly/blob/main/electron.vite.config.ts) | Central Vite configuration defining all three build targets |
| [`electron/main/index.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/index.ts) | Main process entry; creates BrowserWindow with preload path |
| [`electron/preload/index.ts`](https://github.com/lightningpixel/modly/blob/main/electron/preload/index.ts) | Preload entry; initializes context bridge |
| [`electron/preload/electron-api.ts`](https://github.com/lightningpixel/modly/blob/main/electron/preload/electron-api.ts) | API implementation with IPC wrappers |
| [`src/index.html`](https://github.com/lightningpixel/modly/blob/main/src/index.html) | Renderer HTML shell loaded by BrowserWindow |
| `src/` (under `root`) | React application source with path alias resolution |

## Summary

- **electron-vite** in `lightningpixel/modly` configures three separate Vite builds via [`electron.vite.config.ts`](https://github.com/lightningpixel/modly/blob/main/electron.vite.config.ts) for main, preload, and renderer processes
- **Preload process** uses `externalizeDepsPlugin` with `lib.entry` pointing to [`electron/preload/index.ts`](https://github.com/lightningpixel/modly/blob/main/electron/preload/index.ts), which exposes a typed API via `contextBridge.exposeInMainWorld`
- **Renderer process** configures `root: 'src'`, HTML entry point, path aliases (`@`, `@areas`, `@shared`, `@styles`), and React plugin
- **Cross-process communication** flows through `window.electron` methods that wrap `ipcRenderer.invoke` and `ipcRenderer.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.