electron-incremental-update 0.2.2 → 0.4.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
@@ -16,6 +16,8 @@ using RSA + Signature to sign the new `main.asar` downloaded from remote and rep
16
16
 
17
17
  develop with [vite-plugin-electron](https://github.com/electron-vite/vite-plugin-electron), and may be effect in other electron vite frameworks
18
18
 
19
+ **all options are documented in the jsdoc**
20
+
19
21
  ## install
20
22
 
21
23
  ### npm
@@ -50,21 +52,28 @@ src
50
52
 
51
53
  ### setup app
52
54
 
53
- more example see comment on `initApp()`
54
55
 
55
56
  ```ts
56
57
  // electron/app.ts
57
- import { createUpdater, initApp } from 'electron-incremental-update'
58
+ import { createUpdater, getGithubReleaseCdnGroup, initApp, parseGithubCdnURL } from 'electron-incremental-update'
58
59
  import { name, repository } from '../package.json'
59
60
 
60
61
  const SIGNATURE_PUB = '' // auto generate RSA public key when start app
61
62
 
63
+ // create updater when init, no need to set productName
64
+ initApp({ name }, { SIGNATURE_PUB, repository })
65
+
66
+ // or create updater manually
67
+ const { cdnPrefix } = getGithubReleaseCdnGroup()[0]
62
68
  const updater = createUpdater({
63
69
  SIGNATURE_PUB,
64
- repository,
65
70
  productName: name,
71
+ repository,
72
+ updateJsonURL: parseGithubCdnURL(repository, 'fastly.jsdelivr.net/gh', 'version.json'),
73
+ releaseAsarURL: parseGithubCdnURL(repository, cdnPrefix, `download/latest/${name}.asar.gz`),
74
+ debug: true,
66
75
  })
67
- initApp(name, updater)
76
+ initApp({ name }).setUpdater(updater)
68
77
  ```
69
78
 
70
79
  ### setup main
@@ -72,43 +81,43 @@ initApp(name, updater)
72
81
  ```ts
73
82
  // electron/main/index.ts
74
83
  import type { Updater } from 'electron-incremental-update'
75
- import { getAppAsarPath, getAppVersion, getElectronVersion } from 'electron-incremental-update'
84
+ import { getAppAsarPath, getAppVersion, getEntryVersion } from 'electron-incremental-update'
76
85
  import { app } from 'electron'
77
86
  import { name } from '../../package.json'
78
87
 
79
88
  export default function (updater: Updater) {
80
89
  console.log('\ncurrent:')
81
- console.log(`\telectron: ${getElectronVersion()}`)
82
90
  console.log(`\tasar path: ${getAppAsarPath(name)}`)
91
+ console.log(`\tentry: ${getEntryVersion()}`)
83
92
  console.log(`\tapp: ${getAppVersion(name)}`)
84
-
85
- updater.checkUpdate()
86
- updater.on('checkResult', async (result, err) => {
87
- switch (result) {
88
- case 'success':
89
- await dialog.showMessageBox({
90
- type: 'info',
91
- buttons: ['Restart', 'Later'],
92
- message: 'Application successfully updated!',
93
- }).then(({ response }) => {
94
- if (response === 0) {
95
- app.relaunch()
96
- app.quit()
97
- }
98
- })
99
- break
100
- case 'unavailable':
101
- console.log('Update Unavailable')
102
- break
103
- case 'fail':
104
- console.error(err)
105
- break
93
+ let size = 0
94
+ let currentSize = 0
95
+ updater.on('checkResult', async (result) => {
96
+ if (result === false) {
97
+ console.log('Update Unavailable')
98
+ } else if (result instanceof Error) {
99
+ console.error(result)
100
+ } else {
101
+ size = result.size
102
+ console.log('new version: ', result.version)
103
+ const { response } = await dialog.showMessageBox({
104
+ type: 'info',
105
+ buttons: ['Download', 'Later'],
106
+ message: 'Application update available!',
107
+ })
108
+ response === 0 && await updater.downloadUpdate()
106
109
  }
107
110
  })
108
- updater.on('downloadStart', console.log)
109
- updater.on('downloading', console.log)
110
- updater.on('downloadEnd', console.log)
111
+ updater.on('download', () => console.log('download start'))
112
+ updater.on('downloading', (len) => {
113
+ currentSize += len
114
+ console.log(`${(currentSize / size).toFixed(2)}%`)
115
+ })
116
+ updater.on('downloaded', () => console.log('download end'))
111
117
  updater.on('donwnloadError', console.error)
118
+ // to debug, it need to set debug to true in updater options
119
+ updater.on('debug', data => console.log('[updater]:', data))
120
+ updater.checkUpdate()
112
121
 
113
122
  // app logics
114
123
  app.whenReady().then(() => {
@@ -144,7 +153,12 @@ db.close()
144
153
 
145
154
  ### setup vite.config.ts
146
155
 
156
+ make sure the plugin is set in the **last** build task plugin option
157
+
158
+ - set it to preload task plugin, as the end of build task
159
+
147
160
  ```ts
161
+ // vite.config.ts
148
162
  export default defineConfig(({ command }) => {
149
163
 
150
164
  const isBuild = command === 'build'
@@ -162,7 +176,7 @@ export default defineConfig(({ command }) => {
162
176
  // ...
163
177
  vite: {
164
178
  plugins: [
165
- updater({ // !make sure the plugin run pack asar after all build finish
179
+ updater({
166
180
  productName: pkg.name,
167
181
  version: pkg.version,
168
182
  isBuild,
@@ -179,90 +193,6 @@ export default defineConfig(({ command }) => {
179
193
  })
180
194
  ```
181
195
 
182
- #### plugin options
183
-
184
- ```ts
185
- type Options = {
186
- /**
187
- * whether is in build mode
188
- */
189
- isBuild: boolean
190
- /**
191
- * the name of you application
192
- *
193
- * you can set as 'name' in package.json
194
- */
195
- productName: string
196
- /**
197
- * the version of you application
198
- *
199
- * you can set as 'version' in package.json
200
- */
201
- version: string
202
- /**
203
- * Whether to minify
204
- */
205
- minify?: boolean
206
- /**
207
- * path config
208
- */
209
- paths?: {
210
- /**
211
- * Path to app entry file
212
- * @default 'electron/app.ts'
213
- */
214
- entryPath?: string
215
- /**
216
- * Path to app entry output file
217
- * @default 'app.js'
218
- */
219
- entryOutputPath?: string
220
- /**
221
- * Path to asar file
222
- * @default `release/${ProductName}.asar`
223
- */
224
- asarOutputPath?: string
225
- /**
226
- * Path to electron build output
227
- * @default `dist-electron`
228
- */
229
- electronDistPath?: string
230
- /**
231
- * Path to renderer build output
232
- * @default `dist`
233
- */
234
- rendererDistPath?: string
235
- /**
236
- * Path to version info output
237
- * @default `version.json`
238
- */
239
- versionPath?: string
240
- }
241
- /**
242
- * signature config
243
- */
244
- keys?: {
245
- /**
246
- * Path to the pem file that contains private key
247
- * if not ended with .pem, it will be appended
248
- * @default 'public/private.pem'
249
- */
250
- privateKeyPath?: string
251
- /**
252
- * Path to the pem file that contains public key
253
- * if not ended with .pem, it will be appended
254
- * @default 'public/public.pem'
255
- */
256
- publicKeyPath?: string
257
- /**
258
- * Length of the key
259
- * @default 2048
260
- */
261
- keyLength?: number
262
- }
263
- }
264
- ```
265
-
266
196
  ### electron-builder config
267
197
 
268
198
  ```js
package/dist/index.cjs CHANGED
@@ -37,7 +37,8 @@ __export(src_exports, {
37
37
  getGithubReleaseCdnGroup: () => getGithubReleaseCdnGroup,
38
38
  initApp: () => initApp,
39
39
  parseGithubCdnURL: () => parseGithubCdnURL,
40
- requireNative: () => requireNative
40
+ requireNative: () => requireNative,
41
+ restartApp: () => restartApp
41
42
  });
42
43
  module.exports = __toCommonJS(src_exports);
43
44
  var import_node_path2 = require("path");
@@ -62,17 +63,18 @@ function downloadJSONDefault(url, updater, headers) {
62
63
  res.headers = headers;
63
64
  res.on("data", (chunk) => data += chunk);
64
65
  res.on("end", () => {
65
- updater.emit("downloadEnd", true);
66
- const json = JSON.parse(data);
67
- if ("signature" in json && "version" in json && "size" in json) {
68
- resolve2(json);
69
- } else {
70
- throw new Error("invalid update json");
66
+ try {
67
+ const json = JSON.parse(data);
68
+ if ("signature" in json && "version" in json && "size" in json) {
69
+ resolve2(json);
70
+ } else {
71
+ throw Error;
72
+ }
73
+ } catch (e) {
74
+ reject(new Error("invalid json"));
71
75
  }
72
76
  });
73
77
  }).on("error", (e) => {
74
- e && updater.emit("donwnloadError", e);
75
- updater.emit("downloadEnd", false);
76
78
  reject(e);
77
79
  });
78
80
  });
@@ -87,12 +89,9 @@ function downloadBufferDefault(url, updater, headers) {
87
89
  data.push(chunk);
88
90
  });
89
91
  res.on("end", () => {
90
- updater.emit("downloadEnd", true);
91
92
  resolve2(import_node_buffer.Buffer.concat(data));
92
93
  });
93
94
  }).on("error", (e) => {
94
- e && updater.emit("donwnloadError", e);
95
- updater.emit("downloadEnd", false);
96
95
  reject(e);
97
96
  });
98
97
  });
@@ -164,6 +163,10 @@ function getGithubReleaseCdnGroup() {
164
163
  { cdnPrefix: "download.nuaa.cf", maintainer: "LibraryCloud-nuaa" }
165
164
  ];
166
165
  }
166
+ function restartApp() {
167
+ import_electron.app.relaunch();
168
+ import_electron.app.quit();
169
+ }
167
170
 
168
171
  // src/updater/index.ts
169
172
  function createUpdater({
@@ -177,9 +180,13 @@ function createUpdater({
177
180
  compareVersion
178
181
  }) {
179
182
  const updater = new import_node_events.EventEmitter();
183
+ let signature = "";
184
+ let version = "";
185
+ const gzipPath = `../${productName}.asar.gz`;
186
+ const tmpFile = gzipPath.replace(".asar.gz", ".tmp.gz");
180
187
  const { downloadBuffer, downloadJSON, extraHeader, userAgent } = downloadConfig || {};
181
- function log(...args) {
182
- debug && console.log(...args);
188
+ function log(msg) {
189
+ debug && updater.emit("debug", msg);
183
190
  }
184
191
  async function download(url, format) {
185
192
  const ua = userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36";
@@ -188,12 +195,16 @@ function createUpdater({
188
195
  UserAgent: ua,
189
196
  ...extraHeader
190
197
  };
191
- log("[updater] headers", headers);
198
+ log(`download headers: ${JSON.stringify(headers, null, 2)}`);
192
199
  const downloadFn = format === "json" ? downloadJSON ?? downloadJSONDefault : downloadBuffer ?? downloadBufferDefault;
193
- return await downloadFn(url, updater, headers);
200
+ log(`download ${format} from ${url}`);
201
+ const ret = await downloadFn(url, updater, headers);
202
+ log(`download ${format} success`);
203
+ return ret;
194
204
  }
195
205
  async function extractFile(gzipFilePath) {
196
206
  if (!gzipFilePath.endsWith(".asar.gz") || !(0, import_node_fs2.existsSync)(gzipFilePath)) {
207
+ log("update .asar.gz file not exist");
197
208
  return;
198
209
  }
199
210
  gzipFilePath = gzipFilePath.replace(".asar.gz", ".tmp.gz");
@@ -202,108 +213,136 @@ function createUpdater({
202
213
  const input = (0, import_node_fs2.createReadStream)(gzipFilePath);
203
214
  const outputFilePath = gzipFilePath.replace(".tmp.gz", ".asar");
204
215
  const output = (0, import_node_fs2.createWriteStream)(outputFilePath);
205
- log("[updater] outputFilePath", outputFilePath);
216
+ log(`outputFilePath: ${outputFilePath}`);
206
217
  input.pipe(gunzip).pipe(output).on("finish", async () => {
207
218
  await (0, import_promises.rm)(gzipFilePath);
208
- log("[updater] finish");
219
+ log(`${gzipFilePath} unzipped`);
209
220
  resolve2(outputFilePath);
210
221
  }).on("error", async (err) => {
211
222
  await (0, import_promises.rm)(gzipFilePath);
212
- log("[updater] error", err);
213
223
  output.destroy(err);
214
224
  reject(err);
215
225
  });
216
226
  });
217
227
  }
218
- function verify(buffer, signature) {
219
- log("[updater] signature", signature);
220
- return (0, import_node_crypto.createVerify)("RSA-SHA256").update(buffer).verify(SIGNATURE_PUB, signature, "base64");
228
+ function verify(buffer, signature2) {
229
+ return (0, import_node_crypto.createVerify)("RSA-SHA256").update(buffer).verify(SIGNATURE_PUB, signature2, "base64");
221
230
  }
222
- function needUpdate(version) {
223
- if (!version || !import_electron2.app.isPackaged) {
231
+ function needUpdate(version2) {
232
+ if (!import_electron2.app.isPackaged) {
233
+ log("in dev mode, no need to update");
224
234
  return false;
225
235
  }
226
236
  const currentVersion = getEntryVersion();
227
- log("[updater] currentVersion", currentVersion);
228
- log("[updater] newVersion", version);
237
+ log(`check update:
238
+ current version is ${currentVersion},
239
+ new version is ${version2}`);
229
240
  const _compare = compareVersion ?? compareVersionDefault;
230
- return _compare(currentVersion, version);
241
+ return _compare(currentVersion, version2);
231
242
  }
232
- async function checkUpdate(option) {
233
- let {
234
- updateJsonURL = _update,
235
- releaseAsarURL = _release
236
- } = option || {};
237
- if (!updateJsonURL || !releaseAsarURL) {
238
- log("[updater] no updateJsonURL or releaseAsarURL, use repository");
243
+ async function checkUpdate(url) {
244
+ url ??= _update;
245
+ if (!url) {
246
+ log("no updateJsonURL, fallback to use repository");
239
247
  if (!repository) {
240
- throw new Error("updateJsonURL or releaseAsarURL are not set");
248
+ throw new Error("updateJsonURL or repository are not set");
241
249
  }
242
- updateJsonURL = `${repository.replace("github.com", "raw.githubusercontent.com")}/version.json`;
243
- releaseAsarURL = `${repository}/releases/download/latest/${productName}.asar.gz`;
250
+ url = `${repository.replace("github.com", "raw.githubusercontent.com")}/master/version.json`;
244
251
  }
245
- log("[updater] updateJsonURL", updateJsonURL);
246
- log("[updater] releaseAsarURL", releaseAsarURL);
247
- const gzipPath = `../${productName}.asar.gz`;
248
- const tmpFile = gzipPath.replace(".asar.gz", ".tmp.gz");
249
252
  if ((0, import_node_fs2.existsSync)(tmpFile)) {
250
- log("[updater] remove tmp file", tmpFile);
253
+ log(`remove tmp file: ${tmpFile}`);
251
254
  await (0, import_promises.rm)(tmpFile);
252
255
  }
253
- const json = await download(updateJsonURL, "json");
254
- if (!json) {
255
- throw new Error("fetch update json failed");
256
+ if ((0, import_node_fs2.existsSync)(gzipPath)) {
257
+ log(`remove .gz file: ${gzipPath}`);
258
+ await (0, import_promises.rm)(gzipPath);
256
259
  }
260
+ const json = await download(url, "json");
257
261
  const {
258
- signature,
259
- version,
262
+ signature: _sig,
263
+ version: _v,
260
264
  size
261
265
  } = json;
262
- log("[updater] UpdateJSON", json);
263
- if (!needUpdate(version)) {
264
- return "unavailable";
266
+ log(`update info: ${JSON.stringify(json, null, 2)}`);
267
+ if (!await needUpdate(_v)) {
268
+ log(`update unavailable: ${_v}`);
269
+ return false;
270
+ } else {
271
+ log(`update available: ${_v}`);
272
+ signature = _sig;
273
+ version = _v;
274
+ return { size, version };
275
+ }
276
+ }
277
+ async function downloadUpdate(src) {
278
+ if (typeof src !== "object") {
279
+ let _url = src ?? _release;
280
+ if (!_url) {
281
+ log("no releaseAsarURL, fallback to use repository");
282
+ if (!repository) {
283
+ throw new Error("releaseAsarURL or repository are not set");
284
+ }
285
+ _url = `${repository}/releases/download/latest/${productName}.asar.gz`;
286
+ }
287
+ src = await download(_url, "buffer");
265
288
  }
266
- updater.emit("downloadStart", size);
267
- const buffer = await download(releaseAsarURL, "buffer");
268
- log("[updater] start verify");
269
- if (!verify(buffer, signature)) {
270
- throw new Error("file broken, invalid signature!");
289
+ log("verify start");
290
+ if (!verify(src, signature)) {
291
+ log("verify failed");
292
+ throw new Error("invalid signature");
271
293
  }
272
- log("[updater] write file", gzipPath);
273
- await (0, import_promises.writeFile)(gzipPath, buffer);
274
- log("[updater] extract file", gzipPath);
294
+ log("verify success");
295
+ log(`write file: ${gzipPath}`);
296
+ await (0, import_promises.writeFile)(gzipPath, src);
297
+ log(`extract file: ${gzipPath}`);
275
298
  await extractFile(gzipPath);
276
- return "success";
299
+ log(`update success, version: ${version}`);
300
+ updater.emit("downloaded");
277
301
  }
278
- const onCheck = async (option) => {
302
+ const onCheck = async (url) => {
279
303
  try {
280
- const result = await checkUpdate(option);
304
+ const result = await checkUpdate(url);
281
305
  updater.emit("checkResult", result);
282
306
  } catch (error) {
283
- updater.emit("checkResult", "fail", error);
307
+ log(error);
308
+ updater.emit("checkResult", error);
284
309
  }
285
310
  };
286
311
  updater.on("check", onCheck);
287
312
  updater.checkUpdate = onCheck;
313
+ const onDownload = async (src) => {
314
+ try {
315
+ await downloadUpdate(src);
316
+ } catch (error) {
317
+ log(error);
318
+ updater.emit("donwnloadError", error);
319
+ }
320
+ };
321
+ updater.on("download", onDownload);
322
+ updater.downloadUpdate = onDownload;
288
323
  return updater;
289
324
  }
290
325
 
291
326
  // src/index.ts
292
- function initApp(productName, updater, option) {
327
+ function initApp(appOptions, updaterOptions) {
293
328
  const {
329
+ name: productName,
294
330
  electronDistPath = "dist-electron",
295
331
  mainPath = "main/index.js"
296
- } = option ?? {};
332
+ } = appOptions ?? {};
297
333
  const mainDir = import_electron3.app.isPackaged ? `../${productName}.asar` : electronDistPath;
298
334
  const entry = (0, import_node_path2.resolve)(__dirname, mainDir, mainPath);
299
- let _updater;
300
- if ("SIGNATURE_PUB" in updater) {
301
- const _option = updater.productName ? updater : { ...updater, productName };
302
- _updater = createUpdater(_option);
335
+ if (updaterOptions) {
336
+ require(entry)(
337
+ createUpdater({ ...updaterOptions, productName })
338
+ );
303
339
  } else {
304
- _updater = updater;
340
+ return {
341
+ setUpdater(updater) {
342
+ require(entry)(updater);
343
+ }
344
+ };
305
345
  }
306
- return require(entry)(_updater);
307
346
  }
308
347
  // Annotate the CommonJS export names for ESM import in node:
309
348
  0 && (module.exports = {
@@ -314,5 +353,6 @@ function initApp(productName, updater, option) {
314
353
  getGithubReleaseCdnGroup,
315
354
  initApp,
316
355
  parseGithubCdnURL,
317
- requireNative
356
+ requireNative,
357
+ restartApp
318
358
  });
package/dist/index.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
 
3
- type CheckResultType = 'success' | 'fail' | 'unavailable';
3
+ type CheckResultType = Error | false | Omit<UpdateJSON, 'signature'>;
4
4
  type UpdateEvents = {
5
- check: null;
6
- checkResult: [data: CheckResultType, err?: unknown];
7
- downloadStart: [size: number];
5
+ check: [url?: string];
6
+ checkResult: [data: CheckResultType];
7
+ download: [src?: string | Buffer];
8
8
  downloading: [current: number];
9
- downloadEnd: [success: boolean];
9
+ downloaded: null;
10
10
  donwnloadError: [error: unknown];
11
+ debug: [msg: string | Error];
11
12
  };
12
13
  type UpdateJSON = {
13
14
  signature: string;
@@ -15,20 +16,6 @@ type UpdateJSON = {
15
16
  size: number;
16
17
  };
17
18
  type MaybeArray<T> = T extends undefined | null | never ? [] : T extends any[] ? T['length'] extends 1 ? [data: T[0]] : T : [data: T];
18
- interface BaseOption {
19
- /**
20
- * URL of version info json
21
- * @default `${repository.replace('github.com', 'raw.githubusercontent.com')}/version.json`
22
- * @throws if `updateJsonURL` and `repository` are all not set
23
- */
24
- updateJsonURL?: string;
25
- /**
26
- * URL of release asar.gz
27
- * @default `${repository}/releases/download/latest/${productName}.asar.gz`
28
- * @throws if `releaseAsarURL` and `repository` are all not set
29
- */
30
- releaseAsarURL?: string;
31
- }
32
19
  interface TypedUpdater<T extends Record<string | symbol, MaybeArray<any>>, Event extends Exclude<keyof T, number> = Exclude<keyof T, number>> {
33
20
  removeAllListeners<E extends Event>(event?: E): this;
34
21
  listeners<E extends Event>(eventName: E): Function[];
@@ -37,10 +24,16 @@ interface TypedUpdater<T extends Record<string | symbol, MaybeArray<any>>, Event
37
24
  once<E extends Event>(eventName: E, listener: (...args: MaybeArray<T[E]>) => void): this;
38
25
  emit<E extends Event>(eventName: E, ...args: MaybeArray<T[E]>): boolean;
39
26
  off<E extends Event>(eventName: E, listener: (...args: MaybeArray<T[E]>) => void): this;
40
- checkUpdate(options?: BaseOption): Promise<void>;
27
+ /**
28
+ * - `undefined`: errror
29
+ * - `false`: unavailable
30
+ * - `{size: number, version: string}`: success
31
+ */
32
+ checkUpdate(url?: string): Promise<void>;
33
+ downloadUpdate(url?: string | Buffer): Promise<void>;
41
34
  }
42
35
  type Updater = TypedUpdater<UpdateEvents>;
43
- interface UpdaterOption extends BaseOption {
36
+ interface UpdaterOption {
44
37
  /**
45
38
  * public key of signature
46
39
  *
@@ -58,7 +51,7 @@ interface UpdaterOption extends BaseOption {
58
51
  */
59
52
  SIGNATURE_PUB: string;
60
53
  /**
61
- * product name
54
+ * name of your application
62
55
  *
63
56
  * you can use the `name` in `package.json`
64
57
  */
@@ -72,8 +65,29 @@ interface UpdaterOption extends BaseOption {
72
65
  * `repository` will be used to determine the url
73
66
  */
74
67
  repository?: string;
68
+ /**
69
+ * URL of version info json
70
+ * @default `${repository.replace('github.com', 'raw.githubusercontent.com')}/master/version.json`
71
+ * @throws if `updateJsonURL` and `repository` are all not set
72
+ */
73
+ updateJsonURL?: string;
74
+ /**
75
+ * URL of release asar.gz
76
+ * @default `${repository}/releases/download/latest/${productName}.asar.gz`
77
+ * @throws if `releaseAsarURL` and `repository` are all not set
78
+ */
79
+ releaseAsarURL?: string;
80
+ /**
81
+ * whether to enable debug listener
82
+ */
75
83
  debug?: boolean;
76
- compareVersion?: (oldVersion: string, newVersion: string) => boolean;
84
+ /**
85
+ * custom version compare function
86
+ * @param oldVersion old version string
87
+ * @param newVersion new version string
88
+ * @returns whether to update
89
+ */
90
+ compareVersion?: (oldVersion: string, newVersion: string) => boolean | Promise<boolean>;
77
91
  downloadConfig?: {
78
92
  /**
79
93
  * download user agent
@@ -87,7 +101,7 @@ interface UpdaterOption extends BaseOption {
87
101
  /**
88
102
  * download JSON function
89
103
  * @param url download url
90
- * @param updater updater, emit events
104
+ * @param updater updater, to trigger events
91
105
  * @param header download header
92
106
  * @returns `UpdateJSON`
93
107
  */
@@ -95,7 +109,7 @@ interface UpdaterOption extends BaseOption {
95
109
  /**
96
110
  * download buffer function
97
111
  * @param url download url
98
- * @param updater updater, emit events
112
+ * @param updater updater, to trigger events
99
113
  * @param header download header
100
114
  * @returns `Buffer`
101
115
  */
@@ -133,64 +147,67 @@ declare function getGithubReleaseCdnGroup(): {
133
147
  cdnPrefix: string;
134
148
  maintainer: string;
135
149
  }[];
150
+ declare function restartApp(): void;
136
151
 
137
152
  declare function createUpdater({ SIGNATURE_PUB, repository, productName, releaseAsarURL: _release, updateJsonURL: _update, debug, downloadConfig, compareVersion, }: UpdaterOption): Updater;
138
153
 
139
- interface AppOption {
154
+ type AppOption = {
155
+ /**
156
+ * name of your application
157
+ *
158
+ * you can use the `name` in `package.json`
159
+ */
160
+ name: string;
140
161
  /**
141
162
  * path of electron output dist
142
163
  * @default 'dist-electron'
143
- */
164
+ */
144
165
  electronDistPath?: string;
145
166
  /**
146
167
  * relative path of main entry in electron dist
147
168
  * @default 'main/index.js'
148
- */
169
+ */
149
170
  mainPath?: string;
150
- }
171
+ };
172
+ type OptionalProperty<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
173
+ type InitUpdaterOptions = OptionalProperty<UpdaterOption, 'productName'>;
151
174
  /**
152
- * Initialize application
153
- * @param productName name of your application
154
- * @param updater updater instance or updater options
155
- * @param option options for entry, will be used to generate electron main path, default target path: `dist-electron/main/index.js`
156
- * @returns a function to init your application with a updater
157
- *
175
+ * create updater manually
158
176
  * @example
159
- * **manual** generate updater
160
177
  * ```ts
161
- * import { initApp } from 'electron-incremental-updater'
178
+ * import { createUpdater, getGithubReleaseCdnGroup, initApp, parseGithubCdnURL } from 'electron-incremental-update'
162
179
  * import { name, repository } from '../package.json'
163
180
  *
164
- * const SIGNATURE_PUB = '' // auto generate RSA public key when start app
181
+ * const SIGNATURE_PUB = '' // auto generate
182
+ *
183
+ * const { cdnPrefix } = getGithubReleaseCdnGroup()[0]
165
184
  * const updater = createUpdater({
166
185
  * SIGNATURE_PUB,
167
186
  * productName: name,
168
187
  * repository,
188
+ * updateJsonURL: parseGithubCdnURL(repository, 'fastly.jsdelivr.net/gh', 'version.json'),
189
+ * releaseAsarURL: parseGithubCdnURL(repository, cdnPrefix, `download/latest/${name}.asar.gz`),
190
+ * debug: true,
169
191
  * })
170
- * initApp(name, updater)
192
+ * initApp({ name }).setUpdater(updater)
171
193
  * ```
172
- * @example
173
- * **auto** generate updater and set update URL
194
+ */
195
+ declare function initApp(appOptions: AppOption): {
196
+ setUpdater: (updater: Updater) => void;
197
+ };
198
+ /**
199
+ * create updater when init, no need to set productName
174
200
  *
201
+ * @example
175
202
  * ```ts
176
- * import { getReleaseDnsPrefix, initApp } from 'electron-incremental-update'
203
+ * import { initApp } from 'electron-incremental-update'
177
204
  * import { name, repository } from '../package.json'
178
205
  *
179
- * const SIGNATURE_PUB = '' // auto generate RSA public key when start app
206
+ * const SIGNATURE_PUB = '' // auto generate
180
207
  *
181
- * const { urlPrefix } = getReleaseCdnPrefix()[0]
182
- * initApp(name, {
183
- * SIGNATURE_PUB,
184
- * repository,
185
- * updateJsonURL: `https://cdn.jsdelivr.net/gh/${repository.replace('https://github.com', '')}/version.json`,
186
- * releaseAsarURL: `${urlPrefix}/download/latest/${name}.asar.gz`,
187
- * }, {
188
- * // options for main entry
189
- * })
208
+ * initApp({ name }, { SIGNATURE_PUB, repository })
190
209
  * ```
191
- */
192
- declare function initApp(productName: string, updater: Updater | Omit<UpdaterOption, 'productName'> & {
193
- productName?: string;
194
- }, option?: AppOption): any;
210
+ */
211
+ declare function initApp(appOptions: AppOption, updaterOptions: InitUpdaterOptions): undefined;
195
212
 
196
- export { BaseOption, CheckResultType, UpdateJSON, Updater, UpdaterOption, createUpdater, getAppAsarPath, getAppVersion, getEntryVersion, getGithubReleaseCdnGroup, initApp, parseGithubCdnURL, requireNative };
213
+ export { AppOption, CheckResultType, InitUpdaterOptions, UpdateJSON, Updater, UpdaterOption, createUpdater, getAppAsarPath, getAppVersion, getEntryVersion, getGithubReleaseCdnGroup, initApp, parseGithubCdnURL, requireNative, restartApp };
package/dist/index.mjs CHANGED
@@ -25,17 +25,18 @@ function downloadJSONDefault(url, updater, headers) {
25
25
  res.headers = headers;
26
26
  res.on("data", (chunk) => data += chunk);
27
27
  res.on("end", () => {
28
- updater.emit("downloadEnd", true);
29
- const json = JSON.parse(data);
30
- if ("signature" in json && "version" in json && "size" in json) {
31
- resolve2(json);
32
- } else {
33
- throw new Error("invalid update json");
28
+ try {
29
+ const json = JSON.parse(data);
30
+ if ("signature" in json && "version" in json && "size" in json) {
31
+ resolve2(json);
32
+ } else {
33
+ throw Error;
34
+ }
35
+ } catch (e) {
36
+ reject(new Error("invalid json"));
34
37
  }
35
38
  });
36
39
  }).on("error", (e) => {
37
- e && updater.emit("donwnloadError", e);
38
- updater.emit("downloadEnd", false);
39
40
  reject(e);
40
41
  });
41
42
  });
@@ -50,12 +51,9 @@ function downloadBufferDefault(url, updater, headers) {
50
51
  data.push(chunk);
51
52
  });
52
53
  res.on("end", () => {
53
- updater.emit("downloadEnd", true);
54
54
  resolve2(Buffer.concat(data));
55
55
  });
56
56
  }).on("error", (e) => {
57
- e && updater.emit("donwnloadError", e);
58
- updater.emit("downloadEnd", false);
59
57
  reject(e);
60
58
  });
61
59
  });
@@ -127,6 +125,10 @@ function getGithubReleaseCdnGroup() {
127
125
  { cdnPrefix: "download.nuaa.cf", maintainer: "LibraryCloud-nuaa" }
128
126
  ];
129
127
  }
128
+ function restartApp() {
129
+ app.relaunch();
130
+ app.quit();
131
+ }
130
132
 
131
133
  // src/updater/index.ts
132
134
  function createUpdater({
@@ -140,9 +142,13 @@ function createUpdater({
140
142
  compareVersion
141
143
  }) {
142
144
  const updater = new EventEmitter();
145
+ let signature = "";
146
+ let version = "";
147
+ const gzipPath = `../${productName}.asar.gz`;
148
+ const tmpFile = gzipPath.replace(".asar.gz", ".tmp.gz");
143
149
  const { downloadBuffer, downloadJSON, extraHeader, userAgent } = downloadConfig || {};
144
- function log(...args) {
145
- debug && console.log(...args);
150
+ function log(msg) {
151
+ debug && updater.emit("debug", msg);
146
152
  }
147
153
  async function download(url, format) {
148
154
  const ua = userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36";
@@ -151,12 +157,16 @@ function createUpdater({
151
157
  UserAgent: ua,
152
158
  ...extraHeader
153
159
  };
154
- log("[updater] headers", headers);
160
+ log(`download headers: ${JSON.stringify(headers, null, 2)}`);
155
161
  const downloadFn = format === "json" ? downloadJSON ?? downloadJSONDefault : downloadBuffer ?? downloadBufferDefault;
156
- return await downloadFn(url, updater, headers);
162
+ log(`download ${format} from ${url}`);
163
+ const ret = await downloadFn(url, updater, headers);
164
+ log(`download ${format} success`);
165
+ return ret;
157
166
  }
158
167
  async function extractFile(gzipFilePath) {
159
168
  if (!gzipFilePath.endsWith(".asar.gz") || !existsSync(gzipFilePath)) {
169
+ log("update .asar.gz file not exist");
160
170
  return;
161
171
  }
162
172
  gzipFilePath = gzipFilePath.replace(".asar.gz", ".tmp.gz");
@@ -165,108 +175,136 @@ function createUpdater({
165
175
  const input = createReadStream(gzipFilePath);
166
176
  const outputFilePath = gzipFilePath.replace(".tmp.gz", ".asar");
167
177
  const output = createWriteStream(outputFilePath);
168
- log("[updater] outputFilePath", outputFilePath);
178
+ log(`outputFilePath: ${outputFilePath}`);
169
179
  input.pipe(gunzip).pipe(output).on("finish", async () => {
170
180
  await rm(gzipFilePath);
171
- log("[updater] finish");
181
+ log(`${gzipFilePath} unzipped`);
172
182
  resolve2(outputFilePath);
173
183
  }).on("error", async (err) => {
174
184
  await rm(gzipFilePath);
175
- log("[updater] error", err);
176
185
  output.destroy(err);
177
186
  reject(err);
178
187
  });
179
188
  });
180
189
  }
181
- function verify(buffer, signature) {
182
- log("[updater] signature", signature);
183
- return createVerify("RSA-SHA256").update(buffer).verify(SIGNATURE_PUB, signature, "base64");
190
+ function verify(buffer, signature2) {
191
+ return createVerify("RSA-SHA256").update(buffer).verify(SIGNATURE_PUB, signature2, "base64");
184
192
  }
185
- function needUpdate(version) {
186
- if (!version || !app2.isPackaged) {
193
+ function needUpdate(version2) {
194
+ if (!app2.isPackaged) {
195
+ log("in dev mode, no need to update");
187
196
  return false;
188
197
  }
189
198
  const currentVersion = getEntryVersion();
190
- log("[updater] currentVersion", currentVersion);
191
- log("[updater] newVersion", version);
199
+ log(`check update:
200
+ current version is ${currentVersion},
201
+ new version is ${version2}`);
192
202
  const _compare = compareVersion ?? compareVersionDefault;
193
- return _compare(currentVersion, version);
203
+ return _compare(currentVersion, version2);
194
204
  }
195
- async function checkUpdate(option) {
196
- let {
197
- updateJsonURL = _update,
198
- releaseAsarURL = _release
199
- } = option || {};
200
- if (!updateJsonURL || !releaseAsarURL) {
201
- log("[updater] no updateJsonURL or releaseAsarURL, use repository");
205
+ async function checkUpdate(url) {
206
+ url ??= _update;
207
+ if (!url) {
208
+ log("no updateJsonURL, fallback to use repository");
202
209
  if (!repository) {
203
- throw new Error("updateJsonURL or releaseAsarURL are not set");
210
+ throw new Error("updateJsonURL or repository are not set");
204
211
  }
205
- updateJsonURL = `${repository.replace("github.com", "raw.githubusercontent.com")}/version.json`;
206
- releaseAsarURL = `${repository}/releases/download/latest/${productName}.asar.gz`;
212
+ url = `${repository.replace("github.com", "raw.githubusercontent.com")}/master/version.json`;
207
213
  }
208
- log("[updater] updateJsonURL", updateJsonURL);
209
- log("[updater] releaseAsarURL", releaseAsarURL);
210
- const gzipPath = `../${productName}.asar.gz`;
211
- const tmpFile = gzipPath.replace(".asar.gz", ".tmp.gz");
212
214
  if (existsSync(tmpFile)) {
213
- log("[updater] remove tmp file", tmpFile);
215
+ log(`remove tmp file: ${tmpFile}`);
214
216
  await rm(tmpFile);
215
217
  }
216
- const json = await download(updateJsonURL, "json");
217
- if (!json) {
218
- throw new Error("fetch update json failed");
218
+ if (existsSync(gzipPath)) {
219
+ log(`remove .gz file: ${gzipPath}`);
220
+ await rm(gzipPath);
219
221
  }
222
+ const json = await download(url, "json");
220
223
  const {
221
- signature,
222
- version,
224
+ signature: _sig,
225
+ version: _v,
223
226
  size
224
227
  } = json;
225
- log("[updater] UpdateJSON", json);
226
- if (!needUpdate(version)) {
227
- return "unavailable";
228
+ log(`update info: ${JSON.stringify(json, null, 2)}`);
229
+ if (!await needUpdate(_v)) {
230
+ log(`update unavailable: ${_v}`);
231
+ return false;
232
+ } else {
233
+ log(`update available: ${_v}`);
234
+ signature = _sig;
235
+ version = _v;
236
+ return { size, version };
237
+ }
238
+ }
239
+ async function downloadUpdate(src) {
240
+ if (typeof src !== "object") {
241
+ let _url = src ?? _release;
242
+ if (!_url) {
243
+ log("no releaseAsarURL, fallback to use repository");
244
+ if (!repository) {
245
+ throw new Error("releaseAsarURL or repository are not set");
246
+ }
247
+ _url = `${repository}/releases/download/latest/${productName}.asar.gz`;
248
+ }
249
+ src = await download(_url, "buffer");
228
250
  }
229
- updater.emit("downloadStart", size);
230
- const buffer = await download(releaseAsarURL, "buffer");
231
- log("[updater] start verify");
232
- if (!verify(buffer, signature)) {
233
- throw new Error("file broken, invalid signature!");
251
+ log("verify start");
252
+ if (!verify(src, signature)) {
253
+ log("verify failed");
254
+ throw new Error("invalid signature");
234
255
  }
235
- log("[updater] write file", gzipPath);
236
- await writeFile(gzipPath, buffer);
237
- log("[updater] extract file", gzipPath);
256
+ log("verify success");
257
+ log(`write file: ${gzipPath}`);
258
+ await writeFile(gzipPath, src);
259
+ log(`extract file: ${gzipPath}`);
238
260
  await extractFile(gzipPath);
239
- return "success";
261
+ log(`update success, version: ${version}`);
262
+ updater.emit("downloaded");
240
263
  }
241
- const onCheck = async (option) => {
264
+ const onCheck = async (url) => {
242
265
  try {
243
- const result = await checkUpdate(option);
266
+ const result = await checkUpdate(url);
244
267
  updater.emit("checkResult", result);
245
268
  } catch (error) {
246
- updater.emit("checkResult", "fail", error);
269
+ log(error);
270
+ updater.emit("checkResult", error);
247
271
  }
248
272
  };
249
273
  updater.on("check", onCheck);
250
274
  updater.checkUpdate = onCheck;
275
+ const onDownload = async (src) => {
276
+ try {
277
+ await downloadUpdate(src);
278
+ } catch (error) {
279
+ log(error);
280
+ updater.emit("donwnloadError", error);
281
+ }
282
+ };
283
+ updater.on("download", onDownload);
284
+ updater.downloadUpdate = onDownload;
251
285
  return updater;
252
286
  }
253
287
 
254
288
  // src/index.ts
255
- function initApp(productName, updater, option) {
289
+ function initApp(appOptions, updaterOptions) {
256
290
  const {
291
+ name: productName,
257
292
  electronDistPath = "dist-electron",
258
293
  mainPath = "main/index.js"
259
- } = option ?? {};
294
+ } = appOptions ?? {};
260
295
  const mainDir = app3.isPackaged ? `../${productName}.asar` : electronDistPath;
261
296
  const entry = resolve(__dirname, mainDir, mainPath);
262
- let _updater;
263
- if ("SIGNATURE_PUB" in updater) {
264
- const _option = updater.productName ? updater : { ...updater, productName };
265
- _updater = createUpdater(_option);
297
+ if (updaterOptions) {
298
+ __require(entry)(
299
+ createUpdater({ ...updaterOptions, productName })
300
+ );
266
301
  } else {
267
- _updater = updater;
302
+ return {
303
+ setUpdater(updater) {
304
+ __require(entry)(updater);
305
+ }
306
+ };
268
307
  }
269
- return __require(entry)(_updater);
270
308
  }
271
309
  export {
272
310
  createUpdater,
@@ -276,5 +314,6 @@ export {
276
314
  getGithubReleaseCdnGroup,
277
315
  initApp,
278
316
  parseGithubCdnURL,
279
- requireNative
317
+ requireNative,
318
+ restartApp
280
319
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electron-incremental-update",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "electron incremental update tools, powered by vite",
5
5
  "scripts": {
6
6
  "build": "tsup",
@@ -63,4 +63,4 @@
63
63
  "dependencies": {
64
64
  "ci-info": "^3.8.0"
65
65
  }
66
- }
66
+ }