Lightweight incremental update tools for Electron applications built with Vite. The package provides a Vite plugin, an Electron startup wrapper, update providers, bytecode protection, and utilities for common Electron app paths.
Electron applications are commonly shipped as full installers. This package keeps the installer stable and updates only the application asar file, which makes update packages smaller and keeps the runtime workflow explicit.
Key features:
vite-plugin-electron/multi-envThe packaged app uses two asar files:
app.asar: the stable entry asar generated from entry.files${app.name}.asar: the replaceable application asar generated from the Electron main, preload, and renderer build outputsThe update flow is:
createElectronApp() starts from app.asar.${app.name}.asar.tmp exists, it is renamed to ${app.name}.asar.${app.name}.asar.updater.checkForUpdates().version.json.update-available when a newer update exists.updater.downloadUpdate() downloads, decompresses, verifies, and writes ${app.name}.asar.tmp.updater.quitAndInstall() restarts the app so the new asar can be installed on next launch.npm install -D electron-incremental-update @electron/asar @babel/core
pnpm add -D electron-incremental-update @electron/asar @babel/core
yarn add -D electron-incremental-update @electron/asar @babel/core
Upgrading from
3.0.0-beta.x? See MIGRATION.md.
Recommended project layout:
electron
├── entry.ts
├── main
│ └── index.ts
├── preload
│ └── index.ts
└── native
└── db.ts
src
└── ...
electron/entry.ts is the stable startup file. It creates the updater and loads the real main process from ${app.name}.asar.
import { createElectronApp } from 'electron-incremental-update'
import { GitHubProvider } from 'electron-incremental-update/provider'
createElectronApp({
updater: {
provider: new GitHubProvider({
user: 'your-github-user',
repo: 'your-repo',
}),
},
beforeStart(mainFilePath, logger) {
logger?.debug(`Starting app from ${mainFilePath}`)
},
})
The main entry must default-export one function wrapped with startupWithUpdater().
import { app, BrowserWindow, dialog } from 'electron'
import { startupWithUpdater } from 'electron-incremental-update'
import { getAppVersion, getPathFromPreload, loadPage } from 'electron-incremental-update/utils'
export default startupWithUpdater(async (updater) => {
await app.whenReady()
const win = new BrowserWindow({
webPreferences: {
preload: getPathFromPreload('index.js'),
},
})
loadPage(win)
updater.on('update-available', async ({ version }) => {
const { response } = await dialog.showMessageBox({
type: 'info',
message: `Version ${version} is available. Current version is ${getAppVersion()}.`,
buttons: ['Download', 'Later'],
})
if (response === 0) {
await updater.downloadUpdate()
}
})
updater.on('download-progress', (info) => {
win.webContents.send('update-progress', info)
})
updater.on('update-downloaded', async () => {
const { response } = await dialog.showMessageBox({
type: 'info',
message: 'Update downloaded.',
buttons: ['Restart Now', 'Later'],
})
if (response === 0) {
updater.quitAndInstall()
}
})
updater.on('update-not-available', (code, message) => {
console.log(`[${code}] ${message}`)
})
updater.on('error', (error) => {
console.error(error)
})
await updater.checkForUpdates()
})
Use defineElectronConfig() when the same Vite config owns the renderer and Electron processes.
import { defineElectronConfig } from 'electron-incremental-update/vite'
export default defineElectronConfig({
entry: {
files: './electron/entry.ts',
},
main: {
files: './electron/main/index.ts',
},
preload: {
files: './electron/preload/index.ts',
},
updater: {
minimumVersion: '0.0.0',
paths: {
asarOutputPath: 'release/my-app.asar',
compressedPath: 'release/my-app-1.0.0.asar.br',
versionPath: 'release/version.json',
},
},
renderer: {
server: process.env.VSCODE_DEBUG
? {
host: '127.0.0.1',
port: 5173,
}
: undefined,
},
})
Use electronWithUpdater() directly when you want to manage the renderer config yourself.
import { electronWithUpdater } from 'electron-incremental-update/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
electronWithUpdater({
entry: {
files: './electron/entry.ts',
},
main: {
files: './electron/main/index.ts',
},
preload: {
files: './electron/preload/index.ts',
},
}),
],
})
Common options overview:
entry.files: entry process input. Required.main.files: main process input. Required.preload.files: preload process input. Optional.sourcemap: defaults to development or VSCODE_DEBUG.minify: defaults to production builds.bytecode: true or bytecode options.bundleDeps: controls dependency bundling. Defaults to Vite's server-environment behavior.
electron-incremental-update is always bundled.external: additional Vite/Rolldown externals. Use true to externalize dependencies.buildVersionJson: generates update JSON. Defaults to CI only.localDevUpdate: generates and serves a local update package during dev startup. Use true
for defaults, or pass { baseDir, packageJsonPath, chunkSize, chunkDelay }. See Testing The Local Flow for details.updater.minimumVersion: minimum supported entry asar version. Defaults to 0.0.0.📖 See API.md → Plugin Options for the complete reference including all types, defaults, paths, keys, and generator overrides.
Set package.json#main to the entry output file:
{
"main": "dist-entry/entry.js"
}
Minimal electron-builder.config.cjs:
const { name } = require('./package.json')
const targetFile = `${name}.asar`
/**
* @type {import('electron-builder').Configuration}
*/
module.exports = {
appId: `org.${name}`,
productName: name,
files: [
// entry files
'dist-entry',
],
npmRebuild: false,
asarUnpack: ['**/*.{node,dll,dylib,so}'],
directories: {
output: 'release',
},
extraResources: [
{ from: `release/${targetFile}`, to: targetFile }, // <- asar file
],
publish: null, // <- disable publish
}
import { createElectronApp } from 'electron-incremental-update'
import { GitHubProvider } from 'electron-incremental-update/provider'
createElectronApp({
updater: {
provider: new GitHubProvider({
user: 'your-github-user',
repo: 'your-repo',
}),
},
beforeStart(mainFilePath, logger) {
logger?.debug(`Starting app from ${mainFilePath}`)
},
})
The full Updater API, createElectronApp options, AppOption, events, and error codes are documented in the API reference.
📖 See API.md → Entry API for the complete reference.
The package includes GitHub-based providers (file, atom, API) and a local development provider.
version.json from a repository branch, downloads asar from Releases.releases.atom.All GitHub providers support a urlHandler for mirrors and custom gateways.
See the API reference for the complete provider constructor options and method signatures.
Prefer localDevUpdate: true in the Vite plugin over constructing LocalDevProvider manually.
See playground as a complete local update test:
bun run play
bun run play builds the package first, then starts the Vite dev server and Electron playground.
Expected flow:
DEV.asar and a local update archive.createElectronApp() installs any existing DEV.asar.tmp.DEV.asar.version.json.DEV.asar.tmp and restarts Electron.Explicit updater.provider values still take priority. If you set a provider manually, automatic
local provider setup is skipped.
The Vite plugin can generate:
${app.name}.asar${app.name}-${version}.asar.brversion.jsonDefault version.json shape:
{
"version": "1.0.0",
"minimumVersion": "0.0.0",
"signature": "...",
"beta": {
"version": "1.0.1-beta.1",
"minimumVersion": "0.0.0",
"signature": "..."
}
}
Stable releases update both the top-level fields and beta. Prerelease versions update only beta.
Set buildVersionJson: true if you need metadata during non-CI builds.
[!NOTE] The default version parser supports
major.minor.patch[-prerelease[.number]](e.g.1.0.0-beta.1). Build metadata (+build) and complex semver prerelease identifiers (e.g.1.0.0-beta.1.2) are not supported by default.To use full semver or a custom version scheme, override
provider.isLowerVersionwith your own comparator (e.g.semver.ltfrom thesemverpackage).
To keep update packages small, put native modules and their native binaries in app.asar, then load them from the main process with requireNative() or importNative().
import { readdirSync } from 'node:fs'
import { electronWithUpdater } from 'electron-incremental-update/vite'
export default {
plugins: [
electronWithUpdater({
external: false,
entry: {
files: ['./electron/entry.ts', './electron/native/db.ts'],
postBuild({ isBuild, copyToEntryOutputDir }) {
if (!isBuild) {
return
}
copyToEntryOutputDir({
from: './node_modules/better-sqlite3/build/Release/better_sqlite3.node',
skipIfExist: false,
})
const packageName = readdirSync('./node_modules/.pnpm').find((name) =>
name.startsWith('@napi-rs+image-'),
)
if (packageName) {
const archName = packageName.substring('@napi-rs+image-'.length).split('@')[0]
copyToEntryOutputDir({
from: `./node_modules/.pnpm/${packageName}/node_modules/@napi-rs/image-${archName}/image.${archName}.node`,
})
}
},
},
main: {
files: './electron/main/index.ts',
},
}),
],
}
Use the copied native binding from entry asar:
import Database from 'better-sqlite3'
import { getPathFromEntryAsar } from 'electron-incremental-update/utils'
const db = new Database(':memory:', {
nativeBinding: getPathFromEntryAsar('./better_sqlite3.node'),
})
export function testDatabase(): void {
db.exec('CREATE TABLE IF NOT EXISTS employees (name TEXT, salary INTEGER)')
db.prepare('INSERT INTO employees VALUES (:name, :salary)').run({
name: 'James',
salary: 5000,
})
}
Load native helper modules from main:
import { importNative, requireNative } from 'electron-incremental-update/utils'
requireNative<typeof import('../native/db')>('db').testDatabase()
const nativeDb = await importNative<typeof import('../native/db')>('db')
nativeDb.testDatabase()
For electron-builder, exclude node_modules when you have bundled dependencies manually:
module.exports = {
files: [
'dist-entry',
// exclude all dependencies in electron-builder config
'!node_modules/**',
],
}
Before: Redundant 🤮
``` . ├── dist-entry │ ├── chunk-IVHNGRZY-BPUeB0jT.js │ ├── db.js │ ├── entry.js │ └── image.js ├── node_modules │ ├── @napi-rs │ ├── base64-js │ ├── better-sqlite3 │ ├── bindings │ ├── bl │ ├── buffer │ ├── chownr │ ├── decompress-response │ ├── deep-extend │ ├── detect-libc │ ├── end-of-stream │ ├── expand-template │ ├── file-uri-to-path │ ├── fs-constants │ ├── github-fr
$ claude mcp add electron-incremental-update \
-- python -m otcore.mcp_server <graph>