electron-incremental-update 1.2.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
 
@@ -159,12 +159,23 @@ module.exports = {
159
159
 
160
160
  ### Use in main process
161
161
 
162
- To use electron's `net` module for updating, the `checkUpdate` and `download` functions must be called after the app is ready by default.
163
-
164
- 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.
165
163
 
166
164
  **NOTE: There should only one function and should be default export in the entry file**
167
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
+
168
179
  in `electron/main/index.ts`
169
180
 
170
181
  ```ts
@@ -213,7 +224,16 @@ export default startupWithUpdater((updater) => {
213
224
 
214
225
  All the **native modules** should be set as `dependency` in `package.json`. `electron-rebuild` only check dependencies inside `dependency` field.
215
226
 
216
- 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
217
237
 
218
238
  in `vite.config.ts`
219
239
 
@@ -224,6 +244,7 @@ const plugin = electronWithUpdater({
224
244
  entry: {
225
245
  nativeModuleEntryMap: {
226
246
  db: './electron/native/db.ts',
247
+ img: './electron/native/img.ts',
227
248
  },
228
249
  postBuild: async ({ copyToEntryOutputDir }) => {
229
250
  // for better-sqlite3
@@ -248,9 +269,8 @@ in `electron/native/db.ts`
248
269
 
249
270
  ```ts
250
271
  import Database from 'better-sqlite3'
251
- import { getPaths } from 'electron-incremental-update/utils'
252
272
 
253
- const db = new Database(':memory:', { nativeBinding: getPaths().getPathFromEntryAsar('better_sqlite3.node') })
273
+ const db = new Database(':memory:', { nativeBinding: './better_sqlite3.node' })
254
274
 
255
275
  export function test() {
256
276
  db.exec(
@@ -295,7 +315,11 @@ module.exports = {
295
315
 
296
316
  ### Bytecode protection
297
317
 
298
- credit to [electron-vite](https://github.com/alex8088/electron-vite/blob/master/src/plugins/bytecode.ts)
318
+ From v1.2, the vite plugin is able to generate bytecode to protect your application.
319
+
320
+ It will automatically protect your `SIGNATURE_CERT` by default.
321
+
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))
299
323
 
300
324
  ```ts
301
325
  electronWithUpdater({
@@ -311,6 +335,99 @@ electronWithUpdater({
311
335
 
312
336
  ### Types
313
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
+
314
431
  ```ts
315
432
  type ElectronWithUpdaterOptions = {
316
433
  /**
@@ -0,0 +1,235 @@
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()), `${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.${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 match = /^(\d+)\.(\d+)\.(\d+)(?:-([a-z0-9.-]+))?/i.exec(version);
190
+ if (!match) {
191
+ throw new TypeError(`invalid version: ${version}`);
192
+ }
193
+ const [major, minor, patch] = match.slice(1, 4).map(Number);
194
+ const ret = {
195
+ major,
196
+ minor,
197
+ patch,
198
+ stage: "",
199
+ stageVersion: -1
200
+ };
201
+ if (match[4]) {
202
+ let [stage, _v] = match[4].split(".");
203
+ ret.stage = stage;
204
+ ret.stageVersion = Number(_v) || -1;
205
+ }
206
+ if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch) || Number.isNaN(ret.stageVersion)) {
207
+ throw new TypeError(`invalid version: ${version}`);
208
+ }
209
+ return ret;
210
+ }
211
+ function isUpdateJSON(json) {
212
+ const is2 = (j) => !!(j && j.minimumVersion && j.signature && j.size && j.version);
213
+ return is2(json) && is2(json?.beta);
214
+ }
215
+
216
+ export {
217
+ __require,
218
+ is,
219
+ getPathFromAppNameAsar,
220
+ getVersions,
221
+ loadNativeModuleFromEntry,
222
+ restartApp,
223
+ setAppUserModelId,
224
+ disableHWAccForWin7,
225
+ singleInstance,
226
+ setPortableAppDataPath,
227
+ waitAppReady,
228
+ getPaths,
229
+ unzipFile,
230
+ zipFile,
231
+ parseGithubCdnURL,
232
+ handleUnexpectedErrors,
233
+ parseVersion,
234
+ isUpdateJSON
235
+ };