electron-incremental-update 2.2.5 → 2.3.0

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,1030 +1,1030 @@
1
- ## Electron Incremental Update
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.
4
-
5
- ### Key Features
6
-
7
- The solution includes a Vite plugin, a startup entry function, an `Updater` class, and a set of utilities for Electron.
8
-
9
- It use 2 asar file structure for updates:
10
-
11
- - `app.asar`: The application entry, loads the `${electron.app.name}.asar` and initializes the updater on startup
12
- - `${electron.app.name}.asar`: The package that contains main / preload / renderer process code
13
-
14
- ### Update Steps
15
-
16
- 1. Check update from remote server
17
- 2. If update available, download the update asar, verify by presigned RSA + Signature and write to disk
18
- 3. Quit and restart the app
19
- 4. Replace the old `${electron.app.name}.asar` on startup and load the new one
20
-
21
- ### Other Features
22
-
23
- - Update size reduction: All **native modules** should be packaged into `app.asar` to reduce `${electron.app.name}.asar` file size, [see usage](#use-native-modules)
24
- - Bytecode protection: Use V8 cache to protect source code, [see details](#bytecode-protection)
25
-
26
- ## Getting Started
27
-
28
- ### Install
29
-
30
- ```sh
31
- npm install -D electron-incremental-update
32
- ```
33
- ```sh
34
- yarn add -D electron-incremental-update
35
- ```
36
- ```sh
37
- pnpm add -D electron-incremental-update
38
- ```
39
-
40
- ### Project Structure
41
-
42
- Base on [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue)
43
-
44
- ```
45
- electron
46
- ├── entry.ts // <- entry file
47
- ├── main
48
- │ └── index.ts
49
- ├── preload
50
- │ └── index.ts
51
- └── native // <- possible native modules
52
- └── index.ts
53
- src
54
- └── ...
55
- ```
56
-
57
- ### Setup Entry
58
-
59
- The entry is used to load the application and initialize the `Updater`
60
-
61
- `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
62
-
63
- in `electron/entry.ts`
64
-
65
- ```ts
66
- import { createElectronApp } from 'electron-incremental-update'
67
- import { GitHubProvider } from 'electron-incremental-update/provider'
68
-
69
- createElectronApp({
70
- updater: {
71
- // optinal, you can setup later
72
- provider: new GitHubProvider({
73
- username: 'yourname',
74
- repo: 'electron',
75
- }),
76
- },
77
- beforeStart(mainFilePath, logger) {
78
- logger?.debug(mainFilePath)
79
- },
80
- })
81
- ```
82
-
83
- - [some Github CDN resources](https://github.com/XIU2/UserScript/blob/master/GithubEnhanced-High-Speed-Download.user.js#L34)
84
-
85
- ### Setup `vite.config.ts`
86
-
87
- The plugin config, `main` and `preload` parts are reference from [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue)
88
-
89
- - certificate will read from `process.env.UPDATER_CERT` first, if absend, read config
90
- - privatekey will read from `process.env.UPDATER_PK` first, if absend, read config
91
-
92
- See all config in [types](#plugin)
93
-
94
- in `vite.config.mts`
95
-
96
- ```ts
97
- import { debugStartup, electronWithUpdater } from 'electron-incremental-update/vite'
98
- import { defineConfig } from 'vite'
99
-
100
- export default defineConfig(async ({ command }) => {
101
- const isBuild = command === 'build'
102
- return {
103
- plugins: [
104
- electronWithUpdater({
105
- isBuild,
106
- logParsedOptions: true,
107
- main: {
108
- files: ['./electron/main/index.ts', './electron/main/worker.ts'],
109
- // see https://github.com/electron-vite/electron-vite-vue/blob/85ed267c4851bf59f32888d766c0071661d4b94c/vite.config.ts#L22-L28
110
- onstart: debugStartup,
111
- },
112
- preload: {
113
- files: './electron/preload/index.ts',
114
- },
115
- updater: {
116
- // options
117
- }
118
- }),
119
- ],
120
- server: process.env.VSCODE_DEBUG && (() => {
121
- const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL)
122
- return {
123
- host: url.hostname,
124
- port: +url.port,
125
- }
126
- })(),
127
- }
128
- })
129
- ```
130
-
131
- ### Modify package.json
132
-
133
- ```json
134
- {
135
- "main": "dist-entry/entry.js" // <- entry file path
136
- }
137
- ```
138
-
139
- ### Config `electron-builder`
140
-
141
- ```js
142
- const { name } = require('./package.json')
143
-
144
- const targetFile = `${name}.asar`
145
- /**
146
- * @type {import('electron-builder').Configuration}
147
- */
148
- module.exports = {
149
- appId: 'YourAppID',
150
- productName: name,
151
- files: [
152
- // entry files
153
- 'dist-entry',
154
- ],
155
- npmRebuild: false,
156
- asarUnpack: [
157
- '**/*.{node,dll,dylib,so}',
158
- ],
159
- directories: {
160
- output: 'release',
161
- },
162
- extraResources: [
163
- { from: `release/${targetFile}`, to: targetFile }, // <- asar file
164
- ],
165
- // disable publish
166
- publish: null,
167
- }
168
- ```
169
-
170
- ## Usage
171
-
172
- ### Use In Main Process
173
-
174
- In most cases, you should also setup the `UpdateProvider` before updating, unless you setup params when calling `checkUpdate` or `downloadUpdate`.
175
-
176
- The update steps are similar to [electron-updater](https://github.com/electron-userland/electron-updater) and have same methods and events on `Updater`
177
-
178
- **NOTE: There should only one function and should be default export in the main index file**
179
-
180
- in `electron/main/index.ts`
181
-
182
- ```ts
183
- import { app } from 'electron'
184
- import { startupWithUpdater, UpdaterError } from 'electron-incremental-update'
185
- import { getPathFromAppNameAsar, getVersions } from 'electron-incremental-update/utils'
186
-
187
- export default startupWithUpdater((updater) => {
188
- await app.whenReady()
189
-
190
- console.table({
191
- [`${app.name}.asar path:`]: getPathFromAppNameAsar(),
192
- 'app version:': getAppVersion(),
193
- 'entry (installer) version:': getEntryVersion(),
194
- 'electron version:': process.versions.electron,
195
- })
196
-
197
- updater.onDownloading = ({ percent }) => {
198
- console.log(percent)
199
- }
200
-
201
- updater.on('update-available', async ({ version }) => {
202
- const { response } = await dialog.showMessageBox({
203
- type: 'info',
204
- buttons: ['Download', 'Later'],
205
- message: `v${version} update available!`,
206
- })
207
- if (response !== 0) {
208
- return
209
- }
210
- await updater.downloadUpdate()
211
- })
212
-
213
- updater.on('update-not-available', (code, reason, info) => console.log(code, reason, info))
214
-
215
- updater.on('download-progress', (data) => {
216
- console.log(data)
217
- main.send(BrowserWindow.getAllWindows()[0], 'msg', data)
218
- })
219
-
220
- updater.on('update-downloaded', () => {
221
- updater.quitAndInstall()
222
- })
223
-
224
- updater.checkForUpdates()
225
- })
226
- ```
227
-
228
- #### Dynamicly setup `UpdateProvider`
229
-
230
- ```ts
231
- updater.provider = new GitHubProvider({
232
- user: 'yourname',
233
- repo: 'electron',
234
- // setup url handler
235
- urlHandler: (url) => {
236
- url.hostname = 'mirror.ghproxy.com'
237
- url.pathname = `https://github.com${url.pathname}`
238
- return url
239
- }
240
- })
241
- ```
242
-
243
- #### Custom logger
244
-
245
- ```ts
246
- updater.logger = console
247
- ```
248
-
249
- #### Setup Beta Channel
250
-
251
- ```ts
252
- updater.receiveBeta = true
253
- ```
254
-
255
- ### Use Native Modules
256
-
257
- 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.
258
-
259
- If you are using `electron-builder` to build distributions, all the native modules with its **large relavent `node_modiles`** will be packaged into `app.asar` by default.
260
-
261
- Luckily, `vite` can bundle all the dependencies. Just follow the steps:
262
-
263
- 1. setup `nativeModuleEntryMap` option
264
- 2. Manually copy the native binaries in `postBuild` callback
265
- 3. Exclude all the dependencies in `electron-builder`'s config
266
- 4. call the native functions with `requireNative` / `importNative` in your code
267
-
268
- #### Example
269
-
270
- in `vite.config.ts`
271
-
272
- ```ts
273
- const plugin = electronWithUpdater({
274
- // options...
275
- updater: {
276
- entry: {
277
- nativeModuleEntryMap: {
278
- db: './electron/native/db.ts',
279
- img: './electron/native/img.ts',
280
- },
281
- postBuild: async ({ copyToEntryOutputDir, copyModules }) => {
282
- // for better-sqlite3
283
- copyToEntryOutputDir({
284
- from: './node_modules/better-sqlite3/build/Release/better_sqlite3.node',
285
- skipIfExist: false,
286
- })
287
- // for @napi-rs/image
288
- const startStr = '@napi-rs+image-'
289
- const fileName = (await readdir('./node_modules/.pnpm')).filter(p => p.startsWith(startStr))[0]
290
- const archName = fileName.substring(startStr.length).split('@')[0]
291
- copyToEntryOutputDir({
292
- from: `./node_modules/.pnpm/${fileName}/node_modules/@napi-rs/image-${archName}/image.${archName}.node`,
293
- })
294
- // or just copy specific dependency
295
- copyModules({ modules: ['better-sqlite3'] })
296
- },
297
- },
298
- },
299
- })
300
- ```
301
-
302
- in `electron/native/db.ts`
303
-
304
- ```ts
305
- import Database from 'better-sqlite3'
306
- import { getPathFromEntryAsar } from 'electron-incremental-update/utils'
307
-
308
- const db = new Database(':memory:', { nativeBinding: getPathFromEntryAsar('./better_sqlite3.node') })
309
-
310
- export function test(): void {
311
- db.exec(
312
- 'DROP TABLE IF EXISTS employees; '
313
- + 'CREATE TABLE IF NOT EXISTS employees (name TEXT, salary INTEGER)',
314
- )
315
-
316
- db.prepare('INSERT INTO employees VALUES (:n, :s)').run({
317
- n: 'James',
318
- s: 5000,
319
- })
320
-
321
- const r = db.prepare('SELECT * from employees').all()
322
- console.log(r)
323
- // [ { name: 'James', salary: 50000 } ]
324
-
325
- db.close()
326
- }
327
- ```
328
-
329
- in `electron/main/service.ts`
330
-
331
- ```ts
332
- import { importNative, requireNative } from 'electron-incremental-update/utils'
333
-
334
- // commonjs
335
- requireNative<typeof import('../native/db')>('db').test()
336
-
337
- // esm
338
- importNative<typeof import('../native/db')>('db').test()
339
- ```
340
-
341
- in `electron-builder.config.js`
342
-
343
- ```js
344
- module.exports = {
345
- files: [
346
- 'dist-entry',
347
- // exclude all dependencies in electron-builder config
348
- '!node_modules/**',
349
- ]
350
- }
351
- ```
352
-
353
- ### Bytecode Protection
354
-
355
- Use V8 cache to protect the source code
356
-
357
- ```ts
358
- electronWithUpdater({
359
- // ...
360
- bytecode: true, // or options
361
- })
362
- ```
363
-
364
- #### Benifits
365
-
366
- https://electron-vite.org/guide/source-code-protection
367
-
368
- - Improve the string protection (see [original issue](https://github.com/alex8088/electron-vite/issues/552))
369
- - Protect all strings by default
370
- - Minification is allowed
371
-
372
- #### Limitation
373
-
374
- - Only support commonjs
375
- - 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
376
-
377
- ### Utils
378
-
379
- ```ts
380
- /**
381
- * Compile time dev check
382
- */
383
- const isDev: boolean
384
- const isWin: boolean
385
- const isMac: boolean
386
- const isLinux: boolean
387
- /**
388
- * Get joined path of `${electron.app.name}.asar` (not `app.asar`)
389
- *
390
- * If is in dev, **always** return `'DEV.asar'`
391
- */
392
- function getPathFromAppNameAsar(...paths: string[]): string
393
- /**
394
- * Get app version, if is in dev, return `getEntryVersion()`
395
- */
396
- function getAppVersion(): string
397
- /**
398
- * Get entry version
399
- */
400
- function getEntryVersion(): string
401
- /**
402
- * Use `require` to load native module from entry asar
403
- * @param moduleName file name in entry
404
- * @example
405
- * requireNative<typeof import('../native/db')>('db')
406
- */
407
- function requireNative<T = any>(moduleName: string): T
408
- /**
409
- * Use `import` to load native module from entry asar
410
- * @param moduleName file name in entry
411
- * @example
412
- * await importNative<typeof import('../native/db')>('db')
413
- */
414
- function importNative<T = any>(moduleName: string): Promise<T>
415
- /**
416
- * Restarts the Electron app.
417
- */
418
- function restartApp(): void
419
- /**
420
- * Fix app use model id, only for Windows
421
- * @param id app id, default is `org.${electron.app.name}`
422
- */
423
- function setAppUserModelId(id?: string): void
424
- /**
425
- * Disable hardware acceleration for Windows 7
426
- *
427
- * Only support CommonJS
428
- */
429
- function disableHWAccForWin7(): void
430
- /**
431
- * Keep single electron instance and auto restore window on `second-instance` event
432
- * @param window brwoser window to show
433
- */
434
- function singleInstance(window?: BrowserWindow): void
435
- /**
436
- * Set `AppData` dir to the dir of .exe file
437
- *
438
- * Useful for portable Windows app
439
- * @param dirName dir name, default to `data`
440
- */
441
- function setPortableAppDataPath(dirName?: string): void
442
- /**
443
- * Load `process.env.VITE_DEV_SERVER_URL` when dev, else load html file
444
- * @param win window
445
- * @param htmlFilePath html file path, default is `index.html`
446
- */
447
- function loadPage(win: BrowserWindow, htmlFilePath?: string): void
448
- interface BeautifyDevToolsOptions {
449
- /**
450
- * Sans-serif font family
451
- */
452
- sans: string
453
- /**
454
- * Monospace font family
455
- */
456
- mono: string
457
- /**
458
- * Whether to round scrollbar
459
- */
460
- scrollbar?: boolean
461
- }
462
- /**
463
- * Beautify devtools' font and scrollbar
464
- * @param win target window
465
- * @param options sans font family, mono font family and scrollbar
466
- */
467
- function beautifyDevTools(win: BrowserWindow, options: BeautifyDevToolsOptions): void
468
- /**
469
- * Get joined path from main dir
470
- * @param paths rest paths
471
- */
472
- function getPathFromMain(...paths: string[]): string
473
- /**
474
- * Get joined path from preload dir
475
- * @param paths rest paths
476
- */
477
- function getPathFromPreload(...paths: string[]): string
478
- /**
479
- * Get joined path from publich dir
480
- * @param paths rest paths
481
- */
482
- function getPathFromPublic(...paths: string[]): string
483
- /**
484
- * Get joined path from entry asar
485
- * @param paths rest paths
486
- */
487
- function getPathFromEntryAsar(...paths: string[]): string
488
- /**
489
- * Handle all unhandled error
490
- * @param callback callback function
491
- */
492
- function handleUnexpectedErrors(callback: (err: unknown) => void): void
493
- /**
494
- * Safe get value from header
495
- * @param headers response header
496
- * @param key target header key
497
- */
498
- function getHeader(headers: Record<string, Arrayable<string>>, key: any): any
499
- function downloadUtil<T>(
500
- url: string,
501
- headers: Record<string, any>,
502
- signal: AbortSignal,
503
- onResponse: (
504
- resp: IncomingMessage,
505
- resolve: (data: T) => void,
506
- reject: (e: any) => void
507
- ) => void
508
- ): Promise<T>
509
- /**
510
- * Default function to download json and parse to UpdateJson
511
- * @param url target url
512
- * @param headers extra headers
513
- * @param signal abort signal
514
- * @param resolveData on resolve
515
- */
516
- function defaultDownloadJSON<T>(
517
- url: string,
518
- headers: Record<string, any>,
519
- signal: AbortSignal,
520
- resolveData?: ResolveDataFn
521
- ): Promise<T>
522
- /**
523
- * Default function to download json and parse to UpdateJson
524
- * @param url target url
525
- * @param headers extra headers
526
- * @param signal abort signal
527
- */
528
- function defaultDownloadUpdateJSON(
529
- url: string,
530
- headers: Record<string, any>,
531
- signal: AbortSignal
532
- ): Promise<UpdateJSON>
533
- /**
534
- * Default function to download asar buffer,
535
- * get total size from `Content-Length` header
536
- * @param url target url
537
- * @param headers extra headers
538
- * @param signal abort signal
539
- * @param onDownloading on downloading callback
540
- */
541
- function defaultDownloadAsar(
542
- url: string,
543
- headers: Record<string, any>,
544
- signal: AbortSignal,
545
- onDownloading?: (progress: DownloadingInfo) => void
546
- ): Promise<Buffer>
547
- ```
548
-
549
- ### Types
550
-
551
- #### Entry
552
-
553
- ```ts
554
- export interface AppOption {
555
- /**
556
- * Path to index file that make {@link startupWithUpdater} as default export
557
- *
558
- * Generate from plugin configuration by default
559
- */
560
- mainPath?: string
561
- /**
562
- * Updater options
563
- */
564
- updater?: (() => Promisable<Updater>) | UpdaterOption
565
- /**
566
- * Hooks on rename temp asar path to `${app.name}.asar`
567
- */
568
- onInstall?: OnInstallFunction
569
- /**
570
- * Hooks before app startup
571
- * @param mainFilePath main file path of `${app.name}.asar`
572
- * @param logger logger
573
- */
574
- beforeStart?: (mainFilePath: string, logger?: Logger) => Promisable<void>
575
- /**
576
- * Hooks on app startup error
577
- * @param err installing or startup error
578
- * @param logger logger
579
- */
580
- onStartError?: (err: unknown, logger?: Logger) => void
581
- }
582
- /**
583
- * Hooks on rename temp asar path to `${app.name}.asar`
584
- * @param install `() => renameSync(tempAsarPath, appNameAsarPath)`
585
- * @param tempAsarPath temp(updated) asar path
586
- * @param appNameAsarPath `${app.name}.asar` path
587
- * @param logger logger
588
- * @default install(); logger.info('update success!')
589
- */
590
- type OnInstallFunction = (
591
- install: VoidFunction,
592
- tempAsarPath: string,
593
- appNameAsarPath: string,
594
- logger?: Logger
595
- ) => Promisable<void>
596
- ```
597
-
598
- #### Updater
599
-
600
- ```ts
601
- export interface UpdaterOption {
602
- /**
603
- * Update provider
604
- *
605
- * If you will not setup `UpdateJSON` or `Buffer` in params when checking update or download, this option is **required**
606
- */
607
- provider?: IProvider
608
- /**
609
- * Certifaction key of signature, which will be auto generated by plugin,
610
- * generate by `selfsigned` if not set
611
- */
612
- SIGNATURE_CERT?: string
613
- /**
614
- * Whether to receive beta update
615
- */
616
- receiveBeta?: boolean
617
- /**
618
- * Updater logger
619
- */
620
- logger?: Logger
621
- }
622
-
623
- export type Logger = {
624
- info: (msg: string) => void
625
- debug: (msg: string) => void
626
- warn: (msg: string) => void
627
- error: (msg: string, e?: Error) => void
628
- }
629
- ```
630
- #### Provider
631
-
632
- ```ts
633
- export type OnDownloading = (progress: DownloadingInfo) => void
634
-
635
- export interface DownloadingInfo {
636
- /**
637
- * Download buffer delta
638
- */
639
- delta: number
640
- /**
641
- * Downloaded percent, 0 ~ 100
642
- *
643
- * If no `Content-Length` header, will be -1
644
- */
645
- percent: number
646
- /**
647
- * Total size
648
- *
649
- * If not `Content-Length` header, will be -1
650
- */
651
- total: number
652
- /**
653
- * Downloaded size
654
- */
655
- transferred: number
656
- /**
657
- * Download speed, bytes per second
658
- */
659
- bps: number
660
- }
661
-
662
- export interface IProvider {
663
- /**
664
- * Provider name
665
- */
666
- name: string
667
- /**
668
- * Download update json
669
- * @param versionPath parsed version path in project
670
- * @param signal abort signal
671
- */
672
- downloadJSON: (versionPath: string, signal: AbortSignal) => Promise<UpdateJSON>
673
- /**
674
- * Download update asar
675
- * @param name app name
676
- * @param updateInfo existing update info
677
- * @param signal abort signal
678
- * @param onDownloading hook for on downloading
679
- */
680
- downloadAsar: (
681
- name: string,
682
- updateInfo: UpdateInfo,
683
- signal: AbortSignal,
684
- onDownloading?: (info: DownloadingInfo) => void
685
- ) => Promise<Buffer>
686
- /**
687
- * Check the old version is less than new version
688
- * @param oldVer old version string
689
- * @param newVer new version string
690
- */
691
- isLowerVersion: (oldVer: string, newVer: string) => boolean
692
- /**
693
- * Function to decompress file using brotli
694
- * @param buffer compressed file buffer
695
- */
696
- unzipFile: (buffer: Buffer) => Promise<Buffer>
697
- /**
698
- * Verify asar signature,
699
- * if signature is valid, returns the version, otherwise returns `undefined`
700
- * @param buffer file buffer
701
- * @param version target version
702
- * @param signature signature
703
- * @param cert certificate
704
- */
705
- verifySignaure: (buffer: Buffer, version: string, signature: string, cert: string) => Promisable<boolean>
706
- }
707
- ```
708
-
709
- #### Plugin
710
-
711
- ```ts
712
- export interface ElectronWithUpdaterOptions {
713
- /**
714
- * Whether is in build mode
715
- * ```ts
716
- * export default defineConfig(({ command }) => {
717
- * const isBuild = command === 'build'
718
- * })
719
- * ```
720
- */
721
- isBuild: boolean
722
- /**
723
- * Manually setup package.json, read name, version and main,
724
- * use `local-pkg` of `loadPackageJSON()` to load package.json by default
725
- * ```ts
726
- * import pkg from './package.json'
727
- * ```
728
- */
729
- pkg?: PKG
730
- /**
731
- * Whether to generate sourcemap
732
- * @default !isBuild
733
- */
734
- sourcemap?: boolean
735
- /**
736
- * Whether to minify the code
737
- * @default isBuild
738
- */
739
- minify?: boolean
740
- /**
741
- * Whether to generate bytecode
742
- *
743
- * **Only support CommonJS**
744
- *
745
- * Only 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
746
- */
747
- bytecode?: boolean | BytecodeOptions
748
- /**
749
- * Use `NotBundle()` plugin in main
750
- * @default true
751
- */
752
- useNotBundle?: boolean
753
- /**
754
- * Whether to generate version json
755
- * @default isCI
756
- */
757
- buildVersionJson?: boolean
758
- /**
759
- * Whether to log parsed options
760
- *
761
- * To show certificate and private keys, set `logParsedOptions: { showKeys: true }`
762
- */
763
- logParsedOptions?: boolean | { showKeys: boolean }
764
- /**
765
- * Main process options
766
- *
767
- * To change output directories, use `options.updater.paths.electronDistPath` instead
768
- */
769
- main: MakeRequiredAndReplaceKey<ElectronSimpleOptions['main'], 'entry', 'files'> & ExcludeOutputDirOptions
770
- /**
771
- * Preload process options
772
- *
773
- * To change output directories, use `options.updater.paths.electronDistPath` instead
774
- */
775
- preload: MakeRequiredAndReplaceKey<Exclude<ElectronSimpleOptions['preload'], undefined>, 'input', 'files'> & ExcludeOutputDirOptions
776
- /**
777
- * Updater options
778
- */
779
- updater?: ElectronUpdaterOptions
780
- }
781
-
782
- export interface ElectronUpdaterOptions {
783
- /**
784
- * Minimum version of entry
785
- * @default '0.0.0'
786
- */
787
- minimumVersion?: string
788
- /**
789
- * Options for entry (app.asar)
790
- */
791
- entry?: BuildEntryOption
792
- /**
793
- * Options for paths
794
- */
795
- paths?: {
796
- /**
797
- * Path to asar file
798
- * @default `release/${app.name}.asar`
799
- */
800
- asarOutputPath?: string
801
- /**
802
- * Path to version info output, content is {@link UpdateJSON}
803
- * @default `version.json`
804
- */
805
- versionPath?: string
806
- /**
807
- * Path to gzipped asar file
808
- * @default `release/${app.name}-${version}.asar.gz`
809
- */
810
- gzipPath?: string
811
- /**
812
- * Path to electron build output
813
- * @default `dist-electron`
814
- */
815
- electronDistPath?: string
816
- /**
817
- * Path to renderer build output
818
- * @default `dist`
819
- */
820
- rendererDistPath?: string
821
- }
822
- /**
823
- * signature config
824
- */
825
- keys?: {
826
- /**
827
- * Path to the pem file that contains private key
828
- * If not ended with .pem, it will be appended
829
- *
830
- * **If `UPDATER_PK` is set, will read it instead of read from `privateKeyPath`**
831
- * @default 'keys/private.pem'
832
- */
833
- privateKeyPath?: string
834
- /**
835
- * Path to the pem file that contains public key
836
- * If not ended with .pem, it will be appended
837
- *
838
- * **If `UPDATER_CERT` is set, will read it instead of read from `certPath`**
839
- * @default 'keys/cert.pem'
840
- */
841
- certPath?: string
842
- /**
843
- * Length of the key
844
- * @default 2048
845
- */
846
- keyLength?: number
847
- /**
848
- * X509 certificate info
849
- *
850
- * only generate simple **self-signed** certificate **without extensions**
851
- */
852
- certInfo?: {
853
- /**
854
- * The subject of the certificate
855
- *
856
- * @default { commonName: `${app.name}`, organizationName: `org.${app.name}` }
857
- */
858
- subject?: DistinguishedName
859
- /**
860
- * Expire days of the certificate
861
- *
862
- * @default 3650
863
- */
864
- days?: number
865
- }
866
- }
867
- overrideGenerator?: GeneratorOverrideFunctions
868
- }
869
-
870
- export interface BytecodeOptions {
871
- enable: boolean
872
- /**
873
- * Enable in preload script. Remember to set `sandbox: false` when creating window
874
- */
875
- preload?: boolean
876
- /**
877
- * Custom electron binary path
878
- */
879
- electronPath?: string
880
- /**
881
- * Before transformed code compile function. If return `Falsy` value, it will be ignored
882
- * @param code transformed code
883
- * @param id file path
884
- */
885
- beforeCompile?: (code: string, id: string) => Promisable<string | null | undefined | void>
886
- }
887
-
888
- export interface BuildEntryOption {
889
- /**
890
- * Override to minify on entry
891
- * @default isBuild
892
- */
893
- minify?: boolean
894
- /**
895
- * Override to generate sourcemap on entry
896
- */
897
- sourcemap?: boolean
898
- /**
899
- * Path to app entry output file
900
- * @default 'dist-entry'
901
- */
902
- entryOutputDirPath?: string
903
- /**
904
- * Path to app entry file
905
- * @default 'electron/entry.ts'
906
- */
907
- appEntryPath?: string
908
- /**
909
- * Esbuild path map of native modules in entry directory
910
- *
911
- * @default {}
912
- * @example
913
- * { db: './electron/native/db.ts' }
914
- */
915
- nativeModuleEntryMap?: Record<string, string>
916
- /**
917
- * Skip process dynamic require
918
- *
919
- * Useful for `better-sqlite3` and other old packages
920
- */
921
- ignoreDynamicRequires?: boolean
922
- /**
923
- * `external` option in `build.rollupOptions`, external `.node` by default
924
- */
925
- external?: string | string[] | ((source: string, importer: string | undefined, isResolved: boolean) => boolean | null | undefined | void)
926
- /**
927
- * Custom options for `vite` build
928
- * ```ts
929
- * const options = {
930
- * plugins: [esm(), bytecodePlugin()], // load on needed
931
- * build: {
932
- * sourcemap,
933
- * minify,
934
- * outDir: entryOutputDirPath,
935
- * commonjsOptions: { ignoreDynamicRequires },
936
- * rollupOptions: { external },
937
- * },
938
- * define,
939
- * }
940
- * ```
941
- */
942
- overrideViteOptions?: InlineConfig
943
- /**
944
- * Resolve extra files on startup, such as `.node`
945
- * @remark won't trigger will reload
946
- */
947
- postBuild?: (args: {
948
- /**
949
- * Get path from `entryOutputDirPath`
950
- */
951
- getPathFromEntryOutputDir: (...paths: string[]) => string
952
- /**
953
- * Check exist and copy file to `entryOutputDirPath`
954
- *
955
- * If `to` absent, set to `basename(from)`
956
- *
957
- * If `skipIfExist` absent, skip copy if `to` exist
958
- */
959
- copyToEntryOutputDir: (options: {
960
- from: string
961
- to?: string
962
- /**
963
- * Skip copy if `to` exist
964
- * @default true
965
- */
966
- skipIfExist?: boolean
967
- }) => void
968
- /**
969
- * Copy specified modules to entry output dir, just like `external` option in rollup
970
- */
971
- copyModules: (options: {
972
- /**
973
- * External Modules
974
- */
975
- modules: string[]
976
- /**
977
- * Skip copy if `to` exist
978
- * @default true
979
- */
980
- skipIfExist?: boolean
981
- }) => void
982
- }) => Promisable<void>
983
- }
984
-
985
- export interface GeneratorOverrideFunctions {
986
- /**
987
- * Custom signature generate function
988
- * @param buffer file buffer
989
- * @param privateKey private key
990
- * @param cert certificate string, **EOL must be '\n'**
991
- * @param version current version
992
- */
993
- generateSignature?: (
994
- buffer: Buffer,
995
- privateKey: string,
996
- cert: string,
997
- version: string
998
- ) => Promisable<string>
999
- /**
1000
- * Custom generate update json function
1001
- * @param existingJson The existing JSON object.
1002
- * @param buffer file buffer
1003
- * @param signature generated signature
1004
- * @param version current version
1005
- * @param minVersion The minimum version
1006
- */
1007
- generateUpdateJson?: (
1008
- existingJson: UpdateJSON,
1009
- signature: string,
1010
- version: string,
1011
- minVersion: string
1012
- ) => Promisable<UpdateJSON>
1013
- /**
1014
- * Custom generate zip file buffer
1015
- * @param buffer source buffer
1016
- */
1017
- generateGzipFile?: (buffer: Buffer) => Promisable<Buffer>
1018
- }
1019
- ```
1020
-
1021
- ## Credits
1022
-
1023
- - [Obsidian](https://obsidian.md/) for upgrade strategy
1024
- - [vite-plugin-electron](https://github.com/electron-vite/vite-plugin-electron) for vite plugin
1025
- - [electron-builder](https://github.com/electron-userland/electron-builder) for update api
1026
- - [electron-vite](https://github.com/alex8088/electron-vite) for bytecode plugin inspiration
1027
-
1028
- ## License
1029
-
1030
- MIT
1
+ ## Electron Incremental Update
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.
4
+
5
+ ### Key Features
6
+
7
+ The solution includes a Vite plugin, a startup entry function, an `Updater` class, and a set of utilities for Electron.
8
+
9
+ It use 2 asar file structure for updates:
10
+
11
+ - `app.asar`: The application entry, loads the `${electron.app.name}.asar` and initializes the updater on startup
12
+ - `${electron.app.name}.asar`: The package that contains main / preload / renderer process code
13
+
14
+ ### Update Steps
15
+
16
+ 1. Check update from remote server
17
+ 2. If update available, download the update asar, verify by presigned RSA + Signature and write to disk
18
+ 3. Quit and restart the app
19
+ 4. Replace the old `${electron.app.name}.asar` on startup and load the new one
20
+
21
+ ### Other Features
22
+
23
+ - Update size reduction: All **native modules** should be packaged into `app.asar` to reduce `${electron.app.name}.asar` file size, [see usage](#use-native-modules)
24
+ - Bytecode protection: Use V8 cache to protect source code, [see details](#bytecode-protection)
25
+
26
+ ## Getting Started
27
+
28
+ ### Install
29
+
30
+ ```sh
31
+ npm install -D electron-incremental-update
32
+ ```
33
+ ```sh
34
+ yarn add -D electron-incremental-update
35
+ ```
36
+ ```sh
37
+ pnpm add -D electron-incremental-update
38
+ ```
39
+
40
+ ### Project Structure
41
+
42
+ Base on [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue)
43
+
44
+ ```
45
+ electron
46
+ ├── entry.ts // <- entry file
47
+ ├── main
48
+ │ └── index.ts
49
+ ├── preload
50
+ │ └── index.ts
51
+ └── native // <- possible native modules
52
+ └── index.ts
53
+ src
54
+ └── ...
55
+ ```
56
+
57
+ ### Setup Entry
58
+
59
+ The entry is used to load the application and initialize the `Updater`
60
+
61
+ `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
62
+
63
+ in `electron/entry.ts`
64
+
65
+ ```ts
66
+ import { createElectronApp } from 'electron-incremental-update'
67
+ import { GitHubProvider } from 'electron-incremental-update/provider'
68
+
69
+ createElectronApp({
70
+ updater: {
71
+ // optinal, you can setup later
72
+ provider: new GitHubProvider({
73
+ username: 'yourname',
74
+ repo: 'electron',
75
+ }),
76
+ },
77
+ beforeStart(mainFilePath, logger) {
78
+ logger?.debug(mainFilePath)
79
+ },
80
+ })
81
+ ```
82
+
83
+ - [some Github CDN resources](https://github.com/XIU2/UserScript/blob/master/GithubEnhanced-High-Speed-Download.user.js#L34)
84
+
85
+ ### Setup `vite.config.ts`
86
+
87
+ The plugin config, `main` and `preload` parts are reference from [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue)
88
+
89
+ - certificate will read from `process.env.UPDATER_CERT` first, if absend, read config
90
+ - privatekey will read from `process.env.UPDATER_PK` first, if absend, read config
91
+
92
+ See all config in [types](#plugin)
93
+
94
+ in `vite.config.mts`
95
+
96
+ ```ts
97
+ import { debugStartup, electronWithUpdater } from 'electron-incremental-update/vite'
98
+ import { defineConfig } from 'vite'
99
+
100
+ export default defineConfig(async ({ command }) => {
101
+ const isBuild = command === 'build'
102
+ return {
103
+ plugins: [
104
+ electronWithUpdater({
105
+ isBuild,
106
+ logParsedOptions: true,
107
+ main: {
108
+ files: ['./electron/main/index.ts', './electron/main/worker.ts'],
109
+ // see https://github.com/electron-vite/electron-vite-vue/blob/85ed267c4851bf59f32888d766c0071661d4b94c/vite.config.ts#L22-L28
110
+ onstart: debugStartup,
111
+ },
112
+ preload: {
113
+ files: './electron/preload/index.ts',
114
+ },
115
+ updater: {
116
+ // options
117
+ }
118
+ }),
119
+ ],
120
+ server: process.env.VSCODE_DEBUG && (() => {
121
+ const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL)
122
+ return {
123
+ host: url.hostname,
124
+ port: +url.port,
125
+ }
126
+ })(),
127
+ }
128
+ })
129
+ ```
130
+
131
+ ### Modify package.json
132
+
133
+ ```json
134
+ {
135
+ "main": "dist-entry/entry.js" // <- entry file path
136
+ }
137
+ ```
138
+
139
+ ### Config `electron-builder`
140
+
141
+ ```js
142
+ const { name } = require('./package.json')
143
+
144
+ const targetFile = `${name}.asar`
145
+ /**
146
+ * @type {import('electron-builder').Configuration}
147
+ */
148
+ module.exports = {
149
+ appId: 'YourAppID',
150
+ productName: name,
151
+ files: [
152
+ // entry files
153
+ 'dist-entry',
154
+ ],
155
+ npmRebuild: false,
156
+ asarUnpack: [
157
+ '**/*.{node,dll,dylib,so}',
158
+ ],
159
+ directories: {
160
+ output: 'release',
161
+ },
162
+ extraResources: [
163
+ { from: `release/${targetFile}`, to: targetFile }, // <- asar file
164
+ ],
165
+ // disable publish
166
+ publish: null,
167
+ }
168
+ ```
169
+
170
+ ## Usage
171
+
172
+ ### Use In Main Process
173
+
174
+ In most cases, you should also setup the `UpdateProvider` before updating, unless you setup params when calling `checkUpdate` or `downloadUpdate`.
175
+
176
+ The update steps are similar to [electron-updater](https://github.com/electron-userland/electron-updater) and have same methods and events on `Updater`
177
+
178
+ **NOTE: There should only one function and should be default export in the main index file**
179
+
180
+ in `electron/main/index.ts`
181
+
182
+ ```ts
183
+ import { app } from 'electron'
184
+ import { startupWithUpdater, UpdaterError } from 'electron-incremental-update'
185
+ import { getPathFromAppNameAsar, getVersions } from 'electron-incremental-update/utils'
186
+
187
+ export default startupWithUpdater((updater) => {
188
+ await app.whenReady()
189
+
190
+ console.table({
191
+ [`${app.name}.asar path:`]: getPathFromAppNameAsar(),
192
+ 'app version:': getAppVersion(),
193
+ 'entry (installer) version:': getEntryVersion(),
194
+ 'electron version:': process.versions.electron,
195
+ })
196
+
197
+ updater.onDownloading = ({ percent }) => {
198
+ console.log(percent)
199
+ }
200
+
201
+ updater.on('update-available', async ({ version }) => {
202
+ const { response } = await dialog.showMessageBox({
203
+ type: 'info',
204
+ buttons: ['Download', 'Later'],
205
+ message: `v${version} update available!`,
206
+ })
207
+ if (response !== 0) {
208
+ return
209
+ }
210
+ await updater.downloadUpdate()
211
+ })
212
+
213
+ updater.on('update-not-available', (code, reason, info) => console.log(code, reason, info))
214
+
215
+ updater.on('download-progress', (data) => {
216
+ console.log(data)
217
+ main.send(BrowserWindow.getAllWindows()[0], 'msg', data)
218
+ })
219
+
220
+ updater.on('update-downloaded', () => {
221
+ updater.quitAndInstall()
222
+ })
223
+
224
+ updater.checkForUpdates()
225
+ })
226
+ ```
227
+
228
+ #### Dynamicly setup `UpdateProvider`
229
+
230
+ ```ts
231
+ updater.provider = new GitHubProvider({
232
+ user: 'yourname',
233
+ repo: 'electron',
234
+ // setup url handler
235
+ urlHandler: (url) => {
236
+ url.hostname = 'mirror.ghproxy.com'
237
+ url.pathname = `https://github.com${url.pathname}`
238
+ return url
239
+ }
240
+ })
241
+ ```
242
+
243
+ #### Custom logger
244
+
245
+ ```ts
246
+ updater.logger = console
247
+ ```
248
+
249
+ #### Setup Beta Channel
250
+
251
+ ```ts
252
+ updater.receiveBeta = true
253
+ ```
254
+
255
+ ### Use Native Modules
256
+
257
+ 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.
258
+
259
+ If you are using `electron-builder` to build distributions, all the native modules with its **large relavent `node_modiles`** will be packaged into `app.asar` by default.
260
+
261
+ Luckily, `vite` can bundle all the dependencies. Just follow the steps:
262
+
263
+ 1. setup `nativeModuleEntryMap` option
264
+ 2. Manually copy the native binaries in `postBuild` callback
265
+ 3. Exclude all the dependencies in `electron-builder`'s config
266
+ 4. call the native functions with `requireNative` / `importNative` in your code
267
+
268
+ #### Example
269
+
270
+ in `vite.config.ts`
271
+
272
+ ```ts
273
+ const plugin = electronWithUpdater({
274
+ // options...
275
+ updater: {
276
+ entry: {
277
+ nativeModuleEntryMap: {
278
+ db: './electron/native/db.ts',
279
+ img: './electron/native/img.ts',
280
+ },
281
+ postBuild: async ({ copyToEntryOutputDir, copyModules }) => {
282
+ // for better-sqlite3
283
+ copyToEntryOutputDir({
284
+ from: './node_modules/better-sqlite3/build/Release/better_sqlite3.node',
285
+ skipIfExist: false,
286
+ })
287
+ // for @napi-rs/image
288
+ const startStr = '@napi-rs+image-'
289
+ const fileName = (await readdir('./node_modules/.pnpm')).filter(p => p.startsWith(startStr))[0]
290
+ const archName = fileName.substring(startStr.length).split('@')[0]
291
+ copyToEntryOutputDir({
292
+ from: `./node_modules/.pnpm/${fileName}/node_modules/@napi-rs/image-${archName}/image.${archName}.node`,
293
+ })
294
+ // or just copy specific dependency
295
+ copyModules({ modules: ['better-sqlite3'] })
296
+ },
297
+ },
298
+ },
299
+ })
300
+ ```
301
+
302
+ in `electron/native/db.ts`
303
+
304
+ ```ts
305
+ import Database from 'better-sqlite3'
306
+ import { getPathFromEntryAsar } from 'electron-incremental-update/utils'
307
+
308
+ const db = new Database(':memory:', { nativeBinding: getPathFromEntryAsar('./better_sqlite3.node') })
309
+
310
+ export function test(): void {
311
+ db.exec(
312
+ 'DROP TABLE IF EXISTS employees; '
313
+ + 'CREATE TABLE IF NOT EXISTS employees (name TEXT, salary INTEGER)',
314
+ )
315
+
316
+ db.prepare('INSERT INTO employees VALUES (:n, :s)').run({
317
+ n: 'James',
318
+ s: 5000,
319
+ })
320
+
321
+ const r = db.prepare('SELECT * from employees').all()
322
+ console.log(r)
323
+ // [ { name: 'James', salary: 50000 } ]
324
+
325
+ db.close()
326
+ }
327
+ ```
328
+
329
+ in `electron/main/service.ts`
330
+
331
+ ```ts
332
+ import { importNative, requireNative } from 'electron-incremental-update/utils'
333
+
334
+ // commonjs
335
+ requireNative<typeof import('../native/db')>('db').test()
336
+
337
+ // esm
338
+ importNative<typeof import('../native/db')>('db').test()
339
+ ```
340
+
341
+ in `electron-builder.config.js`
342
+
343
+ ```js
344
+ module.exports = {
345
+ files: [
346
+ 'dist-entry',
347
+ // exclude all dependencies in electron-builder config
348
+ '!node_modules/**',
349
+ ]
350
+ }
351
+ ```
352
+
353
+ ### Bytecode Protection
354
+
355
+ Use V8 cache to protect the source code
356
+
357
+ ```ts
358
+ electronWithUpdater({
359
+ // ...
360
+ bytecode: true, // or options
361
+ })
362
+ ```
363
+
364
+ #### Benifits
365
+
366
+ https://electron-vite.org/guide/source-code-protection
367
+
368
+ - Improve the string protection (see [original issue](https://github.com/alex8088/electron-vite/issues/552))
369
+ - Protect all strings by default
370
+ - Minification is allowed
371
+
372
+ #### Limitation
373
+
374
+ - Only support commonjs
375
+ - 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
376
+
377
+ ### Utils
378
+
379
+ ```ts
380
+ /**
381
+ * Compile time dev check
382
+ */
383
+ const isDev: boolean
384
+ const isWin: boolean
385
+ const isMac: boolean
386
+ const isLinux: boolean
387
+ /**
388
+ * Get joined path of `${electron.app.name}.asar` (not `app.asar`)
389
+ *
390
+ * If is in dev, **always** return `'DEV.asar'`
391
+ */
392
+ function getPathFromAppNameAsar(...paths: string[]): string
393
+ /**
394
+ * Get app version, if is in dev, return `getEntryVersion()`
395
+ */
396
+ function getAppVersion(): string
397
+ /**
398
+ * Get entry version
399
+ */
400
+ function getEntryVersion(): string
401
+ /**
402
+ * Use `require` to load native module from entry asar
403
+ * @param moduleName file name in entry
404
+ * @example
405
+ * requireNative<typeof import('../native/db')>('db')
406
+ */
407
+ function requireNative<T = any>(moduleName: string): T
408
+ /**
409
+ * Use `import` to load native module from entry asar
410
+ * @param moduleName file name in entry
411
+ * @example
412
+ * await importNative<typeof import('../native/db')>('db')
413
+ */
414
+ function importNative<T = any>(moduleName: string): Promise<T>
415
+ /**
416
+ * Restarts the Electron app.
417
+ */
418
+ function restartApp(): void
419
+ /**
420
+ * Fix app use model id, only for Windows
421
+ * @param id app id, default is `org.${electron.app.name}`
422
+ */
423
+ function setAppUserModelId(id?: string): void
424
+ /**
425
+ * Disable hardware acceleration for Windows 7
426
+ *
427
+ * Only support CommonJS
428
+ */
429
+ function disableHWAccForWin7(): void
430
+ /**
431
+ * Keep single electron instance and auto restore window on `second-instance` event
432
+ * @param window brwoser window to show
433
+ */
434
+ function singleInstance(window?: BrowserWindow): void
435
+ /**
436
+ * Set `AppData` dir to the dir of .exe file
437
+ *
438
+ * Useful for portable Windows app
439
+ * @param dirName dir name, default to `data`
440
+ */
441
+ function setPortableAppDataPath(dirName?: string): void
442
+ /**
443
+ * Load `process.env.VITE_DEV_SERVER_URL` when dev, else load html file
444
+ * @param win window
445
+ * @param htmlFilePath html file path, default is `index.html`
446
+ */
447
+ function loadPage(win: BrowserWindow, htmlFilePath?: string): void
448
+ interface BeautifyDevToolsOptions {
449
+ /**
450
+ * Sans-serif font family
451
+ */
452
+ sans: string
453
+ /**
454
+ * Monospace font family
455
+ */
456
+ mono: string
457
+ /**
458
+ * Whether to round scrollbar
459
+ */
460
+ scrollbar?: boolean
461
+ }
462
+ /**
463
+ * Beautify devtools' font and scrollbar
464
+ * @param win target window
465
+ * @param options sans font family, mono font family and scrollbar
466
+ */
467
+ function beautifyDevTools(win: BrowserWindow, options: BeautifyDevToolsOptions): void
468
+ /**
469
+ * Get joined path from main dir
470
+ * @param paths rest paths
471
+ */
472
+ function getPathFromMain(...paths: string[]): string
473
+ /**
474
+ * Get joined path from preload dir
475
+ * @param paths rest paths
476
+ */
477
+ function getPathFromPreload(...paths: string[]): string
478
+ /**
479
+ * Get joined path from publich dir
480
+ * @param paths rest paths
481
+ */
482
+ function getPathFromPublic(...paths: string[]): string
483
+ /**
484
+ * Get joined path from entry asar
485
+ * @param paths rest paths
486
+ */
487
+ function getPathFromEntryAsar(...paths: string[]): string
488
+ /**
489
+ * Handle all unhandled error
490
+ * @param callback callback function
491
+ */
492
+ function handleUnexpectedErrors(callback: (err: unknown) => void): void
493
+ /**
494
+ * Safe get value from header
495
+ * @param headers response header
496
+ * @param key target header key
497
+ */
498
+ function getHeader(headers: Record<string, Arrayable<string>>, key: any): any
499
+ function downloadUtil<T>(
500
+ url: string,
501
+ headers: Record<string, any>,
502
+ signal: AbortSignal,
503
+ onResponse: (
504
+ resp: IncomingMessage,
505
+ resolve: (data: T) => void,
506
+ reject: (e: any) => void
507
+ ) => void
508
+ ): Promise<T>
509
+ /**
510
+ * Default function to download json and parse to UpdateJson
511
+ * @param url target url
512
+ * @param headers extra headers
513
+ * @param signal abort signal
514
+ * @param resolveData on resolve
515
+ */
516
+ function defaultDownloadJSON<T>(
517
+ url: string,
518
+ headers: Record<string, any>,
519
+ signal: AbortSignal,
520
+ resolveData?: ResolveDataFn
521
+ ): Promise<T>
522
+ /**
523
+ * Default function to download json and parse to UpdateJson
524
+ * @param url target url
525
+ * @param headers extra headers
526
+ * @param signal abort signal
527
+ */
528
+ function defaultDownloadUpdateJSON(
529
+ url: string,
530
+ headers: Record<string, any>,
531
+ signal: AbortSignal
532
+ ): Promise<UpdateJSON>
533
+ /**
534
+ * Default function to download asar buffer,
535
+ * get total size from `Content-Length` header
536
+ * @param url target url
537
+ * @param headers extra headers
538
+ * @param signal abort signal
539
+ * @param onDownloading on downloading callback
540
+ */
541
+ function defaultDownloadAsar(
542
+ url: string,
543
+ headers: Record<string, any>,
544
+ signal: AbortSignal,
545
+ onDownloading?: (progress: DownloadingInfo) => void
546
+ ): Promise<Buffer>
547
+ ```
548
+
549
+ ### Types
550
+
551
+ #### Entry
552
+
553
+ ```ts
554
+ export interface AppOption {
555
+ /**
556
+ * Path to index file that make {@link startupWithUpdater} as default export
557
+ *
558
+ * Generate from plugin configuration by default
559
+ */
560
+ mainPath?: string
561
+ /**
562
+ * Updater options
563
+ */
564
+ updater?: (() => Promisable<Updater>) | UpdaterOption
565
+ /**
566
+ * Hooks on rename temp asar path to `${app.name}.asar`
567
+ */
568
+ onInstall?: OnInstallFunction
569
+ /**
570
+ * Hooks before app startup
571
+ * @param mainFilePath main file path of `${app.name}.asar`
572
+ * @param logger logger
573
+ */
574
+ beforeStart?: (mainFilePath: string, logger?: Logger) => Promisable<void>
575
+ /**
576
+ * Hooks on app startup error
577
+ * @param err installing or startup error
578
+ * @param logger logger
579
+ */
580
+ onStartError?: (err: unknown, logger?: Logger) => void
581
+ }
582
+ /**
583
+ * Hooks on rename temp asar path to `${app.name}.asar`
584
+ * @param install `() => renameSync(tempAsarPath, appNameAsarPath)`
585
+ * @param tempAsarPath temp(updated) asar path
586
+ * @param appNameAsarPath `${app.name}.asar` path
587
+ * @param logger logger
588
+ * @default install(); logger.info('update success!')
589
+ */
590
+ type OnInstallFunction = (
591
+ install: VoidFunction,
592
+ tempAsarPath: string,
593
+ appNameAsarPath: string,
594
+ logger?: Logger
595
+ ) => Promisable<void>
596
+ ```
597
+
598
+ #### Updater
599
+
600
+ ```ts
601
+ export interface UpdaterOption {
602
+ /**
603
+ * Update provider
604
+ *
605
+ * If you will not setup `UpdateJSON` or `Buffer` in params when checking update or download, this option is **required**
606
+ */
607
+ provider?: IProvider
608
+ /**
609
+ * Certifaction key of signature, which will be auto generated by plugin,
610
+ * generate by `selfsigned` if not set
611
+ */
612
+ SIGNATURE_CERT?: string
613
+ /**
614
+ * Whether to receive beta update
615
+ */
616
+ receiveBeta?: boolean
617
+ /**
618
+ * Updater logger
619
+ */
620
+ logger?: Logger
621
+ }
622
+
623
+ export type Logger = {
624
+ info: (msg: string) => void
625
+ debug: (msg: string) => void
626
+ warn: (msg: string) => void
627
+ error: (msg: string, e?: Error) => void
628
+ }
629
+ ```
630
+ #### Provider
631
+
632
+ ```ts
633
+ export type OnDownloading = (progress: DownloadingInfo) => void
634
+
635
+ export interface DownloadingInfo {
636
+ /**
637
+ * Download buffer delta
638
+ */
639
+ delta: number
640
+ /**
641
+ * Downloaded percent, 0 ~ 100
642
+ *
643
+ * If no `Content-Length` header, will be -1
644
+ */
645
+ percent: number
646
+ /**
647
+ * Total size
648
+ *
649
+ * If not `Content-Length` header, will be -1
650
+ */
651
+ total: number
652
+ /**
653
+ * Downloaded size
654
+ */
655
+ transferred: number
656
+ /**
657
+ * Download speed, bytes per second
658
+ */
659
+ bps: number
660
+ }
661
+
662
+ export interface IProvider {
663
+ /**
664
+ * Provider name
665
+ */
666
+ name: string
667
+ /**
668
+ * Download update json
669
+ * @param versionPath parsed version path in project
670
+ * @param signal abort signal
671
+ */
672
+ downloadJSON: (versionPath: string, signal: AbortSignal) => Promise<UpdateJSON>
673
+ /**
674
+ * Download update asar
675
+ * @param name app name
676
+ * @param updateInfo existing update info
677
+ * @param signal abort signal
678
+ * @param onDownloading hook for on downloading
679
+ */
680
+ downloadAsar: (
681
+ name: string,
682
+ updateInfo: UpdateInfo,
683
+ signal: AbortSignal,
684
+ onDownloading?: (info: DownloadingInfo) => void
685
+ ) => Promise<Buffer>
686
+ /**
687
+ * Check the old version is less than new version
688
+ * @param oldVer old version string
689
+ * @param newVer new version string
690
+ */
691
+ isLowerVersion: (oldVer: string, newVer: string) => boolean
692
+ /**
693
+ * Function to decompress file using brotli
694
+ * @param buffer compressed file buffer
695
+ */
696
+ unzipFile: (buffer: Buffer) => Promise<Buffer>
697
+ /**
698
+ * Verify asar signature,
699
+ * if signature is valid, returns the version, otherwise returns `undefined`
700
+ * @param buffer file buffer
701
+ * @param version target version
702
+ * @param signature signature
703
+ * @param cert certificate
704
+ */
705
+ verifySignaure: (buffer: Buffer, version: string, signature: string, cert: string) => Promisable<boolean>
706
+ }
707
+ ```
708
+
709
+ #### Plugin
710
+
711
+ ```ts
712
+ export interface ElectronWithUpdaterOptions {
713
+ /**
714
+ * Whether is in build mode
715
+ * ```ts
716
+ * export default defineConfig(({ command }) => {
717
+ * const isBuild = command === 'build'
718
+ * })
719
+ * ```
720
+ */
721
+ isBuild: boolean
722
+ /**
723
+ * Manually setup package.json, read name, version and main,
724
+ * use `local-pkg` of `loadPackageJSON()` to load package.json by default
725
+ * ```ts
726
+ * import pkg from './package.json'
727
+ * ```
728
+ */
729
+ pkg?: PKG
730
+ /**
731
+ * Whether to generate sourcemap
732
+ * @default !isBuild
733
+ */
734
+ sourcemap?: boolean
735
+ /**
736
+ * Whether to minify the code
737
+ * @default isBuild
738
+ */
739
+ minify?: boolean
740
+ /**
741
+ * Whether to generate bytecode
742
+ *
743
+ * **Only support CommonJS**
744
+ *
745
+ * Only 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
746
+ */
747
+ bytecode?: boolean | BytecodeOptions
748
+ /**
749
+ * Use `NotBundle()` plugin in main
750
+ * @default true
751
+ */
752
+ useNotBundle?: boolean
753
+ /**
754
+ * Whether to generate version json
755
+ * @default isCI
756
+ */
757
+ buildVersionJson?: boolean
758
+ /**
759
+ * Whether to log parsed options
760
+ *
761
+ * To show certificate and private keys, set `logParsedOptions: { showKeys: true }`
762
+ */
763
+ logParsedOptions?: boolean | { showKeys: boolean }
764
+ /**
765
+ * Main process options
766
+ *
767
+ * To change output directories, use `options.updater.paths.electronDistPath` instead
768
+ */
769
+ main: MakeRequiredAndReplaceKey<ElectronSimpleOptions['main'], 'entry', 'files'> & ExcludeOutputDirOptions
770
+ /**
771
+ * Preload process options
772
+ *
773
+ * To change output directories, use `options.updater.paths.electronDistPath` instead
774
+ */
775
+ preload: MakeRequiredAndReplaceKey<Exclude<ElectronSimpleOptions['preload'], undefined>, 'input', 'files'> & ExcludeOutputDirOptions
776
+ /**
777
+ * Updater options
778
+ */
779
+ updater?: ElectronUpdaterOptions
780
+ }
781
+
782
+ export interface ElectronUpdaterOptions {
783
+ /**
784
+ * Minimum version of entry
785
+ * @default '0.0.0'
786
+ */
787
+ minimumVersion?: string
788
+ /**
789
+ * Options for entry (app.asar)
790
+ */
791
+ entry?: BuildEntryOption
792
+ /**
793
+ * Options for paths
794
+ */
795
+ paths?: {
796
+ /**
797
+ * Path to asar file
798
+ * @default `release/${app.name}.asar`
799
+ */
800
+ asarOutputPath?: string
801
+ /**
802
+ * Path to version info output, content is {@link UpdateJSON}
803
+ * @default `version.json`
804
+ */
805
+ versionPath?: string
806
+ /**
807
+ * Path to gzipped asar file
808
+ * @default `release/${app.name}-${version}.asar.gz`
809
+ */
810
+ gzipPath?: string
811
+ /**
812
+ * Path to electron build output
813
+ * @default `dist-electron`
814
+ */
815
+ electronDistPath?: string
816
+ /**
817
+ * Path to renderer build output
818
+ * @default `dist`
819
+ */
820
+ rendererDistPath?: string
821
+ }
822
+ /**
823
+ * signature config
824
+ */
825
+ keys?: {
826
+ /**
827
+ * Path to the pem file that contains private key
828
+ * If not ended with .pem, it will be appended
829
+ *
830
+ * **If `UPDATER_PK` is set, will read it instead of read from `privateKeyPath`**
831
+ * @default 'keys/private.pem'
832
+ */
833
+ privateKeyPath?: string
834
+ /**
835
+ * Path to the pem file that contains public key
836
+ * If not ended with .pem, it will be appended
837
+ *
838
+ * **If `UPDATER_CERT` is set, will read it instead of read from `certPath`**
839
+ * @default 'keys/cert.pem'
840
+ */
841
+ certPath?: string
842
+ /**
843
+ * Length of the key
844
+ * @default 2048
845
+ */
846
+ keyLength?: number
847
+ /**
848
+ * X509 certificate info
849
+ *
850
+ * only generate simple **self-signed** certificate **without extensions**
851
+ */
852
+ certInfo?: {
853
+ /**
854
+ * The subject of the certificate
855
+ *
856
+ * @default { commonName: `${app.name}`, organizationName: `org.${app.name}` }
857
+ */
858
+ subject?: DistinguishedName
859
+ /**
860
+ * Expire days of the certificate
861
+ *
862
+ * @default 3650
863
+ */
864
+ days?: number
865
+ }
866
+ }
867
+ overrideGenerator?: GeneratorOverrideFunctions
868
+ }
869
+
870
+ export interface BytecodeOptions {
871
+ enable: boolean
872
+ /**
873
+ * Enable in preload script. Remember to set `sandbox: false` when creating window
874
+ */
875
+ preload?: boolean
876
+ /**
877
+ * Custom electron binary path
878
+ */
879
+ electronPath?: string
880
+ /**
881
+ * Before transformed code compile function. If return `Falsy` value, it will be ignored
882
+ * @param code transformed code
883
+ * @param id file path
884
+ */
885
+ beforeCompile?: (code: string, id: string) => Promisable<string | null | undefined | void>
886
+ }
887
+
888
+ export interface BuildEntryOption {
889
+ /**
890
+ * Override to minify on entry
891
+ * @default isBuild
892
+ */
893
+ minify?: boolean
894
+ /**
895
+ * Override to generate sourcemap on entry
896
+ */
897
+ sourcemap?: boolean
898
+ /**
899
+ * Path to app entry output file
900
+ * @default 'dist-entry'
901
+ */
902
+ entryOutputDirPath?: string
903
+ /**
904
+ * Path to app entry file
905
+ * @default 'electron/entry.ts'
906
+ */
907
+ appEntryPath?: string
908
+ /**
909
+ * Esbuild path map of native modules in entry directory
910
+ *
911
+ * @default {}
912
+ * @example
913
+ * { db: './electron/native/db.ts' }
914
+ */
915
+ nativeModuleEntryMap?: Record<string, string>
916
+ /**
917
+ * Skip process dynamic require
918
+ *
919
+ * Useful for `better-sqlite3` and other old packages
920
+ */
921
+ ignoreDynamicRequires?: boolean
922
+ /**
923
+ * `external` option in `build.rollupOptions`, external `.node` by default
924
+ */
925
+ external?: string | string[] | ((source: string, importer: string | undefined, isResolved: boolean) => boolean | null | undefined | void)
926
+ /**
927
+ * Custom options for `vite` build
928
+ * ```ts
929
+ * const options = {
930
+ * plugins: [esm(), bytecodePlugin()], // load on needed
931
+ * build: {
932
+ * sourcemap,
933
+ * minify,
934
+ * outDir: entryOutputDirPath,
935
+ * commonjsOptions: { ignoreDynamicRequires },
936
+ * rollupOptions: { external },
937
+ * },
938
+ * define,
939
+ * }
940
+ * ```
941
+ */
942
+ overrideViteOptions?: InlineConfig
943
+ /**
944
+ * Resolve extra files on startup, such as `.node`
945
+ * @remark won't trigger will reload
946
+ */
947
+ postBuild?: (args: {
948
+ /**
949
+ * Get path from `entryOutputDirPath`
950
+ */
951
+ getPathFromEntryOutputDir: (...paths: string[]) => string
952
+ /**
953
+ * Check exist and copy file to `entryOutputDirPath`
954
+ *
955
+ * If `to` absent, set to `basename(from)`
956
+ *
957
+ * If `skipIfExist` absent, skip copy if `to` exist
958
+ */
959
+ copyToEntryOutputDir: (options: {
960
+ from: string
961
+ to?: string
962
+ /**
963
+ * Skip copy if `to` exist
964
+ * @default true
965
+ */
966
+ skipIfExist?: boolean
967
+ }) => void
968
+ /**
969
+ * Copy specified modules to entry output dir, just like `external` option in rollup
970
+ */
971
+ copyModules: (options: {
972
+ /**
973
+ * External Modules
974
+ */
975
+ modules: string[]
976
+ /**
977
+ * Skip copy if `to` exist
978
+ * @default true
979
+ */
980
+ skipIfExist?: boolean
981
+ }) => void
982
+ }) => Promisable<void>
983
+ }
984
+
985
+ export interface GeneratorOverrideFunctions {
986
+ /**
987
+ * Custom signature generate function
988
+ * @param buffer file buffer
989
+ * @param privateKey private key
990
+ * @param cert certificate string, **EOL must be '\n'**
991
+ * @param version current version
992
+ */
993
+ generateSignature?: (
994
+ buffer: Buffer,
995
+ privateKey: string,
996
+ cert: string,
997
+ version: string
998
+ ) => Promisable<string>
999
+ /**
1000
+ * Custom generate update json function
1001
+ * @param existingJson The existing JSON object.
1002
+ * @param buffer file buffer
1003
+ * @param signature generated signature
1004
+ * @param version current version
1005
+ * @param minVersion The minimum version
1006
+ */
1007
+ generateUpdateJson?: (
1008
+ existingJson: UpdateJSON,
1009
+ signature: string,
1010
+ version: string,
1011
+ minVersion: string
1012
+ ) => Promisable<UpdateJSON>
1013
+ /**
1014
+ * Custom generate zip file buffer
1015
+ * @param buffer source buffer
1016
+ */
1017
+ generateGzipFile?: (buffer: Buffer) => Promisable<Buffer>
1018
+ }
1019
+ ```
1020
+
1021
+ ## Credits
1022
+
1023
+ - [Obsidian](https://obsidian.md/) for upgrade strategy
1024
+ - [vite-plugin-electron](https://github.com/electron-vite/vite-plugin-electron) for vite plugin
1025
+ - [electron-builder](https://github.com/electron-userland/electron-builder) for update api
1026
+ - [electron-vite](https://github.com/alex8088/electron-vite) for bytecode plugin inspiration
1027
+
1028
+ ## License
1029
+
1030
+ MIT