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 Incremental Update
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 based on
vite-plugin-electron/multi-env - Dual asar runtime layout
- Signed update metadata and asar verification
- GitHub and local development providers
- Optional V8 bytecode generation
- Utilities for app paths, native modules, renderer loading, and Electron startup
The packaged app uses two asar files:
app.asar: the stable entry asar generated fromentry.files${app.name}.asar: the replaceable application asar generated from the Electron main, preload, and renderer build outputs
The update flow is:
createElectronApp()starts fromapp.asar.- If
${app.name}.asar.tmpexists, it is renamed to${app.name}.asar. - The configured main file is loaded from
${app.name}.asar. - Your main process calls
updater.checkForUpdates(). - The provider downloads
version.json. - The updater compares versions and emits
update-availablewhen 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/corepnpm add -D electron-incremental-update @electron/asar @babel/coreyarn add -D electron-incremental-update @electron/asar @babel/coreUpgrading 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 orVSCODE_DEBUG.minify: defaults to production builds.bytecode:trueor bytecode options.bundleDeps: controls dependency bundling. Defaults to Vite's server-environment behavior.electron-incremental-updateis always bundled.external: additional Vite/Rolldown externals. Usetrueto externalizedependencies.buildVersionJson: generates update JSON. Defaults to CI only.localDevUpdate: generates and serves a local update package during dev startup. Usetruefor defaults, or pass{ baseDir, packageJsonPath, chunkSize, chunkDelay }. See Testing The Local Flow for details.updater.minimumVersion: minimum supported entry asar version. Defaults to0.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.
- GitHubProvider — reads
version.jsonfrom a repository branch, downloads asar from Releases. - GitHubAtomProvider — reads the latest release from
releases.atom. - GitHubApiProvider — uses the GitHub Releases API (supports tokens for private repos).
- LocalDevProvider — reads update artifacts from the local filesystem for dev testing.
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 playbun run play builds the package first, then starts the Vite dev server and Electron playground.
Expected flow:
- The plugin builds the Electron main, preload, and entry outputs.
- The plugin creates
DEV.asarand a local update archive. createElectronApp()installs any existingDEV.asar.tmp.- The app starts from
DEV.asar. - The updater checks the generated
version.json. - The playground shows an available local update.
- Clicking download emits simulated progress events.
- Clicking restart installs
DEV.asar.tmpand 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.json
Default 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.isLowerVersion with your own comparator
(e.g. semver.lt from the semver package).
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-from-package
│ ├── ieee754
│ ├── inherits
│ ├── ini
│ ├── mimic-response
│ ├── minimist
│ ├── mkdirp-classic
│ ├── napi-build-utils
│ ├── node-abi
│ ├── once
│ ├── prebuild-install
│ ├── pump
│ ├── rc
│ ├── readable-stream
│ ├── safe-buffer
│ ├── semver
│ ├── simple-concat
│ ├── simple-get
│ ├── string_decoder
│ ├── strip-json-comments
│ ├── tar-fs
│ ├── tar-stream
│ ├── tunnel-agent
│ ├── util-deprecate
│ └── wrappy
└── package.json
After: Clean 😍
.
├── dist-entry
│ ├── better_sqlite3.node
│ ├── chunk-IVHNGRZY-BPUeB0jT.js
│ ├── db.js
│ ├── entry.js
│ ├── image.js
│ └── image.win32-x64-msvc.node
└── package.json
Bytecode protection compiles JavaScript into V8 bytecode.
electronWithUpdater({
bytecode: true,
entry: {
files: './electron/entry.ts',
},
main: {
files: './electron/main/index.ts',
},
})Notes:
- CommonJS only. Remove
"type": "module"frompackage.jsonwhen enabling bytecode. - Main process bytecode is enabled by default.
- To include preload scripts, use
bytecode: { enablePreload: true }. - If preload bytecode is enabled, create the
BrowserWindowwithsandbox: false.
bundleDeps controls dependency handling for entry, main, and preload environments. It defaults to
'vite', which preserves Vite's server-environment behavior.
electronWithUpdater({
bundleDeps: {
dev: {
exclude: true,
},
build: {
include: ['some-dependency'],
},
},
entry: {
files: './electron/entry.ts',
},
main: {
files: './electron/main/index.ts',
},
})Use 'auto' to bundle all dependencies except package.json dependencies, true to bundle all
dependencies, or false to externalize all dependencies. both applies a policy in both modes
and is merged with the active dev or build policy. include maps to Vite's
resolve.noExternal; exclude maps to resolve.external. electron-incremental-update and its
subpath imports are always bundled.
Import from electron-incremental-update/utils for path helpers, platform checks, native module loading, crypto, compression, download, and version parsing utilities.
📖 See API.md → Utilities API for the complete function list with signatures and descriptions.
For the full API documentation including all types, options tables, provider constructors, and plugin configuration, see the standalone reference:
📖 API.md — Auto-generated JSDoc extraction + manual reference sections.
- Obsidian for the dual asar update strategy
- vite-plugin-electron for the Vite Electron plugin foundation
- electron-builder for Electron packaging
- electron-vite for bytecode plugin inspiration
MIT