electron-incremental-update 3.0.0-beta.5 → 3.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,107 +1,108 @@
1
1
  # Electron Incremental Update
2
2
 
3
- This project is built on top of [vite-plugin-electron](https://github.com/electron-vite/vite-plugin-electron), offers a lightweight update solution for Electron applications without using native executables.
3
+ 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.
4
+
5
+ ## Contents
4
6
 
5
7
  - [Electron Incremental Update](#electron-incremental-update)
6
- - [Key Features](#key-features)
7
- - [Dual Asar Architecture](#dual-asar-architecture)
8
- - [Update Process](#update-process)
9
- - [Additional Features](#additional-features)
10
- - [Getting Started](#getting-started)
11
- - [Install](#install)
12
- - [Project Structure](#project-structure)
13
- - [Setup Entry](#setup-entry)
14
- - [Setup `vite.config.ts`](#setup-viteconfigts)
15
- - [Modify package.json](#modify-packagejson)
16
- - [Config `electron-builder`](#config-electron-builder)
17
- - [Usage](#usage)
18
- - [Use In Main Process](#use-in-main-process)
19
- - [Alternative Provider Setup](#alternative-provider-setup)
20
- - [Custom logger](#custom-logger)
21
- - [Beta Channel Updates](#beta-channel-updates)
22
- - [Use Native Modules](#use-native-modules)
23
- - [Example](#example)
24
- - [Result in app.asar](#result-in-appasar)
25
- - [Bytecode Protection](#bytecode-protection)
26
- - [Benefits](#benefits)
27
- - [Limitation](#limitation)
28
- - [Utils](#utils)
29
- - [Electron Utilities](#electron-utilities)
30
- - [Crypto Utilities](#crypto-utilities)
31
- - [Zip Utilities](#zip-utilities)
8
+ - [Contents](#contents)
9
+ - [Why](#why)
10
+ - [How It Works](#how-it-works)
11
+ - [Installation](#installation)
12
+ - [Quick Start](#quick-start)
13
+ - [Entry Process](#entry-process)
14
+ - [Main Process](#main-process)
15
+ - [Vite Configuration](#vite-configuration)
16
+ - [Plugin Options](#plugin-options)
17
+ - [Electron Builder](#electron-builder)
18
+ - [Runtime Usage](#runtime-usage)
19
+ - [Create The App](#create-the-app)
20
+ - [Updater API](#updater-api)
21
+ - [Providers](#providers)
22
+ - [GitHub](#github)
23
+ - [Default](#default)
24
+ - [Atom](#atom)
25
+ - [Api](#api)
26
+ - [URL Handling](#url-handling)
27
+ - [Local](#local)
28
+ - [Recommended Setup](#recommended-setup)
29
+ - [Custom Options](#custom-options)
30
+ - [Manual Provider](#manual-provider)
31
+ - [Testing The Local Flow](#testing-the-local-flow)
32
+ - [Update Artifacts](#update-artifacts)
33
+ - [Native Modules](#native-modules)
34
+ - [Result in app.asar](#result-in-appasar)
35
+ - [Bytecode Protection](#bytecode-protection)
36
+ - [Development Bundling](#development-bundling)
37
+ - [Utilities](#utilities)
32
38
  - [Credits](#credits)
33
39
  - [License](#license)
34
40
 
35
- ## Key Features
36
-
37
- This solution provides a comprehensive update system for Electron applications, including:
38
-
39
- - **Vite Plugin** - Seamlessly integrates with your existing Vite build process
40
- - **Startup Entry Function** - Handles application initialization and update checking
41
- - **Updater Class** - Manages the complete update workflow with event-driven API
42
- - **Utility Functions** - Helper functions for file paths, version management, and more
43
-
44
- ### Dual Asar Architecture
41
+ ## Why
45
42
 
46
- The update system uses a two-file structure for efficient incremental updates:
43
+ 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.
47
44
 
48
- - **`app.asar`** - The application entry point that loads and initializes the updater
49
- - **`${electron.app.name}.asar`** - Contains your application code (main process, preload scripts, and renderer) that gets replaced during updates
45
+ Key features:
50
46
 
51
- ### Update Process
47
+ - Vite plugin based on `vite-plugin-electron/multi-env`
48
+ - Dual asar runtime layout
49
+ - Signed update metadata and asar verification
50
+ - GitHub and local development providers
51
+ - Optional V8 bytecode generation
52
+ - Utilities for app paths, native modules, renderer loading, and Electron startup
52
53
 
53
- The update workflow follows these steps:
54
+ ## How It Works
54
55
 
55
- 1. **Check for Updates** - Query the remote server for available updates
56
- 2. **Download and Verify** - Download the update asar file and verify it using RSA signatures
57
- 3. **Prepare for Update** - The application quits to prepare for the update
58
- 4. **Apply Update** - On next launch, replace the old `${electron.app.name}.asar` with the new version and load it
56
+ The packaged app uses two asar files:
59
57
 
60
- ### Additional Features
58
+ - `app.asar`: the stable entry asar generated from `entry.files`
59
+ - `${app.name}.asar`: the replaceable application asar generated from the Electron main, preload, and renderer build outputs
61
60
 
62
- - **Smaller Update Packages** - Package all native modules into `app.asar` to minimize the update file size and download time [see usage](#use-native-modules)
63
- - **Source Code Protection** - Leverage V8 bytecode compilation to obfuscate and protect your JavaScript source code [see details](#bytecode-protection)
61
+ The update flow is:
64
62
 
65
- ## Getting Started
63
+ 1. `createElectronApp()` starts from `app.asar`.
64
+ 2. If `${app.name}.asar.tmp` exists, it is renamed to `${app.name}.asar`.
65
+ 3. The configured main file is loaded from `${app.name}.asar`.
66
+ 4. Your main process calls `updater.checkForUpdates()`.
67
+ 5. The provider downloads `version.json`.
68
+ 6. The updater compares versions and emits `update-available` when a newer update exists.
69
+ 7. `updater.downloadUpdate()` downloads, decompresses, verifies, and writes `${app.name}.asar.tmp`.
70
+ 8. `updater.quitAndInstall()` restarts the app so the new asar can be installed on next launch.
66
71
 
67
- ### Install
72
+ ## Installation
68
73
 
69
74
  ```sh
70
- npm install -D electron-incremental-update
75
+ npm install -D electron-incremental-update @electron/asar @babel/core
71
76
  ```
72
77
 
73
78
  ```sh
74
- yarn add -D electron-incremental-update
79
+ pnpm add -D electron-incremental-update @electron/asar @babel/core
75
80
  ```
76
81
 
77
82
  ```sh
78
- pnpm add -D electron-incremental-update
83
+ yarn add -D electron-incremental-update @electron/asar @babel/core
79
84
  ```
80
85
 
81
- ### Project Structure
86
+ ## Quick Start
82
87
 
83
- Base on [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue)
88
+ Recommended project layout:
84
89
 
85
- ```
90
+ ```txt
86
91
  electron
87
- ├── entry.ts // <- entry file
92
+ ├── entry.ts
88
93
  ├── main
89
94
  │ └── index.ts
90
95
  ├── preload
91
96
  │ └── index.ts
92
- └── native // <- possible native modules
93
- └── index.ts
97
+ └── native
98
+ └── db.ts
94
99
  src
95
100
  └── ...
96
101
  ```
97
102
 
98
- ### Setup Entry
99
-
100
- The entry is used to load the application and initialize the `Updater`
103
+ ### Entry Process
101
104
 
102
- `Updater` use the `provider` to check and download the update. The built-in `GithubProvider` is based on `BaseProvider`, which implements the `IProvider` interface (see [types](#provider)). And the `provider` is optional, you can setup later
103
-
104
- in `electron/entry.ts`
105
+ `electron/entry.ts` is the stable startup file. It creates the updater and loads the real main process from `${app.name}.asar`.
105
106
 
106
107
  ```ts
107
108
  import { createElectronApp } from 'electron-incremental-update'
@@ -109,123 +110,193 @@ import { GitHubProvider } from 'electron-incremental-update/provider'
109
110
 
110
111
  createElectronApp({
111
112
  updater: {
112
- // optional, you can setup later
113
113
  provider: new GitHubProvider({
114
- username: 'yourname',
115
- repo: 'electron',
114
+ user: 'your-github-user',
115
+ repo: 'your-repo',
116
116
  }),
117
117
  },
118
118
  beforeStart(mainFilePath, logger) {
119
- logger?.debug(mainFilePath)
119
+ logger?.debug(`Starting app from ${mainFilePath}`)
120
120
  },
121
121
  })
122
122
  ```
123
123
 
124
- - [some Github CDN resources](https://github.com/XIU2/UserScript/blob/master/GithubEnhanced-High-Speed-Download.user.js#L34)
124
+ ### Main Process
125
125
 
126
- ### Setup `vite.config.ts`
126
+ The main entry must default-export one function wrapped with `startupWithUpdater()`.
127
127
 
128
- The plugin config, `main` and `preload` parts are reference from [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue)
128
+ ```ts
129
+ import { app, BrowserWindow, dialog } from 'electron'
130
+ import { startupWithUpdater } from 'electron-incremental-update'
131
+ import { getAppVersion, getPathFromPreload, loadPage } from 'electron-incremental-update/utils'
129
132
 
130
- - certificate will read from `process.env.UPDATER_CERT` first, if absent, read config
131
- - privatekey will read from `process.env.UPDATER_PK` first, if absent, read config
133
+ export default startupWithUpdater(async (updater) => {
134
+ await app.whenReady()
132
135
 
133
- See all config in [types](#plugin)
136
+ const win = new BrowserWindow({
137
+ webPreferences: {
138
+ preload: getPathFromPreload('index.js'),
139
+ },
140
+ })
134
141
 
135
- in `vite.config.mts`
142
+ loadPage(win)
136
143
 
137
- ```ts
138
- import { debugStartup, electronWithUpdater } from 'electron-incremental-update/vite'
139
- import { defineConfig } from 'vite'
144
+ updater.on('update-available', async ({ version }) => {
145
+ const { response } = await dialog.showMessageBox({
146
+ type: 'info',
147
+ message: `Version ${version} is available. Current version is ${getAppVersion()}.`,
148
+ buttons: ['Download', 'Later'],
149
+ })
140
150
 
141
- export default defineConfig(async ({ command }) => {
142
- const isBuild = command === 'build'
143
- return {
144
- plugins: [
145
- electronWithUpdater({
146
- isBuild,
147
- entry: {
148
- files: ['./electron/entry.ts', './electron/native/index.ts'],
149
- },
150
- main: {
151
- files: ['./electron/main/index.ts', './electron/main/worker.ts'],
152
- // see https://github.com/electron-vite/electron-vite-vue/blob/85ed267c4851bf59f32888d766c0071661d4b94c/vite.config.ts#L22-L28
153
- onstart: debugStartup,
154
- },
155
- preload: {
156
- files: './electron/preload/index.ts',
157
- },
158
- updater: {
159
- // options
160
- },
161
- }),
162
- ],
163
- server:
164
- process.env.VSCODE_DEBUG &&
165
- (() => {
166
- const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL)
167
- return {
168
- host: url.hostname,
169
- port: +url.port,
170
- }
171
- })(),
172
- }
151
+ if (response === 0) {
152
+ await updater.downloadUpdate()
153
+ }
154
+ })
155
+
156
+ updater.on('download-progress', (info) => {
157
+ win.webContents.send('update-progress', info)
158
+ })
159
+
160
+ updater.on('update-downloaded', async () => {
161
+ const { response } = await dialog.showMessageBox({
162
+ type: 'info',
163
+ message: 'Update downloaded.',
164
+ buttons: ['Restart Now', 'Later'],
165
+ })
166
+
167
+ if (response === 0) {
168
+ updater.quitAndInstall()
169
+ }
170
+ })
171
+
172
+ updater.on('update-not-available', (code, message) => {
173
+ console.log(`[${code}] ${message}`)
174
+ })
175
+
176
+ updater.on('error', (error) => {
177
+ console.error(error)
178
+ })
179
+
180
+ await updater.checkForUpdates()
173
181
  })
174
182
  ```
175
183
 
176
- Or use the helper function
184
+ ## Vite Configuration
185
+
186
+ Use `defineElectronConfig()` when the same Vite config owns the renderer and Electron processes.
177
187
 
178
188
  ```ts
179
189
  import { defineElectronConfig } from 'electron-incremental-update/vite'
180
190
 
181
191
  export default defineElectronConfig({
182
192
  entry: {
183
- files: ['./electron/entry.ts', './electron/native/index.ts'],
193
+ files: './electron/entry.ts',
184
194
  },
185
195
  main: {
186
- files: ['./electron/main/index.ts', './electron/main/worker.ts'],
187
- // see https://github.com/electron-vite/electron-vite-vue/blob/85ed267c4851bf59f32888d766c0071661d4b94c/vite.config.ts#L22-L28
188
- onstart: debugStartup,
196
+ files: './electron/main/index.ts',
189
197
  },
190
198
  preload: {
191
199
  files: './electron/preload/index.ts',
192
200
  },
193
201
  updater: {
194
- // options
202
+ minimumVersion: '0.0.0',
203
+ paths: {
204
+ asarOutputPath: 'release/my-app.asar',
205
+ gzipPath: 'release/my-app-1.0.0.asar.gz',
206
+ versionPath: 'release/version.json',
207
+ },
195
208
  },
196
209
  renderer: {
197
- server:
198
- process.env.VSCODE_DEBUG &&
199
- (() => {
200
- const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL)
201
- return {
202
- host: url.hostname,
203
- port: +url.port,
210
+ server: process.env.VSCODE_DEBUG
211
+ ? {
212
+ host: '127.0.0.1',
213
+ port: 5173,
204
214
  }
205
- })(),
215
+ : undefined,
206
216
  },
207
217
  })
208
218
  ```
209
219
 
210
- ### Modify package.json
220
+ Use `electronWithUpdater()` directly when you want to manage the renderer config yourself.
221
+
222
+ ```ts
223
+ import { electronWithUpdater } from 'electron-incremental-update/vite'
224
+ import { defineConfig } from 'vite'
225
+
226
+ export default defineConfig({
227
+ plugins: [
228
+ electronWithUpdater({
229
+ entry: {
230
+ files: './electron/entry.ts',
231
+ },
232
+ main: {
233
+ files: './electron/main/index.ts',
234
+ },
235
+ preload: {
236
+ files: './electron/preload/index.ts',
237
+ },
238
+ }),
239
+ ],
240
+ })
241
+ ```
242
+
243
+ ### Plugin Options
244
+
245
+ Common options:
246
+
247
+ - `entry.files`: entry process input. Required.
248
+ - `main.files`: main process input. Required.
249
+ - `preload.files`: preload process input. Optional.
250
+ - `sourcemap`: defaults to development or `VSCODE_DEBUG`.
251
+ - `minify`: defaults to production builds.
252
+ - `bytecode`: `true` or bytecode options.
253
+ - `notBundle`: externalizes Node modules in development. Defaults to `true`.
254
+ - `external`: additional Vite/Rolldown externals. Use `true` to externalize `dependencies`.
255
+ - `buildVersionJson`: generates update JSON. Defaults to CI only.
256
+ - `localDevUpdate`: generates and serves a local update package during dev startup. Use `true`
257
+ for defaults, or pass `{ baseDir, packageJsonPath, chunkSize, chunkDelay }`. See [Local Development](#local-development) for details.
258
+ - `updater.minimumVersion`: minimum supported entry asar version. Defaults to `0.0.0`.
259
+
260
+ Default output paths:
261
+
262
+ - `updater.paths.asarOutputPath`: `release/${app.name}.asar`
263
+ - `updater.paths.gzipPath`: `release/${app.name}-${version}.asar.gz`
264
+ - `updater.paths.versionPath`: `release/version.json`
265
+ - `updater.paths.entryOutDir`: `dist-entry`
266
+ - `updater.paths.electronDistPath`: `dist-electron`
267
+ - `updater.paths.rendererDistPath`: `dist`
268
+
269
+ Signing keys:
270
+
271
+ - `updater.keys.privateKeyPath`: defaults to `keys/private.pem`
272
+ - `updater.keys.certPath`: defaults to `keys/cert.pem`
273
+ - `UPDATER_PK`: overrides `privateKeyPath`
274
+ - `UPDATER_CERT`: overrides `certPath`
275
+
276
+ The plugin creates a simple self-signed certificate when the key files are missing.
277
+
278
+ ## Electron Builder
279
+
280
+ Set `package.json#main` to the entry output file:
211
281
 
212
282
  ```json
213
283
  {
214
- "main": "dist-entry/entry.js" // <- entry file path
284
+ "main": "dist-entry/entry.js"
215
285
  }
216
286
  ```
217
287
 
218
- ### Config `electron-builder`
288
+ Minimal `electron-builder.config.cjs`:
219
289
 
220
290
  ```js
221
291
  const { name } = require('./package.json')
222
292
 
223
293
  const targetFile = `${name}.asar`
294
+
224
295
  /**
225
296
  * @type {import('electron-builder').Configuration}
226
297
  */
227
298
  module.exports = {
228
- appId: 'YourAppID',
299
+ appId: `org.${name}`,
229
300
  productName: name,
230
301
  files: [
231
302
  // entry files
@@ -243,168 +314,327 @@ module.exports = {
243
314
  }
244
315
  ```
245
316
 
246
- ## Usage
317
+ ## Runtime Usage
247
318
 
248
- ### Use In Main Process
319
+ ### Create The App
249
320
 
250
- The updater should be initialized in your main process. The startup function will automatically handle update checks and installation.
321
+ ```ts
322
+ import { createElectronApp } from 'electron-incremental-update'
251
323
 
252
- **NOTE: There should only be one function and should be the default export in the main index file**
324
+ createElectronApp({
325
+ updater: {
326
+ provider,
327
+ receiveBeta: false,
328
+ logger: console,
329
+ },
330
+ onInstall(install, tempAsarPath, appNameAsarPath, logger) {
331
+ logger?.info(`Installing ${tempAsarPath} to ${appNameAsarPath}`)
332
+ install()
333
+ },
334
+ onStartError(error, logger) {
335
+ logger?.error('Failed to start app', error)
336
+ },
337
+ })
338
+ ```
253
339
 
254
- in `electron/main/index.ts`
340
+ `updater` can also be an async factory:
255
341
 
256
342
  ```ts
257
- import { app, dialog } from 'electron'
258
- import { startupWithUpdater } from 'electron-incremental-update'
259
- import {
260
- getPathFromAppNameAsar,
261
- getAppVersion,
262
- getEntryVersion,
263
- } from 'electron-incremental-update/utils'
343
+ createElectronApp({
344
+ updater: async () => {
345
+ const { Updater } = await import('electron-incremental-update')
346
+ return new Updater({ provider })
347
+ },
348
+ })
349
+ ```
264
350
 
265
- export default startupWithUpdater(async (updater) => {
266
- await app.whenReady()
351
+ ### Updater API
267
352
 
268
- // Display current app information
269
- console.table({
270
- [`${app.name}.asar path:`]: getPathFromAppNameAsar(),
271
- 'app version:': getAppVersion(),
272
- 'entry (installer) version:': getEntryVersion(),
273
- 'electron version:': process.versions.electron,
274
- })
353
+ ```ts
354
+ updater.provider = provider
355
+ updater.receiveBeta = true
356
+ updater.logger = console
275
357
 
276
- // Listen for download progress
277
- updater.onDownloading = ({ percent }) => {
278
- console.log(`Download progress: ${percent}%`)
279
- }
358
+ await updater.checkForUpdates()
359
+ await updater.downloadUpdate()
360
+ updater.cancel()
361
+ updater.quitAndInstall()
362
+ ```
280
363
 
281
- // Listen for available updates
282
- updater.on('update-available', async ({ version }) => {
283
- const { response } = await dialog.showMessageBox({
284
- type: 'info',
285
- buttons: ['Download', 'Later'],
286
- message: `Version ${version} update available!`,
287
- })
288
- if (response === 0) {
289
- // 0 = Download button
290
- await updater.downloadUpdate()
291
- }
292
- })
364
+ Events:
293
365
 
294
- // Handle no updates available
295
- updater.on('update-not-available', (code, reason, info) => {
296
- console.log('No update available:', reason)
297
- })
366
+ - `update-available`: emitted with update info and current app/entry versions.
367
+ - `update-not-available`: emitted with a code, message, and optional update info.
368
+ - `download-progress`: emitted while downloading.
369
+ - `update-downloaded`: emitted after the temporary asar is ready.
370
+ - `update-cancelled`: emitted after cancellation.
371
+ - `error`: emitted with `UpdaterError`.
298
372
 
299
- // Handle download progress (alternative to onDownloading)
300
- updater.on('download-progress', (data) => {
301
- console.log('Download progress:', data)
302
- // Send progress to renderer if needed
303
- const [mainWindow] = BrowserWindow.getAllWindows()
304
- if (mainWindow) {
305
- mainWindow.webContents.send('update-progress', data)
306
- }
307
- })
373
+ ## Providers
308
374
 
309
- // Handle update completion
310
- updater.on('update-downloaded', () => {
311
- dialog
312
- .showMessageBox({
313
- type: 'info',
314
- message: 'Update downloaded successfully!',
315
- buttons: ['Restart Now', 'Later'],
316
- })
317
- .then(({ response }) => {
318
- if (response === 0) {
319
- updater.quitAndInstall()
320
- }
321
- })
322
- })
375
+ ### GitHub
323
376
 
324
- // Handle errors
325
- updater.on('error', (error) => {
326
- console.error('Update error:', error)
327
- dialog.showErrorBox('Update Error', error.message || 'Failed to check for updates')
328
- })
377
+ GitHub providers read update metadata from a repository or release and download the generated
378
+ `{name}-{version}.asar.gz` artifact from GitHub Releases.
379
+
380
+ #### Default
381
+
382
+ Reads `version.json` from the repository branch and downloads the asar from GitHub Releases.
383
+
384
+ ```ts
385
+ import { GitHubProvider } from 'electron-incremental-update/provider'
386
+
387
+ const provider = new GitHubProvider({
388
+ user: 'your-github-user',
389
+ repo: 'your-repo',
390
+ branch: 'HEAD',
391
+ })
392
+ ```
393
+
394
+ Default URLs:
395
+
396
+ - update JSON: `https://github.com/{user}/{repo}/raw/{branch}/{versionPath}`
397
+ - asar: `https://github.com/{user}/{repo}/releases/download/v{version}/{name}-{version}.asar.gz`
329
398
 
330
- // Start checking for updates
331
- updater.checkForUpdates()
399
+ #### Atom
400
+
401
+ Reads the latest release tag from `releases.atom`, then downloads the update JSON and asar from that release.
402
+
403
+ ```ts
404
+ import { GitHubAtomProvider } from 'electron-incremental-update/provider'
405
+
406
+ const provider = new GitHubAtomProvider({
407
+ user: 'your-github-user',
408
+ repo: 'your-repo',
409
+ })
410
+ ```
411
+
412
+ #### Api
413
+
414
+ Uses the GitHub Releases API and can use a token for private repositories or higher rate limits.
415
+
416
+ ```ts
417
+ import { GitHubApiProvider } from 'electron-incremental-update/provider'
418
+
419
+ const provider = new GitHubApiProvider({
420
+ user: 'your-github-user',
421
+ repo: 'your-repo',
422
+ token: process.env.GITHUB_TOKEN,
332
423
  })
333
424
  ```
334
425
 
335
- #### Alternative Provider Setup
426
+ #### URL Handling
336
427
 
337
- You can also change the provider dynamically:
428
+ GitHub providers support `urlHandler` for mirrors, custom gateways, or request rewriting.
338
429
 
339
430
  ```ts
340
- // In main.ts
341
- updater.provider = new GitHubProvider({
342
- owner: 'your-username',
431
+ const provider = new GitHubProvider({
432
+ user: 'your-github-user',
343
433
  repo: 'your-repo',
344
- // Custom URL handling for mirrors or private repos
345
- urlHandler: (url) => {
346
- url.hostname = 'mirror.ghproxy.com'
434
+ urlHandler(url) {
435
+ url.hostname = 'mirror.example.com'
347
436
  url.pathname = `https://github.com${url.pathname}`
348
437
  return url
349
438
  },
350
439
  })
351
440
  ```
352
441
 
353
- #### Custom logger
442
+ ### Local
443
+
444
+ Local updates are for development and manual update-flow testing. They let you test
445
+ `checkForUpdates()`, download progress, `update-downloaded`, and restart/install behavior without
446
+ publishing a GitHub release.
447
+
448
+ Most projects should use the Vite `localDevUpdate` option instead of manually creating a
449
+ `LocalDevProvider`.
450
+
451
+ #### Recommended Setup
452
+
453
+ Enable local update generation in `vite.config.ts`:
354
454
 
355
455
  ```ts
356
- updater.logger = console
456
+ import { electronWithUpdater } from 'electron-incremental-update/vite'
457
+ import { defineConfig } from 'vite'
458
+
459
+ export default defineConfig({
460
+ plugins: [
461
+ electronWithUpdater({
462
+ entry: {
463
+ files: './electron/entry.ts',
464
+ },
465
+ main: {
466
+ files: './electron/main/index.ts',
467
+ },
468
+ preload: {
469
+ files: './electron/preload/index.ts',
470
+ },
471
+ localDevUpdate: true,
472
+ }),
473
+ ],
474
+ })
357
475
  ```
358
476
 
359
- #### Beta Channel Updates
477
+ When `localDevUpdate` is enabled in development:
478
+
479
+ - the Vite plugin creates a local update package before Electron starts
480
+ - `createElectronApp()` auto-configures `LocalDevProvider` when no explicit `updater.provider` is set
481
+ - dev-only `forceUpdate` is enabled so update checks are not skipped in development
482
+ - production builds keep the normal provider and signature behavior
483
+
484
+ The default generated files are:
485
+
486
+ - `release/local-update/release/version.json`
487
+ - `release/local-update/{name}-{version}.asar.gz`
488
+ - `DEV.asar`
489
+ - `DEV.asar.tmp` after `downloadUpdate()`
490
+
491
+ The generated update version is the next patch version from the installed local dev asar. For
492
+ example, if `DEV.asar` contains version `1.2.3`, the generated update is `1.2.4`.
493
+
494
+ #### Custom Options
360
495
 
361
496
  ```ts
362
- updater.receiveBeta = true
497
+ export default defineConfig({
498
+ plugins: [
499
+ electronWithUpdater({
500
+ // ...
501
+ localDevUpdate: {
502
+ baseDir: 'release/local-update',
503
+ packageJsonPath: 'playground/package.json',
504
+ chunkSize: 32 * 1024,
505
+ chunkDelay: 50,
506
+ },
507
+ }),
508
+ ],
509
+ })
510
+ ```
511
+
512
+ Options:
513
+
514
+ - `baseDir`: directory for generated local update resources. Defaults to `release/local-update`.
515
+ - `packageJsonPath`: package metadata used for update name/version. Defaults to the project
516
+ `package.json`.
517
+ - `chunkSize`: bytes per simulated download progress chunk. Defaults to `64 * 1024`.
518
+ - `chunkDelay`: delay between progress chunks in milliseconds. Defaults to `30`.
519
+
520
+ `chunkSize` must be greater than `0`, and `chunkDelay` must be greater than or equal to `0`.
521
+
522
+ #### Manual Provider
523
+
524
+ Use `LocalDevProvider` directly only when you need to wire local artifacts yourself.
525
+
526
+ ```ts
527
+ import { LocalDevProvider } from 'electron-incremental-update/provider'
528
+
529
+ const provider = new LocalDevProvider({
530
+ baseDir: 'release/local-update',
531
+ chunkSize: 32 * 1024,
532
+ chunkDelay: 50,
533
+ })
534
+ ```
535
+
536
+ It reads:
537
+
538
+ - `{baseDir}/{versionPath}`
539
+ - `{baseDir}/{name}-{version}.asar.gz`
540
+
541
+ #### Testing The Local Flow
542
+
543
+ Use the [playground](./playground) as a complete local update test:
544
+
545
+ ```sh
546
+ bun run play
363
547
  ```
364
548
 
365
- ### Use Native Modules
549
+ `bun run play` builds the package first, then starts the Vite dev server and Electron playground.
550
+
551
+ Expected flow:
366
552
 
367
- To reduce production size, it is recommended that all the **native modules** should be set as `dependency` in `package.json` and other packages should be set as `devDependencies`. Also, `electron-rebuild` only check dependencies inside `dependency` field.
553
+ 1. The plugin builds the Electron main, preload, and entry outputs.
554
+ 2. The plugin creates `DEV.asar` and a local update archive.
555
+ 3. `createElectronApp()` installs any existing `DEV.asar.tmp`.
556
+ 4. The app starts from `DEV.asar`.
557
+ 5. The updater checks the generated `version.json`.
558
+ 6. The playground shows an available local update.
559
+ 7. Clicking download emits simulated progress events.
560
+ 8. Clicking restart installs `DEV.asar.tmp` and restarts Electron.
368
561
 
369
- If you are using `electron-builder` to build distributions, all the native modules with its **large relevant `node_modules`** will be packaged into `app.asar` by default.
562
+ Explicit `updater.provider` values still take priority. If you set a provider manually, automatic
563
+ local provider setup is skipped.
370
564
 
371
- Luckily, `vite` can bundle all the dependencies. Just follow the steps:
565
+ ## Update Artifacts
372
566
 
373
- 1. setup `entry.files` option
374
- 2. Manually copy the native binaries in `entry.postBuild` callback
375
- 3. Exclude all the dependencies in `electron-builder`'s config
376
- 4. call the native functions with `requireNative` / `importNative` in your code
567
+ The Vite plugin can generate:
377
568
 
378
- #### Example
569
+ - `${app.name}.asar`
570
+ - `${app.name}-${version}.asar.gz`
571
+ - `version.json`
379
572
 
380
- in `vite.config.ts`
573
+ Default `version.json` shape:
574
+
575
+ ```json
576
+ {
577
+ "version": "1.0.0",
578
+ "minimumVersion": "0.0.0",
579
+ "signature": "...",
580
+ "beta": {
581
+ "version": "1.0.1-beta.1",
582
+ "minimumVersion": "0.0.0",
583
+ "signature": "..."
584
+ }
585
+ }
586
+ ```
587
+
588
+ Stable releases update both the top-level fields and `beta`. Prerelease versions update only `beta`.
589
+
590
+ Set `buildVersionJson: true` if you need metadata during non-CI builds.
591
+
592
+ ## Native Modules
593
+
594
+ 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()`.
381
595
 
382
596
  ```ts
383
- const plugin = electronWithUpdater({
384
- // options...
385
- entry: {
386
- files: ['./electron/native/entry.ts', './electron/native/db.ts', './electron/native/img.ts'],
387
- postBuild: ({ copyToEntryOutputDir, copyModules }) => {
388
- // for better-sqlite3
389
- copyToEntryOutputDir({
390
- from: './node_modules/better-sqlite3/build/Release/better_sqlite3.node',
391
- skipIfExist: false,
392
- })
393
- // for @napi-rs/image
394
- const startStr = '@napi-rs+image-'
395
- const fileName = readdirSync('./node_modules/.pnpm').find((p) => p.startsWith(startStr))!
396
- const archName = fileName.substring(startStr.length).split('@')[0]
397
- copyToEntryOutputDir({
398
- from: `./node_modules/.pnpm/${fileName}/node_modules/@napi-rs/image-${archName}/image.${archName}.node`,
399
- })
400
- // or just copy specific dependency
401
- copyModules({ modules: ['better-sqlite3'] })
402
- },
403
- },
404
- })
597
+ import { readdirSync } from 'node:fs'
598
+
599
+ import { electronWithUpdater } from 'electron-incremental-update/vite'
600
+
601
+ export default {
602
+ plugins: [
603
+ electronWithUpdater({
604
+ external: false,
605
+ entry: {
606
+ files: ['./electron/entry.ts', './electron/native/db.ts'],
607
+ postBuild({ isBuild, copyToEntryOutputDir }) {
608
+ if (!isBuild) {
609
+ return
610
+ }
611
+
612
+ copyToEntryOutputDir({
613
+ from: './node_modules/better-sqlite3/build/Release/better_sqlite3.node',
614
+ skipIfExist: false,
615
+ })
616
+
617
+ const packageName = readdirSync('./node_modules/.pnpm').find((name) =>
618
+ name.startsWith('@napi-rs+image-'),
619
+ )
620
+
621
+ if (packageName) {
622
+ const archName = packageName.substring('@napi-rs+image-'.length).split('@')[0]
623
+ copyToEntryOutputDir({
624
+ from: `./node_modules/.pnpm/${packageName}/node_modules/@napi-rs/image-${archName}/image.${archName}.node`,
625
+ })
626
+ }
627
+ },
628
+ },
629
+ main: {
630
+ files: './electron/main/index.ts',
631
+ },
632
+ }),
633
+ ],
634
+ }
405
635
  ```
406
636
 
407
- in `electron/native/db.ts`
637
+ Use the copied native binding from entry asar:
408
638
 
409
639
  ```ts
410
640
  import Database from 'better-sqlite3'
@@ -414,38 +644,27 @@ const db = new Database(':memory:', {
414
644
  nativeBinding: getPathFromEntryAsar('./better_sqlite3.node'),
415
645
  })
416
646
 
417
- export function test(): void {
418
- db.exec(
419
- 'DROP TABLE IF EXISTS employees; ' +
420
- 'CREATE TABLE IF NOT EXISTS employees (name TEXT, salary INTEGER)',
421
- )
422
-
423
- db.prepare('INSERT INTO employees VALUES (:n, :s)').run({
424
- n: 'James',
425
- s: 5000,
647
+ export function testDatabase(): void {
648
+ db.exec('CREATE TABLE IF NOT EXISTS employees (name TEXT, salary INTEGER)')
649
+ db.prepare('INSERT INTO employees VALUES (:name, :salary)').run({
650
+ name: 'James',
651
+ salary: 5000,
426
652
  })
427
-
428
- const r = db.prepare('SELECT * from employees').all()
429
- console.log(r)
430
- // [ { name: 'James', salary: 50000 } ]
431
-
432
- db.close()
433
653
  }
434
654
  ```
435
655
 
436
- in `electron/main/service.ts`
656
+ Load native helper modules from main:
437
657
 
438
658
  ```ts
439
659
  import { importNative, requireNative } from 'electron-incremental-update/utils'
440
660
 
441
- // commonjs
442
- requireNative<typeof import('../native/db')>('db').test()
661
+ requireNative<typeof import('../native/db')>('db').testDatabase()
443
662
 
444
- // esm
445
- importNative<typeof import('../native/db')>('db').test()
663
+ const nativeDb = await importNative<typeof import('../native/db')>('db')
664
+ nativeDb.testDatabase()
446
665
  ```
447
666
 
448
- in `electron-builder.config.js`
667
+ For `electron-builder`, exclude `node_modules` when you have bundled dependencies manually:
449
668
 
450
669
  ```js
451
670
  module.exports = {
@@ -525,114 +744,103 @@ After: Clean 😍
525
744
  └── package.json
526
745
  ```
527
746
 
528
- ### Bytecode Protection
747
+ ## Bytecode Protection
529
748
 
530
- Use V8 cache to protect the source code
749
+ Bytecode protection compiles JavaScript into V8 bytecode.
531
750
 
532
751
  ```ts
533
752
  electronWithUpdater({
534
- // ...
535
- bytecode: true, // or options
753
+ bytecode: true,
754
+ entry: {
755
+ files: './electron/entry.ts',
756
+ },
757
+ main: {
758
+ files: './electron/main/index.ts',
759
+ },
536
760
  })
537
761
  ```
538
762
 
539
- #### Benefits
763
+ Notes:
540
764
 
541
- https://electron-vite.org/guide/source-code-protection
765
+ - CommonJS only. Remove `"type": "module"` from `package.json` when enabling bytecode.
766
+ - Main process bytecode is enabled by default.
767
+ - To include preload scripts, use `bytecode: { enablePreload: true }`.
768
+ - If preload bytecode is enabled, create the `BrowserWindow` with `sandbox: false`.
542
769
 
543
- - Improve the string protection (see [original issue](https://github.com/alex8088/electron-vite/issues/552))
544
- - Protect all strings by default
545
- - Minification is allowed
770
+ ## Development Bundling
546
771
 
547
- #### Limitation
772
+ `notBundle` is enabled by default in development. It externalizes Node modules in entry and main builds to improve startup speed.
548
773
 
549
- - Only support commonjs
550
- - Only for main process by default, if you want to use in preload script, please use `electronWithUpdater({ bytecode: { enablePreload: true } })` and set `sandbox: false` when creating window
774
+ ```ts
775
+ electronWithUpdater({
776
+ notBundle: false,
777
+ entry: {
778
+ files: './electron/entry.ts',
779
+ },
780
+ main: {
781
+ files: './electron/main/index.ts',
782
+ },
783
+ })
784
+ ```
551
785
 
552
- ### Utils
786
+ ## Utilities
553
787
 
554
- Utility functions of Electron helper, crypto, and file compression.
788
+ Import from `electron-incremental-update/utils`:
555
789
 
556
790
  ```ts
557
791
  import {
558
- // Electron utilities
559
- isDev,
560
- isWin,
561
- isMac,
562
- isLinux,
792
+ aesDecrypt,
793
+ aesEncrypt,
794
+ beautifyDevTools,
795
+ defaultIsLowerVersion,
796
+ defaultSignature,
797
+ defaultUnzipFile,
798
+ defaultVerifySignature,
799
+ defaultZipFile,
800
+ getAppVersion,
801
+ getEntryVersion,
563
802
  getPathFromAppNameAsar,
564
803
  getPathFromEntryAsar,
565
804
  getPathFromMain,
566
805
  getPathFromPreload,
567
806
  getPathFromPublic,
568
- getAppVersion,
569
- getEntryVersion,
570
- requireNative,
807
+ handleUnexpectedErrors,
808
+ hashBuffer,
571
809
  importNative,
810
+ isDev,
811
+ isLinux,
812
+ isMac,
813
+ isWin,
814
+ loadPage,
815
+ parseVersion,
816
+ requireNative,
572
817
  restartApp,
573
818
  setAppUserModelId,
574
- disableHWAccForWin7,
575
- singleInstance,
576
819
  setPortableDataPath,
577
- loadPage,
578
- beautifyDevTools,
579
- handleUnexpectedErrors,
580
-
581
- // Crypto utilities
582
- hashBuffer,
583
- aesEncrypt,
584
- aesDecrypt,
585
- defaultSignature,
586
- defaultVerifySignature,
587
-
588
- // Zip utilities
589
- defaultZipFile,
590
- defaultUnzipFile,
820
+ singleInstance,
591
821
  } from 'electron-incremental-update/utils'
592
822
  ```
593
823
 
594
- #### Electron Utilities
595
-
596
- - **isDev** - Compile-time dev check
597
- - **isWin** - Check if running on Windows
598
- - **isMac** - Check if running on macOS
599
- - **isLinux** - Check if running on Linux
600
- - **getPathFromAppNameAsar(...paths)** - Get joined path of `${electron.app.name}.asar`
601
- - **getPathFromEntryAsar(...paths)** - Get joined path from entry asar
602
- - **getPathFromMain(...paths)** - Get joined path from main dir
603
- - **getPathFromPreload(...paths)** - Get joined path from preload dir
604
- - **getPathFromPublic(...paths)** - Get joined path from public dir
605
- - **getAppVersion()** - Get app version (returns entry version in dev)
606
- - **getEntryVersion()** - Get entry version
607
- - **requireNative(moduleName)** - Load native module using require from entry asar
608
- - **importNative(moduleName)** - Load native module using import from entry asar
609
- - **restartApp()** - Restart the Electron app
610
- - **setAppUserModelId(id)** - Fix app model ID (Windows only)
611
- - **disableHWAccForWin7()** - Disable hardware acceleration for Windows 7
612
- - **singleInstance(window)** - Keep single instance and restore window
613
- - **setPortableDataPath(dirName, create)** - Set userData dir to exe dir for portable apps
614
- - **loadPage(win, htmlFilePath)** - Load dev server URL in dev or HTML file otherwise
615
- - **beautifyDevTools(win, options)** - Beautify devtools font and scrollbar
616
- - **handleUnexpectedErrors(callback)** - Handle all unhandled errors
617
-
618
- #### Crypto Utilities
619
-
620
- - **hashBuffer(data, length)** - Hash data using SHA-256
621
- - **aesEncrypt(plainText, key, iv)** - Encrypt text using AES
622
- - **aesDecrypt(encryptedText, key, iv)** - Decrypt text using AES
623
- - **defaultSignature(buffer, privateKey, cert, version)** - Generate RSA signature for asar file
624
- - **defaultVerifySignature(buffer, version, signature, cert)** - Verify RSA signature of asar file
625
-
626
- #### Zip Utilities
627
-
628
- - **defaultZipFile(buffer)** - Compress file using brotli
629
- - **defaultUnzipFile(buffer)** - Decompress file using brotli
824
+ Common helpers:
825
+
826
+ - `getPathFromAppNameAsar(...paths)`: path inside `${app.name}.asar`.
827
+ - `getPathFromEntryAsar(...paths)`: path inside `app.asar`.
828
+ - `getPathFromMain(...paths)`: path inside the built main directory.
829
+ - `getPathFromPreload(...paths)`: path inside the built preload directory.
830
+ - `getPathFromPublic(...paths)`: path inside `public` in dev or renderer output in production.
831
+ - `getAppVersion()`: current app version. In dev, returns the entry version.
832
+ - `getEntryVersion()`: version from Electron app metadata.
833
+ - `loadPage(win, htmlFilePath)`: loads `VITE_DEV_SERVER_URL` in dev or renderer HTML in production.
834
+ - `singleInstance(window)`: restores and focuses the window on `second-instance`.
835
+ - `setPortableDataPath(dirName, create)`: stores user data beside the executable for portable apps.
836
+ - `requireNative(moduleName)`: loads a CommonJS native helper from entry asar.
837
+ - `importNative(moduleName)`: imports an ES module native helper from entry asar.
630
838
 
631
839
  ## Credits
632
840
 
633
- - [Obsidian](https://obsidian.md/) for upgrade strategy
634
- - [vite-plugin-electron](https://github.com/electron-vite/vite-plugin-electron) for vite plugin
635
- - [electron-builder](https://github.com/electron-userland/electron-builder) for update api
841
+ - [Obsidian](https://obsidian.md/) for the dual asar update strategy
842
+ - [vite-plugin-electron](https://github.com/electron-vite/vite-plugin-electron) for the Vite Electron plugin foundation
843
+ - [electron-builder](https://github.com/electron-userland/electron-builder) for Electron packaging
636
844
  - [electron-vite](https://github.com/alex8088/electron-vite) for bytecode plugin inspiration
637
845
 
638
846
  ## License