Electron (Desktop Apps)
@ubean/integrations/electron is ubean’s built-in desktop app integration that wraps vite-plugin-electron to provide Electron integration with sensible defaults. It handles main/preload/renderer build coordination, Hot Restart, Hot Reload, HMR, and auto-starts the desktop app on build completion.
Features
- One-line enable:
electron: trueinubean.config.ts - Default main/preload entries (
electron/main.ts,electron/preload.ts) — zero config to start - Auto-disables SSR when enabled (desktop apps don’t need SSR unless explicitly set)
- Full pass-through of vite-plugin-electron capabilities: Hot Restart, Hot Reload, HMR, auto-launch
- Type-safe configuration via
ElectronOptions/ElectronMainOptions/ElectronPreloadOptions - Custom Vite config injection for main/preload builds
Installation
@ubean/integrations/electron is a built-in integration (a subpath of @ubean/integrations). Install it as a dev dependency in your project:
pnpm add -D @ubean/integrations/electron electron
electronis a peer dependency — you control its version.@ubean/integrations/electronsupports Electron^28through^37.
Configuration
Minimal Setup (Defaults)
The simplest configuration uses default entries and auto-disables SSR:
// ubean.config.ts
import { defineConfig } from 'ubean';
export default defineConfig({
electron: true
});This assumes:
- Main process entry:
electron/main.ts - Preload script entry:
electron/preload.ts - SSR: disabled (overridden to
false)
Custom Entries
Override defaults when your file layout differs:
// ubean.config.ts
import { defineConfig } from 'ubean';
export default defineConfig({
electron: {
main: { entry: 'src/main/index.ts' },
preload: { input: 'src/preload/index.ts' }
}
});With Custom Vite Config
Pass additional Vite config to main/preload builds:
import { defineConfig } from 'ubean';
export default defineConfig({
electron: {
main: {
entry: 'electron/main.ts',
vite: {
build: { rollupOptions: { external: ['better-sqlite3'] } }
}
},
preload: {
input: 'electron/preload.ts',
vite: {
build: { rollupOptions: { external: ['electron'] } }
}
},
renderer: {
nodeIntegration: false
}
}
});Keep SSR Enabled
If you need SSR alongside Electron (rare), explicitly set ssr: true:
export default defineConfig({
ssr: true, // overrides electron's auto-disable
electron: true
});Project Structure
A typical ubean + Electron project:
my-app/
├── electron/ # Electron entry files (matches defaults)
│ ├── main.ts # Main process entry
│ └── preload.ts # Preload script entry
├── src/ # ubean app source
│ ├── pages/
│ ├── routes/
│ ├── layouts/
│ └── ...
├── ubean.config.ts # Framework config (electron: true)
├── package.json
└── electron-builder.yml # Packaging config (optional, for distribution)Main Process Example
// electron/main.ts
import { app, BrowserWindow } from 'electron';
import { join } from 'node:path';
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
});
// In dev, load the Vite dev server
if (process.env.NODE_ENV === 'development') {
win.loadURL('http://localhost:5173');
win.webContents.openDevTools();
} else {
// In production, load the built index.html
win.loadFile(join(__dirname, '../dist/client/index.html'));
}
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});Preload Script Example
// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
// Expose a typed API to the renderer
contextBridge.exposeInMainWorld('electronAPI', {
getVersion: () => ipcRenderer.invoke('app:getVersion'),
onMenuAction: (callback: (action: string) => void) =>
ipcRenderer.on('menu:action', (_event, action) => callback(action))
});Renderer Usage
Access the exposed API in your Vue components:
<script setup lang="ts">
// Type the global API
declare global {
interface Window {
electronAPI: {
getVersion: () => Promise<string>;
onMenuAction: (callback: (action: string) => void) => void;
};
}
}
const version = ref('');
onMounted(async () => {
version.value = await window.electronAPI.getVersion();
});
</script>
<template>
<div>App version: {{ version }}</div>
</template>How It Works
@ubean/integrations/electron is a thin wrapper around vite-plugin-electron/simple. The plugin:
- Builds the main process (
electron/main.ts→dist/main/index.js) with Node.js target - Builds the preload script (
electron/preload.ts→dist/preload/index.js) with Electron renderer context - Coordinates with the renderer build — your ubean app’s client build becomes the renderer
- Auto-starts Electron in dev mode via
electron .after builds complete - Provides HMR — changes to main/preload trigger Hot Restart; renderer changes use standard Vite HMR
- Removes the stray root
index.htmlthatvite-plugin-electronwrites (see below)
The stray root index.html
vite-plugin-electron decides whether Vite has an entry from three sources: build.rollupOptions.input, build.lib, or an existing index.html in the project root. ubean provides none of them — its entries are declared per environment (environments.client.build.rollupOptions.input) and the dev client entry is a virtual module (virtual:ubean-client-entry). The plugin therefore writes its own 178-byte placeholder index.html to your project root on every ubean dev.
That file is never served: GET / is answered by ubean’s SSR pipeline and GET /index.html returns 404. It only shows up in git status.
It is also silent — the plugin’s own No entry found, writing mock ... message is classified as informational by ubean’s logging gate and hidden by default, so nothing tells you it happened. And vite-plugin-electron only cleans it up on graceful exit: a hard kill (SIGKILL, a crash, stopping the task from your IDE) leaves the placeholder behind, where it survives because it is byte-identical to what the next run would write.
ubean removes it for you. A enforce: 'post'-style plugin registered after vite-plugin-electron deletes the file in configResolved, but only when its contents match the official mock byte for byte — a root index.html you wrote yourself is never touched. Nothing about the dev server depends on the file; delete it manually whenever you like.
SSR Behavior
When electron: true is set and ssr is not explicitly configured, ubean automatically sets ssr: false. This is because:
- Desktop apps render locally — there’s no server-side rendering benefit
- Disabling SSR simplifies the build (no SSR bundle to manage)
- The renderer loads from Vite dev server (dev) or built static files (production)
To keep SSR enabled (e.g., for a hybrid app that also runs as a web service), explicitly set ssr: true.
Programmatic API
import {
ubeanElectronPlugin,
defineElectronConfig,
DEFAULT_MAIN_ENTRY,
DEFAULT_PRELOAD_INPUT,
createElectronIndexHtmlCleanupPlugin,
isVitePluginElectronMock,
ELECTRON_INDEX_CLEANUP_PLUGIN_NAME
} from '@ubean/integrations/electron';
import type {
ElectronOptions,
ElectronMainOptions,
ElectronPreloadOptions,
ElectronRendererOptions
} from '@ubean/integrations/electron';
// Use defaults directly
const defaultEntry = DEFAULT_MAIN_ENTRY; // 'electron/main.ts'
const defaultPreload = DEFAULT_PRELOAD_INPUT; // 'electron/preload.ts'
// Type-safe config helper
const config = defineElectronConfig({
main: { entry: 'electron/main.ts' },
preload: { input: 'electron/preload.ts' }
});ubeanElectronPlugin() already appends the cleanup plugin, so you only need these two exports when integrating the wrapper into a custom Vite config by hand:
| Export | Purpose |
|---|---|
createElectronIndexHtmlCleanupPlugin() | The apply: 'serve' plugin that deletes the placeholder. |
isVitePluginElectronMock(content) | Whether a string is vite-plugin-electron’s placeholder. Use it to guard your own cleanup. |
VITE_PLUGIN_ELECTRON_MOCK_INDEX_HTML | The placeholder verbatim, for byte comparison. |
ELECTRON_INDEX_CLEANUP_PLUGIN_NAME | 'ubean:electron:index-html-cleanup', to find the plugin in a list. |
Usually you don’t need to call
ubeanElectronPlugindirectly — the module system loads it automatically whenelectron: trueis set inubean.config.ts. Use this API only when integrating manually in a custom Vite config.
Packaging
@ubean/integrations/electron handles the build pipeline but does not package the app for distribution. Use electron-builder for packaging:
pnpm add -D electron-builder# electron-builder.yml
appId: com.example.myapp
directories:
output: dist-electron
files:
- dist/client/**/*
- dist/main/**/*
- dist/preload/**/*
mac:
category: public.app-category.developer-tools
target: dmg
win:
target: nsis
linux:
target: AppImage// package.json
{
"scripts": {
"build:electron": "ubean build && electron-builder"
}
}Best Practices
Use default entries: Stick with
electron/main.tsandelectron/preload.tsunless you have a strong reason to deviate — it keeps the project structure predictable.Enable contextIsolation: Always set
contextIsolation: trueandnodeIntegration: falsein BrowserWindow webPreferences. Expose only what’s needed viacontextBridge.Keep main process lean: The main process should only handle window lifecycle, IPC, and native OS integration. Business logic belongs in the renderer or ubean’s server runtime.
External native modules: If you use native modules like
better-sqlite3ornode-pty, mark them as external in the main process Vite config and rebuild them for Electron withelectron-rebuild.Enable SSR only when needed: Desktop apps almost never need SSR. Let
@ubean/integrations/electronauto-disable it.Type the preload bridge: Always declare types for
window.electronAPIto get end-to-end type safety between main and renderer.