electron-incremental-update 1.1.0 → 1.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
@@ -10,7 +10,7 @@ The new `${electron.app.name}.asar`, which can download from remote or load from
10
10
 
11
11
  All **native modules** should be packaged into `app.asar` to reduce `${electron.app.name}.asar` file size, [see usage](#use-native-modules). Therefore, auto upgrade of portable app is possible.
12
12
 
13
- no `vite-plugin-electron-renderer` config
13
+ No `vite-plugin-electron-renderer` config
14
14
 
15
15
  - inspired by [Obsidian](https://obsidian.md/)'s upgrade strategy
16
16
 
@@ -57,16 +57,17 @@ import { initApp } from 'electron-incremental-update'
57
57
  import { parseGithubCdnURL } from 'electron-incremental-update/utils'
58
58
  import { repository } from '../package.json'
59
59
 
60
- const SIGNATURE_CERT = '' // auto generate certificate when start app
61
-
62
- initApp({ onStart: console.log })
63
- .setUpdater({
64
- SIGNATURE_CERT,
65
- // repository,
66
- // updateJsonURL: parseGithubCdnURL(repository, 'https://your.cdn.url/', 'version.json'),
67
- // releaseAsarURL: parseGithubCdnURL(repository, 'https://your.cdn.url/', `download/latest/${name}.asar.gz`),
68
- // receiveBeta: true
69
- })
60
+ initApp({
61
+ // can be updater option or function that return updater
62
+ updater: {
63
+ SIGNATURE_CERT: 'custom certificate',
64
+ repository,
65
+ updateJsonURL: parseGithubCdnURL(repository, jsonPrefix, 'version.json'),
66
+ releaseAsarURL: parseGithubCdnURL(repository, asarPrefix, `download/latest/${app.name}.asar.gz`),
67
+ receiveBeta: true,
68
+ },
69
+ onStart: console.log
70
+ })
70
71
  ```
71
72
 
72
73
  - [some CDN resources](https://github.com/XIU2/UserScript/blob/master/GithubEnhanced-High-Speed-Download.user.js#L34):
@@ -83,14 +84,12 @@ in `vite.config.mts`
83
84
  ```ts
84
85
  import { defineConfig } from 'vite'
85
86
  import { debugStartup, electronWithUpdater } from 'electron-incremental-update/vite'
86
- import pkg from './package.json'
87
87
 
88
88
  export default defineConfig(async ({ command }) => {
89
89
  const isBuild = command === 'build'
90
90
  return {
91
91
  plugins: [
92
92
  electronWithUpdater({
93
- pkg,
94
93
  isBuild,
95
94
  logParsedOptions: true,
96
95
  main: {
@@ -160,12 +159,23 @@ module.exports = {
160
159
 
161
160
  ### Use in main process
162
161
 
163
- To use electron's `net` module for updating, the `checkUpdate` and `download` functions must be called after the app is ready by default.
164
-
165
- However, you have the option to customize the download function when creating the updater.
162
+ To use electron's `net` module for updating, the `checkUpdate` and `download` functions must be called after the app is ready by default. You have the option to customize the download function when creating the updater.
166
163
 
167
164
  **NOTE: There should only one function and should be default export in the entry file**
168
165
 
166
+ in `electron/entry.ts`
167
+
168
+ ```ts
169
+ initApp({
170
+ updater: {
171
+ overrideFunctions: {
172
+ downloadJSON: (url: string, headers: Record<string, any>) => {}
173
+ // ...
174
+ }
175
+ },
176
+ })
177
+ ```
178
+
169
179
  in `electron/main/index.ts`
170
180
 
171
181
  ```ts
@@ -214,7 +224,16 @@ export default startupWithUpdater((updater) => {
214
224
 
215
225
  All the **native modules** should be set as `dependency` in `package.json`. `electron-rebuild` only check dependencies inside `dependency` field.
216
226
 
217
- 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. You can setup `nativeModuleEntryMap` option to prebundle all the native modules and skip bundled by `electron-builder`
227
+ 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.
228
+
229
+ Luckily, `Esbuild` can bundle all the dependencies. Just follow the steps:
230
+
231
+ 1. setup `nativeModuleEntryMap` option
232
+ 2. Manually copy the native binaries in `postBuild` callback
233
+ 3. Exclude all the dependencies in `electron-builder`'s config
234
+ 4. call the native functions with `loadNativeModuleFromEntry` in your code
235
+
236
+ #### Example
218
237
 
219
238
  in `vite.config.ts`
220
239
 
@@ -225,6 +244,7 @@ const plugin = electronWithUpdater({
225
244
  entry: {
226
245
  nativeModuleEntryMap: {
227
246
  db: './electron/native/db.ts',
247
+ img: './electron/native/img.ts',
228
248
  },
229
249
  postBuild: async ({ copyToEntryOutputDir }) => {
230
250
  // for better-sqlite3
@@ -249,9 +269,8 @@ in `electron/native/db.ts`
249
269
 
250
270
  ```ts
251
271
  import Database from 'better-sqlite3'
252
- import { getPaths } from 'electron-incremental-update/utils'
253
272
 
254
- const db = new Database(':memory:', { nativeBinding: getPaths().getPathFromEntryAsar('better_sqlite3.node') })
273
+ const db = new Database(':memory:', { nativeBinding: './better_sqlite3.node' })
255
274
 
256
275
  export function test() {
257
276
  db.exec(
@@ -296,14 +315,119 @@ module.exports = {
296
315
 
297
316
  ### Bytecode protection
298
317
 
299
- WIP
318
+ From v1.2, the vite plugin is able to generate bytecode to protect your application.
300
319
 
301
- plan to use [electron-vite](https://github.com/alex8088/electron-vite/blob/master/src/plugins/bytecode.ts), but fail to load the default function in `${electron.app.name}.asar/dist-electron/index.js`.
320
+ It will automatically protect your `SIGNATURE_CERT` by default.
302
321
 
303
- try to wrap with [`Module.wrap`](https://github.com/bytenode/bytenode?tab=readme-ov-file#bytenodecompileelectroncodejavascriptcode-options--promisebuffer), but still fail.
322
+ credit to [electron-vite](https://github.com/alex8088/electron-vite/blob/master/src/plugins/bytecode.ts), and improve the string protection (see [original issue](https://github.com/alex8088/electron-vite/issues/552))
323
+
324
+ ```ts
325
+ electronWithUpdater({
326
+ // ...
327
+ bytecode: true,
328
+ })
329
+ ```
330
+
331
+ #### Limitation
332
+
333
+ - only support commonjs
334
+ - 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
304
335
 
305
336
  ### Types
306
337
 
338
+ #### Updater
339
+
340
+ ```ts
341
+ export interface UpdaterOption {
342
+ /**
343
+ * public key of signature, which will be auto generated by plugin,
344
+ * generate by `selfsigned` if not set
345
+ */
346
+ SIGNATURE_CERT?: string
347
+ /**
348
+ * repository url, e.g. `https://github.com/electron/electron`
349
+ *
350
+ * you can use the `repository` in `package.json`
351
+ *
352
+ * if `updateJsonURL` or `releaseAsarURL` are absent,
353
+ * `repository` will be used to determine the url
354
+ */
355
+ repository?: string
356
+ /**
357
+ * URL of version info json
358
+ * @default `${repository.replace('github.com', 'raw.githubusercontent.com')}/master/version.json`
359
+ * @throws if `updateJsonURL` and `repository` are all not set
360
+ */
361
+ updateJsonURL?: string
362
+ /**
363
+ * URL of release asar.gz
364
+ * @default `${repository}/releases/download/v${version}/${app.name}-${version}.asar.gz`
365
+ * @throws if `releaseAsarURL` and `repository` are all not set
366
+ */
367
+ releaseAsarURL?: string
368
+ /**
369
+ * whether to receive beta update
370
+ */
371
+ receiveBeta?: boolean
372
+ overrideFunctions?: UpdaterOverrideFunctions
373
+ downloadConfig?: UpdaterDownloadConfig
374
+ }
375
+ export type Logger = {
376
+ info: (msg: string) => void
377
+ debug: (msg: string) => void
378
+ warn: (msg: string) => void
379
+ error: (msg: string, e?: Error) => void
380
+ }
381
+
382
+ export type UpdaterOverrideFunctions = {
383
+ /**
384
+ * custom version compare function
385
+ * @param version1 old version string
386
+ * @param version2 new version string
387
+ * @returns if version1 < version2
388
+ */
389
+ isLowerVersion?: (version1: string, version2: string) => boolean | Promise<boolean>
390
+ /**
391
+ * custom verify signature function
392
+ * @param buffer file buffer
393
+ * @param signature signature
394
+ * @param cert certificate
395
+ * @returns if signature is valid, returns the version or `true` , otherwise returns `false`
396
+ */
397
+ verifySignaure?: (buffer: Buffer, signature: string, cert: string) => string | false | Promise<string | false>
398
+ /**
399
+ * custom download JSON function
400
+ * @param url download url
401
+ * @param header download header
402
+ * @returns `UpdateJSON`
403
+ */
404
+ downloadJSON?: (url: string, headers: Record<string, any>) => Promise<UpdateJSON>
405
+ /**
406
+ * custom download buffer function
407
+ * @param url download url
408
+ * @param headers download header
409
+ * @param total precaculated file total size
410
+ * @param onDownloading on downloading callback
411
+ * @returns `Buffer`
412
+ */
413
+ downloadBuffer?: (url: string, headers: Record<string, any>, total: number, onDownloading?: (progress: DownloadingInfo) => void) => Promise<Buffer>
414
+ }
415
+
416
+ export type UpdaterDownloadConfig = {
417
+ /**
418
+ * download user agent
419
+ * @default 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36'
420
+ */
421
+ userAgent?: string
422
+ /**
423
+ * extra download header, `accept` and `user-agent` is set by default
424
+ */
425
+ extraHeader?: Record<string, string>
426
+ }
427
+ ```
428
+
429
+ #### Plugin
430
+
307
431
  ```ts
308
432
  type ElectronWithUpdaterOptions = {
309
433
  /**
@@ -316,7 +440,7 @@ type ElectronWithUpdaterOptions = {
316
440
  */
317
441
  isBuild: boolean
318
442
  /**
319
- * manullay setup package.json, read name, version and main
443
+ * manually setup package.json, read name, version and main
320
444
  * ```ts
321
445
  * import pkg from './package.json'
322
446
  * ```
@@ -330,6 +454,14 @@ type ElectronWithUpdaterOptions = {
330
454
  * whether to minify the code
331
455
  */
332
456
  minify?: boolean
457
+ /**
458
+ * whether to generate bytecode
459
+ *
460
+ * **only support commonjs**
461
+ *
462
+ * 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
463
+ */
464
+ bytecode?: boolean | BytecodeOptions
333
465
  /**
334
466
  * use NotBundle() plugin in main
335
467
  * @default true
@@ -337,8 +469,10 @@ type ElectronWithUpdaterOptions = {
337
469
  useNotBundle?: boolean
338
470
  /**
339
471
  * Whether to log parsed options
472
+ *
473
+ * to show certificate and private keys, set `logParsedOptions: { showKeys: true }`
340
474
  */
341
- logParsedOptions?: boolean
475
+ logParsedOptions?: boolean | { showKeys: boolean }
342
476
  /**
343
477
  * main options
344
478
  */
@@ -471,7 +605,7 @@ type BuildEntryOption = {
471
605
  */
472
606
  nativeModuleEntryMap?: Record<string, string>
473
607
  /**
474
- * custom options for esbuild
608
+ * override options for esbuild
475
609
  * ```ts
476
610
  * // default options
477
611
  * const options = {
@@ -490,6 +624,9 @@ type BuildEntryOption = {
490
624
  * loader: {
491
625
  * '.node': 'empty',
492
626
  * },
627
+ * define: {
628
+ * __SIGNATURE_CERT__: JSON.stringify(cert),
629
+ * },
493
630
  * }
494
631
  * ```
495
632
  */
@@ -0,0 +1,236 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/utils/electron.ts
9
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
10
+ import { basename, dirname, join } from "node:path";
11
+ import { release } from "node:os";
12
+ import { app } from "electron";
13
+ var is = {
14
+ dev: !app.isPackaged,
15
+ win: process.platform === "win32",
16
+ mac: process.platform === "darwin",
17
+ linux: process.platform === "linux"
18
+ };
19
+ function getPathFromAppNameAsar(...path) {
20
+ return is.dev ? "DEV.asar" : join(dirname(app.getAppPath()), `${electron.app.name}.asar`, ...path);
21
+ }
22
+ function getVersions() {
23
+ const platform = is.win ? "Windows" : is.mac ? "MacOS" : process.platform.toUpperCase();
24
+ return {
25
+ appVersion: is.dev ? app.getVersion() : readFileSync(getPathFromAppNameAsar("version"), "utf-8"),
26
+ entryVersion: app.getVersion(),
27
+ electronVersion: process.versions.electron,
28
+ nodeVersion: process.versions.node,
29
+ systemVersion: `${platform} ${release()}`
30
+ };
31
+ }
32
+ function loadNativeModuleFromEntry(devEntryDirPath = "../../dist-entry", entryDirPath = join(app.getAppPath(), basename(devEntryDirPath))) {
33
+ const path = is.dev ? devEntryDirPath : entryDirPath;
34
+ return (moduleName) => {
35
+ try {
36
+ return __require(join(path, moduleName));
37
+ } catch (error) {
38
+ console.error("fail to load module", error);
39
+ }
40
+ };
41
+ }
42
+ function restartApp() {
43
+ app.relaunch();
44
+ app.quit();
45
+ }
46
+ function setAppUserModelId(id) {
47
+ app.setAppUserModelId(is.dev ? process.execPath : id ?? `org.${electron.app.name}`);
48
+ }
49
+ function disableHWAccForWin7() {
50
+ if (release().startsWith("6.1")) {
51
+ app.disableHardwareAcceleration();
52
+ }
53
+ }
54
+ function singleInstance(window) {
55
+ const result = app.requestSingleInstanceLock();
56
+ result ? app.on("second-instance", () => {
57
+ if (window) {
58
+ window.show();
59
+ if (window.isMinimized()) {
60
+ window.restore();
61
+ }
62
+ window.focus();
63
+ }
64
+ }) : app.quit();
65
+ return result;
66
+ }
67
+ function setPortableAppDataPath(dirName = "data") {
68
+ const portablePath = join(dirname(app.getPath("exe")), dirName);
69
+ if (!existsSync(portablePath)) {
70
+ mkdirSync(portablePath);
71
+ }
72
+ app.setPath("appData", portablePath);
73
+ }
74
+ function waitAppReady(timeout = 1e3) {
75
+ return app.isReady() ? Promise.resolve() : new Promise((resolve, reject) => {
76
+ const _ = setTimeout(() => {
77
+ reject(new Error("app is not ready"));
78
+ }, timeout);
79
+ app.whenReady().then(() => {
80
+ clearTimeout(_);
81
+ resolve();
82
+ });
83
+ });
84
+ }
85
+ function getPaths(entryDirName = "dist-entry") {
86
+ const root = join(__dirname, "..");
87
+ const mainDirPath = join(root, "main");
88
+ const preloadDirPath = join(root, "preload");
89
+ const rendererDirPath = join(root, "renderer");
90
+ const devServerURL = process.env.VITE_DEV_SERVER_URL;
91
+ const indexHTMLPath = join(rendererDirPath, "index.html");
92
+ const publicDirPath = devServerURL ? join(root, "../public") : rendererDirPath;
93
+ return {
94
+ /**
95
+ * @example
96
+ * ```ts
97
+ * devServerURL && win.loadURL(devServerURL)
98
+ * ```
99
+ */
100
+ devServerURL,
101
+ /**
102
+ * @example
103
+ * ```ts
104
+ * win.loadFile(indexHTMLPath)
105
+ * ```
106
+ */
107
+ indexHTMLPath,
108
+ /**
109
+ * get path inside entry asar
110
+ * @param paths joined path
111
+ */
112
+ getPathFromEntryAsar(...paths) {
113
+ return join(app.getAppPath(), entryDirName, ...paths);
114
+ },
115
+ /**
116
+ * get path inside `${electron.app.name}.asar/main`
117
+ * @param paths joined path
118
+ */
119
+ getPathFromMain(...paths) {
120
+ return join(mainDirPath, ...paths);
121
+ },
122
+ /**
123
+ * get path inside `${electron.app.name}.asar/preload`
124
+ * @param paths joined path
125
+ */
126
+ getPathFromPreload(...paths) {
127
+ return join(preloadDirPath, ...paths);
128
+ },
129
+ /**
130
+ * get path inside public dir
131
+ * @param paths joined path
132
+ */
133
+ getPathFromPublic(...paths) {
134
+ return join(publicDirPath, ...paths);
135
+ }
136
+ };
137
+ }
138
+
139
+ // src/utils/zip.ts
140
+ import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
141
+ import { brotliCompress, brotliDecompress } from "node:zlib";
142
+ async function unzipFile(gzipPath, targetFilePath = gzipPath.slice(0, -3)) {
143
+ if (!existsSync2(gzipPath)) {
144
+ throw new Error(`path to zipped file not exist: ${gzipPath}`);
145
+ }
146
+ const compressedBuffer = readFileSync2(gzipPath);
147
+ return new Promise((resolve, reject) => {
148
+ brotliDecompress(compressedBuffer, (err, buffer) => {
149
+ rmSync(gzipPath);
150
+ if (err) {
151
+ reject(err);
152
+ }
153
+ writeFileSync(targetFilePath, buffer);
154
+ resolve(null);
155
+ });
156
+ });
157
+ }
158
+ async function zipFile(filePath, targetFilePath = `${filePath}.gz`) {
159
+ if (!existsSync2(filePath)) {
160
+ throw new Error(`path to be zipped not exist: ${filePath}`);
161
+ }
162
+ const buffer = readFileSync2(filePath);
163
+ return new Promise((resolve, reject) => {
164
+ brotliCompress(buffer, (err, buffer2) => {
165
+ if (err) {
166
+ reject(err);
167
+ }
168
+ writeFileSync(targetFilePath, buffer2);
169
+ resolve(null);
170
+ });
171
+ });
172
+ }
173
+
174
+ // src/utils/pure.ts
175
+ function parseGithubCdnURL(originRepoURL, cdnPrefix, relativeFilePath) {
176
+ if (!originRepoURL.startsWith("https://github.com/")) {
177
+ throw new Error("origin url must start with https://github.com/");
178
+ }
179
+ originRepoURL = originRepoURL.trim().replace(/\/?$/, "/").trim();
180
+ relativeFilePath = relativeFilePath.trim().replace(/^\/|\/?$/g, "").trim();
181
+ cdnPrefix = cdnPrefix.trim().replace(/^\/?|\/?$/g, "").trim();
182
+ return originRepoURL.replace("github.com", cdnPrefix) + relativeFilePath;
183
+ }
184
+ function handleUnexpectedErrors(callback) {
185
+ process.on("uncaughtException", callback);
186
+ process.on("unhandledRejection", callback);
187
+ }
188
+ function parseVersion(version) {
189
+ const semver = /^(\d+)\.(\d+)\.(\d+)(?:-([a-z0-9.-]+))?/i;
190
+ const match = semver.exec(version);
191
+ if (!match) {
192
+ throw new TypeError(`invalid version: ${version}`);
193
+ }
194
+ const [major, minor, patch] = match.slice(1, 4).map(Number);
195
+ const ret = {
196
+ major,
197
+ minor,
198
+ patch,
199
+ stage: "",
200
+ stageVersion: -1
201
+ };
202
+ if (match[4]) {
203
+ let [stage, _v] = match[4].split(".");
204
+ ret.stage = stage;
205
+ ret.stageVersion = Number(_v) || -1;
206
+ }
207
+ if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch) || Number.isNaN(ret.stageVersion)) {
208
+ throw new TypeError(`invalid version: ${version}`);
209
+ }
210
+ return ret;
211
+ }
212
+ function isUpdateJSON(json) {
213
+ const is2 = (j) => !!(j && j.minimumVersion && j.signature && j.size && j.version);
214
+ return is2(json) && is2(json?.beta);
215
+ }
216
+
217
+ export {
218
+ __require,
219
+ is,
220
+ getPathFromAppNameAsar,
221
+ getVersions,
222
+ loadNativeModuleFromEntry,
223
+ restartApp,
224
+ setAppUserModelId,
225
+ disableHWAccForWin7,
226
+ singleInstance,
227
+ setPortableAppDataPath,
228
+ waitAppReady,
229
+ getPaths,
230
+ unzipFile,
231
+ zipFile,
232
+ parseGithubCdnURL,
233
+ handleUnexpectedErrors,
234
+ parseVersion,
235
+ isUpdateJSON
236
+ };