Electron 🔗 Vite
English | 简体中文
In short, vite-plugin-electron makes developing Electron apps as easy as normal Vite projects.
Note
This plugin supports both Vite 7 and Vite 8.
Build config keys are adapted automatically, using rolldownOptions on Vite 8+ and rollupOptions on Vite < 8.
Migrating from 0.x? See Migrate to v1 for full migration guide.
- 🌱 Fully compatible with Vite and Vite's ecosystem (Based on Vite)
- 🔮 Full-featured JavaScript API, really easy to integrate with complex projects.
- 🐣 Few APIs, easy to use
- Add the following dependency to your project
npm i -D vite-plugin-electron- Add
vite-plugin-electronto thepluginssection ofvite.config.ts
import electron from 'vite-plugin-electron/simple'
export default {
plugins: [
electron({
main: {
// Shortcut of `build.lib.entry`
entry: 'electron/main.ts',
},
preload: {
// Shortcut of `build.rolldownOptions.input` (`build.rollupOptions.input` on Vite < 8)
input: 'electron/preload.ts',
},
// Optional: Use Node.js API in the Renderer process
renderer: {},
}),
],
}- Create the
electron/main.tsfile and type the following code
import { app, BrowserWindow } from 'electron'
app.whenReady().then(() => {
const win = new BrowserWindow({
title: 'Main window',
})
// You can use `process.env.VITE_DEV_SERVER_URL` when the vite command is called `serve`
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL)
} else {
// Load your file
win.loadFile('dist/index.html')
}
})- Add the
mainentry topackage.json
{
+ "main": "dist-electron/main.js"
}That's it! You can now use Electron in your Vite app ✨
It just executes the electron . command in the Vite closeBundle() hook and then starts or restarts the Electron App.
- 🚨 By default, the files in
electronfolder will be built into thedist-electrondirectory
In most cases, the vite-plugin-electron/simple API is recommended. If you know very well how this plugin works or you want to use vite-plugin-electron API as a secondary encapsulation of low-level API, then the flat API is more suitable for you. It is also simple but more flexible. :)
The difference compared to the simple API is that it does not identify which entry represents preload and the adaptation to preload.
import electron from 'vite-plugin-electron'
export default {
plugins: [
electron({
entry: 'electron/main.ts',
}),
],
}- Simple API is based on the Flat API
- Simple API includes some Preload scripts preset configs.
- Flat API provides some more general APIs, which you can use for secondary encapsulation, such as nuxt-electron.
electron(options: ElectronOptions | ElectronOptions[])
export interface ElectronOptions {
/**
* Shortcut of `build.lib.entry`
*/
entry?: import('vite').LibraryOptions['entry']
vite?: import('vite').InlineConfig
/**
* Triggered when Vite is built every time -- `vite serve` command only.
*
* If this `onstart` is passed, Electron App will not start automatically.
* However, you can start Electron App via `startup` function.
*/
onstart?: (args: {
/**
* Electron App startup function.
* It will mount the Electron App child-process to `process.electronApp`.
*
* You can also set environment variables to control the Electron CLI flags.
* Supported env vars:
* - `REMOTE_DEBUGGING_PORT`
* - `ELECTRON_IGNORE_CERTIFICATE_ERRORS`
* - `ELECTRON_DISABLE_WEB_SECURITY`
* - `ELECTRON_INSPECT`
* - `ELECTRON_INSPECT_BRK`
*
* `1` or `true` turns a flag on, `0` or `false` turns it off, and any other non-empty
* value is appended as `=<value>`.
*
* @param argv default value `['.', '--no-sandbox']`
* @param options options for `child_process.spawn`
* @param customElectronPkg custom electron package name (default: 'electron')
* @returns `true` if the Electron app is started, or `false` if the startup is prevented by `startup.prevent` or `ELECTRON_STARTUP_PREVENT` env var.
*/
startup: (
argv?: string[],
options?: import('node:child_process').SpawnOptions,
customElectronPkg?: string,
) => Promise<boolean>
/** Reload Electron-Renderer */
reload: () => void
}) => void | Promise<void>
}Important
vite-plugin-electron/multi-env is only available in vite-plugin-electron@>=1.0.0.
It does not exist in 0.x releases.
Using Vite's Environment API to build Electron targets instead of manually calling build(). It is the future-facing way to handle multi-target builds, and the configuration is more concise and easier to maintain: use rolldownOptions.input on Vite 8+ or rollupOptions.input on Vite < 8 to specify the entry and overridable environment config for each target.
Flat API:
import electron from 'vite-plugin-electron/multi-env'
export default {
plugins: [
electron([
{
input: 'electron/main.ts',
},
{
input: 'electron/preload.ts',
},
]),
],
}Simple API:
import { electronSimple } from 'vite-plugin-electron/multi-env'
export default {
plugins: [
electronSimple({
main: {
input: 'electron/main.ts',
bundleDeps: {
dev: {
// Bundle this dependency during development.
include: ['some-cjs-dependency'],
},
build: {
// Leave this dependency in node_modules in the production output.
exclude: ['electron-updater'],
},
},
options: {
define: {
__ELECTRON_TARGET__: JSON.stringify('main'),
},
},
},
preload: {
input: 'electron/preload.ts',
options: {
define: {
__ELECTRON_TARGET__: JSON.stringify('preload'),
},
},
},
// You can also add custom targets, and they will be built in the same way as the main process, but with different environment variables.
custom: {
input: 'electron/custom.ts',
options: {
define: {
__ELECTRON_TARGET__: JSON.stringify('custom'),
},
},
},
}),
],
}electronSimple() accepts an object grouped by environment name. The main and preload keys reuse the same default presets as vite-plugin-electron/simple, while custom keys are built like main-process targets with their own environment options.
If you need to resolve Electron targets dynamically, use electronPluginFactory(). The factory receives the resolved project root and packageJson, and it can return a single option, an array of options, or a Promise that resolves to either shape.
import { electronPluginFactory } from 'vite-plugin-electron/multi-env'
export default {
plugins: [
electronPluginFactory(async ({ root, packageJson }) => {
const isESM = packageJson?.type === 'module'
return [
{
name: 'main',
input: 'electron/main.ts',
options: {
define: {
__APP_ROOT__: JSON.stringify(root),
},
},
},
{
name: 'preload',
input: 'electron/preload.ts',
options: {
build: {
minify: !isESM,
},
},
},
]
}),
],
}export interface MultiEnvElectronOptions {
/**
* Optional name for the Electron environment `electron_${name}`.
*
* By default, the plugin will generate environment names like `electron_0`,
* `electron_1`, etc. based on the order of the options provided.
*/
name?: string
/**
* Shortcut of `options.build.rolldownOptions.input` (`options.build.rollupOptions.input` on Vite < 8)
*/
input?: import('vite').BuildEnvironmentOptions['rolldownOptions']['input']
/**
* Shortcut of `options.build.rolldownOptions.plugins` (`options.build.rollupOptions.plugins` on Vite < 8)
*/
plugins?: import('vite').BuildEnvironmentOptions['rolldownOptions']['plugins']
/**
* Per-environment Vite options.
*/
options?: import('vite').EnvironmentOptions
/**
* Controls how dependencies are bundled.
*
* - `'vite'` (default) keeps Vite's server-environment behavior.
* - `'auto'` bundles all dependencies except package.json dependencies.
* - `true` bundles all dependencies.
* - `false` externalizes all dependencies.
*/
bundleDeps?:
| 'vite'
| 'auto'
| boolean
| {
both?: {
include?: string | RegExp | (string | RegExp)[] | true
exclude?: string[] | true
}
dev?: {
include?: string | RegExp | (string | RegExp)[] | true
exclude?: string[] | true
}
build?: {
include?: string | RegExp | (string | RegExp)[] | true
exclude?: string[] | true
}
}
onstart?: ElectronOptions['onstart']
}
export interface ElectronFactoryContext {
root: string
packageJson?: PackageJson | null
isDev: boolean
}
export type MultiEnvElectronOptionsFactory = (
context: ElectronFactoryContext,
) =>
| MultiEnvElectronOptions
| MultiEnvElectronOptions[]
| Promise<MultiEnvElectronOptions | MultiEnvElectronOptions[]>Since v0.29.0, when preload scripts are rebuilt, they send an electron-vite&type=hot-reload event to the main process.
If your App doesn't need a renderer process, this will give you hot-reload.
// electron/main.ts
process.on('message', (msg) => {
if (msg === 'electron-vite&type=hot-reload') {
for (const win of BrowserWindow.getAllWindows()) {
// Hot reload preload scripts
win.webContents.reload()
}
}
})Let's use the official template-vanilla-ts created based on create vite as an example
+ ├─┬ electron
+ │ └── main.ts
├─┬ src
│ ├── main.ts
│ ├── style.css
│ └── vite-env.d.ts
├── .gitignore
├── favicon.svg
├── index.html
├── package.json
├── tsconfig.json
+ └── vite.config.tsThis is just the default behavior, and you can modify them at any time through custom config in the vite.config.js
{ "type": "module" }
┏————————————————————┳——————————┳———————————┓
│ built │ format │ suffix │
┠————————————————————╂——————————╂———————————┨
│ main process │ esm │ .js │
┠————————————————————╂——————————╂———————————┨
│ preload scripts │ cjs │ .mjs │ diff
┠————————————————————╂——————————╂———————————┨
│ renderer process │ - │ .js │
┗————————————————————┸——————————┸———————————┛
{ "type": "commonjs" } - default
┏————————————————————┳——————————┳———————————┓
│ built │ format │ suffix │
┠————————————————————╂——————————╂———————————┨
│ main process │ cjs │ .js │
┠————————————————————╂——————————╂———————————┨
│ preload scripts │ cjs │ .js │ diff
┠————————————————————╂——————————╂———————————┨
│ renderer process │ - │ .js │
┗————————————————————┸——————————┸———————————┛
There are many cases here 👉 electron-vite-samples
The local demo suite lives in playground/ and includes flat, simple, multi-env, and worker modes that import the plugin source directly from this repo.
vite-plugin-electron's JavaScript APIs are fully typed, and it's recommended to use TypeScript or enable JS type checking in VS Code to leverage the intellisense and validation.
ElectronOptions- typeresolveViteConfig- function, Resolve the default Vite'sInlineConfigfor build Electron-MainwithExternalBuiltins- functionbuild- functionstartup- function
Example
import { build, startup } from 'vite-plugin-electron'
const isDev = process.env.NODE_ENV === 'development'
const isProd = process.env.NODE_ENV === 'production'
build({
entry: 'electron/main.ts',
vite: {
mode: process.env.NODE_ENV,
build: {
minify: isProd,
watch: isDev ? {} : null,
},
plugins: [
{
name: 'plugin-start-electron',
closeBundle() {
if (isDev) {
// Startup Electron App
startup()
}
},
},
],
},
})startup(argv, options, customElectronPkg) supports custom Electron forks. The package is resolved from app roots (process.cwd(), options.cwd, and INIT_CWD) before falling back to standard module resolution.
If the package cannot be resolved, startup() throws an error describing how to install or pass the package explicitly.
Important
Environment variables are only available in vite-plugin-electron@>=1.0.0.
They do not exist in 0.x releases.
startup() uses these env vars. 1 or true turns a flag on, 0 or false turns it off, and any other non-empty value is appended as =<value>.
REMOTE_DEBUGGING_PORTappends--remote-debugging-port=<value>ELECTRON_IGNORE_CERTIFICATE_ERRORSappends--ignore-certificate-errorsELECTRON_DISABLE_WEB_SECURITYappends--disable-web-securityELECTRON_INSPECTappends--inspect=<value>ELECTRON_INSPECT_BRKappends--inspect-brk=<value>
Important
Startup controls are only available in vite-plugin-electron@>=1.0.0.
They do not exist in 0.x releases.
Use startup.prevent = true, ELECTRON_STARTUP_PREVENT=1, or ELECTRON_STARTUP_PREVENT=true to disable automatic electron app startup. This lets you decide when to call startup.prevent = false; startup() manually (for example, after waiting for another local service).
Await the return value of startup() to know if the startup was triggered or prevented by the controls.
On Ctrl+C, vite serve sends SIGINT to the Electron app and waits for it to exit before exiting itself (exit code 130). An app that has not exited after 5 seconds is killed. Press Ctrl+C a second time to kill it immediately. In server.middlewareMode, the host process handles signals itself.
startup.exit(signal?) returns a Promise that resolves once the Electron app has exited.
When the Electron app quits by itself, the dev server is closed first and then Vite exits with the app's exit code. If Vite exits by any other path (process.exit(), an uncaught error), the running Electron app is sent SIGTERM.
By default the plugin only signals and waits for the Electron process itself. Processes the app starts (a local server, a worker binary, ...) are the app's responsibility, e.g. stop them in app.on('will-quit'). On Windows, Electron also stays alive after quitting while a process it started before its first window is still running, so the dev process never ends.
Set startup.treeKill = true to let the plugin clean them up during development:
// vite.config.ts
import { defineConfig } from 'vite'
import electron, { startup } from 'vite-plugin-electron'
startup.treeKill = true
export default defineConfig({
plugins: [electron({ entry: 'electron/main.ts' })],
})When enabled:
- A small script is preloaded into the Electron main process via
NODE_OPTIONS=--require(your ownNODE_OPTIONSare kept, and processes the app starts see the original value). When the main process exits, it kills every process the app started, including detached ones, but not Chromium's own helpers. The script is written tovite-plugin-electron/inside Vite'scacheDir(node_modules/.viteby default), not a shared temp directory, and is never part of production builds. - Shutdowns the plugin starts (restart after a rebuild,
startup.exit(), Ctrl+C) list Electron's descendants before signaling it and kill the leftovers after it exits. ASIGKILL(the 5-second timeout, a second Ctrl+C) kills the whole tree. - On Windows, if Electron exits without the preloaded script finishing its cleanup (a crash, a kill from outside such as Task Manager, or the script was not loaded), the plugin finds the processes left behind and kills them. When the script did finish, it leaves a marker file next to itself and the plugin skips this extra scan.
The process table is read synchronously (PowerShell or wmic on Windows, ps elsewhere), which adds a short delay to each restart and shutdown, about 1 second on Windows. Without startup.treeKill, the plugin never reads the process table.
Limitations:
- On macOS and Linux, processes the app started are not cleaned up when Electron crashes or is killed from outside (e.g.
kill -9). They are re-parented to init and can no longer be traced back to Electron, and the preloaded script never gets to run. - The script is loaded through
NODE_OPTIONS, so it is skipped when Electron'sEnableNodeOptionsEnvironmentVariablefuse is turned off. If it cannot be written tocacheDir, a warning is printed and Electron starts without it. In both cases only the plugin-side cleanup above applies. - Processes started from a
utilityProcess(or another Chromium helper) are only cleaned up by shutdowns the plugin starts.
Use notBundle() to externalize dependencies in Electron entries.
This keeps startup fast by skipping dependency bundling while running vite serve.
In production builds, it still externalizes dependencies, but it uses a narrower default set.
Tip
Using vite-plugin-electron/multi-env?
Use bundleDeps instead of this plugin. It maps directly to Vite's Environment API:
import { electronSimple } from 'vite-plugin-electron/multi-env'
electronSimple({
main: {
input: 'electron/main.ts',
bundleDeps: {
dev: { include: ['some-cjs-dependency'] },
build: { exclude: ['electron-updater'] },
},
},
})include maps to resolve.noExternal, and exclude maps to resolve.external. When no
bundleDeps policy is supplied, Vite's server-environment defaults apply. both is merged
with the corresponding dev or build policy. Use 'auto' to bundle all dependencies except
the package.json dependencies selected by the standalone notBundle() plugin, true to bundle
all dependencies, or false to externalize all dependencies.
Important
Behavior change in v1.0.0:
notBundle()now configuresbuild.rolldownOptions.external(orbuild.rollupOptions.externalon Vite < 8) from yourpackage.jsononce at config time, instead of verifying each import is CommonJS-loadable on everyresolveId.- Every package listed in
dependencies/devDependencies/peerDependencies/optionalDependenciesis externalized unconditionally. If a package is missing at runtime, the failure now surfaces at runtime rather than being silently bundled. Use thefilteroption to narrow or override the externalized set. - The default external set is broader during development:
dependencies,devDependencies,peerDependencies, andoptionalDependenciesare all externalized. In production, onlydependenciesare externalized. Usefilterto override the set explicitly.
import { defineConfig } from 'vite'
import electron from 'vite-plugin-electron'
import { notBundle } from 'vite-plugin-electron/plugin'
export default defineConfig({
plugins: [
electron({
entry: 'electron/main.ts',
vite: {
plugins: [notBundle()],
},
}),
],
})Under the Hood
Use build.rolldownOptions.external on Vite 8+ or build.rollupOptions.external on Vite < 8 to externalize dependencies from package.json
API
notBundle(options?: NotBundleOptions)
export interface NotBundleOptions {
/**
* Override `build.rolldownOptions.external` (`build.rollupOptions.external` on Vite < 8).
*
* When omitted, development externalizes dependencies, devDependencies,
* peerDependencies, and optionalDependencies from package.json.
* Production only externalizes dependencies.
*
* When provided, this replaces the default package.json-derived external set.
*
* Use `import { getIsViteDev } from 'vite-plugin-electron/plugin'` to detect if it's during dev.
*/
filter?: RolldownOrRollupOptions['external']
}Default behavior of extracting external dependencies from package.json.
- During development,
dependencies,devDependencies,peerDependencies, andoptionalDependenciesare externalized. - During production, only
dependenciesare externalized.
/**
* @param pkg package.json content
*/
export function extractExternalDeps(pkg: Record<string, any>): RolldownOrRollupOptions['external']Important
esmShim plugin is only available in vite-plugin-electron@>=1.0.0.
It does not exist in 0.x releases.
Use esmShim() to inject __dirname and __filename shims for ESM Electron entries that rely on these CJS globals.
Only files that actually reference __dirname or __filename are transformed, so there is no overhead for files that don't need it.
import { defineConfig } from 'vite'
import electron from 'vite-plugin-electron'
import { esmShim } from 'vite-plugin-electron/plugin'
export default defineConfig({
plugins: [
electron({
entry: 'electron/main.ts',
vite: {
plugins: [esmShim()],
},
}),
],
})Under the Hood
For each matching file, the following shim is prepended:
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)API
esmShim() — no options, just add it to plugins.
electron-builder packages dependencies into the final app, and Vite/Rolldown may also bundle dependencies into the renderer output. To avoid shipping the same code twice, native modules should stay in dependencies by default because electron-builder needs to collect their binary files. Other buildable modules should stay in devDependencies; otherwise, they can be bundled by Vite and packaged again by electron-builder.
| Classify | e.g. | dependencies | devDependencies |
|---|---|---|---|
| Node.js C/C++ native modules | serialport, sqlite3 | ✅ | ❌ |
| Node.js CJS packages | electron-store | ❌ | ✅ |
| Node.js ESM packages | execa, got, node-fetch | ❌ | ✅ |
| Web packages | Vue, React | ❌ | ✅ |
If you manually handle the binary files and runtime dependency layout for native modules, you can also move those native modules to devDependencies to further reduce the packaged app size.