electron-incremental-update 0.3.0 → 0.5.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/dist/index.d.ts CHANGED
@@ -1,13 +1,9 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
 
3
- type CheckResultType = Error | false | Omit<UpdateJSON, 'signature'>;
3
+ type CheckResultType = Omit<UpdateJSON, 'signature'> | undefined | Error;
4
+ type DownloadResult = true | Error;
4
5
  type UpdateEvents = {
5
- check: null;
6
- checkResult: [data: CheckResultType];
7
- download: null;
8
- downloading: [current: number];
9
- downloaded: null;
10
- donwnloadError: [error: unknown];
6
+ downloading: [progress: number];
11
7
  debug: [msg: string | Error];
12
8
  };
13
9
  type UpdateJSON = {
@@ -16,38 +12,27 @@ type UpdateJSON = {
16
12
  size: number;
17
13
  };
18
14
  type MaybeArray<T> = T extends undefined | null | never ? [] : T extends any[] ? T['length'] extends 1 ? [data: T[0]] : T : [data: T];
19
- interface BaseOption {
20
- /**
21
- * URL of version info json
22
- * @default `${repository.replace('github.com', 'raw.githubusercontent.com')}/version.json`
23
- * @throws if `updateJsonURL` and `repository` are all not set
24
- */
25
- updateJsonURL?: string;
26
- /**
27
- * URL of release asar.gz
28
- * @default `${repository}/releases/download/latest/${productName}.asar.gz`
29
- * @throws if `releaseAsarURL` and `repository` are all not set
30
- */
31
- releaseAsarURL?: string;
32
- }
33
15
  interface TypedUpdater<T extends Record<string | symbol, MaybeArray<any>>, Event extends Exclude<keyof T, number> = Exclude<keyof T, number>> {
34
16
  removeAllListeners<E extends Event>(event?: E): this;
35
17
  listeners<E extends Event>(eventName: E): Function[];
36
18
  eventNames(): (Event)[];
37
19
  on<E extends Event>(eventName: E, listener: (...data: MaybeArray<T[E]>) => void): this;
38
- once<E extends Event>(eventName: E, listener: (...args: MaybeArray<T[E]>) => void): this;
39
20
  emit<E extends Event>(eventName: E, ...args: MaybeArray<T[E]>): boolean;
40
21
  off<E extends Event>(eventName: E, listener: (...args: MaybeArray<T[E]>) => void): this;
41
22
  /**
42
- * - `undefined`: errror
23
+ * - `{size: number, version: string}`: available
43
24
  * - `false`: unavailable
44
- * - `{size: number, version: string}`: success
25
+ * - `Error`: fail
45
26
  */
46
- checkUpdate(url?: BaseOption['updateJsonURL']): Promise<void>;
47
- downloadUpdate(url?: BaseOption['releaseAsarURL'] | Buffer): Promise<void>;
27
+ checkUpdate(url?: string): Promise<CheckResultType>;
28
+ /**
29
+ * - `true`: success
30
+ * - `Error`: fail
31
+ */
32
+ downloadUpdate(url?: string | Buffer): Promise<DownloadResult>;
48
33
  }
49
34
  type Updater = TypedUpdater<UpdateEvents>;
50
- interface UpdaterOption extends BaseOption {
35
+ interface UpdaterOption {
51
36
  /**
52
37
  * public key of signature
53
38
  *
@@ -65,7 +50,7 @@ interface UpdaterOption extends BaseOption {
65
50
  */
66
51
  SIGNATURE_PUB: string;
67
52
  /**
68
- * product name
53
+ * name of your application
69
54
  *
70
55
  * you can use the `name` in `package.json`
71
56
  */
@@ -79,8 +64,29 @@ interface UpdaterOption extends BaseOption {
79
64
  * `repository` will be used to determine the url
80
65
  */
81
66
  repository?: string;
67
+ /**
68
+ * URL of version info json
69
+ * @default `${repository.replace('github.com', 'raw.githubusercontent.com')}/master/version.json`
70
+ * @throws if `updateJsonURL` and `repository` are all not set
71
+ */
72
+ updateJsonURL?: string;
73
+ /**
74
+ * URL of release asar.gz
75
+ * @default `${repository}/releases/download/latest/${productName}.asar.gz`
76
+ * @throws if `releaseAsarURL` and `repository` are all not set
77
+ */
78
+ releaseAsarURL?: string;
79
+ /**
80
+ * whether to enable debug listener
81
+ */
82
82
  debug?: boolean;
83
- compareVersion?: (oldVersion: string, newVersion: string) => boolean;
83
+ /**
84
+ * custom version compare function
85
+ * @param oldVersion old version string
86
+ * @param newVersion new version string
87
+ * @returns whether to update
88
+ */
89
+ compareVersion?: (oldVersion: string, newVersion: string) => boolean | Promise<boolean>;
84
90
  downloadConfig?: {
85
91
  /**
86
92
  * download user agent
@@ -94,7 +100,7 @@ interface UpdaterOption extends BaseOption {
94
100
  /**
95
101
  * download JSON function
96
102
  * @param url download url
97
- * @param updater updater, emit events
103
+ * @param updater updater, to trigger events
98
104
  * @param header download header
99
105
  * @returns `UpdateJSON`
100
106
  */
@@ -102,7 +108,7 @@ interface UpdaterOption extends BaseOption {
102
108
  /**
103
109
  * download buffer function
104
110
  * @param url download url
105
- * @param updater updater, emit events
111
+ * @param updater updater, to trigger events
106
112
  * @param header download header
107
113
  * @returns `Buffer`
108
114
  */
@@ -144,61 +150,63 @@ declare function restartApp(): void;
144
150
 
145
151
  declare function createUpdater({ SIGNATURE_PUB, repository, productName, releaseAsarURL: _release, updateJsonURL: _update, debug, downloadConfig, compareVersion, }: UpdaterOption): Updater;
146
152
 
147
- interface AppOption {
153
+ type AppOption = {
154
+ /**
155
+ * name of your application
156
+ *
157
+ * you can use the `name` in `package.json`
158
+ */
159
+ name: string;
148
160
  /**
149
161
  * path of electron output dist
150
162
  * @default 'dist-electron'
151
- */
163
+ */
152
164
  electronDistPath?: string;
153
165
  /**
154
166
  * relative path of main entry in electron dist
155
167
  * @default 'main/index.js'
156
- */
168
+ */
157
169
  mainPath?: string;
158
- }
170
+ };
171
+ type OptionalProperty<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
172
+ type InitUpdaterOptions = OptionalProperty<UpdaterOption, 'productName'>;
159
173
  /**
160
- * Initialize application
161
- * @param productName name of your application
162
- * @param updater updater instance or updater options
163
- * @param option options for entry, will be used to generate electron main path, default target path: `dist-electron/main/index.js`
164
- * @returns a function to init your application with a updater
165
- *
174
+ * create updater manually
166
175
  * @example
167
- * **manual** generate updater
168
176
  * ```ts
169
- * import { initApp } from 'electron-incremental-updater'
177
+ * import { createUpdater, getGithubReleaseCdnGroup, initApp, parseGithubCdnURL } from 'electron-incremental-update'
170
178
  * import { name, repository } from '../package.json'
171
179
  *
172
- * const SIGNATURE_PUB = '' // auto generate RSA public key when start app
180
+ * const SIGNATURE_PUB = '' // auto generate
181
+ *
182
+ * const { cdnPrefix } = getGithubReleaseCdnGroup()[0]
173
183
  * const updater = createUpdater({
174
184
  * SIGNATURE_PUB,
175
185
  * productName: name,
176
186
  * repository,
187
+ * updateJsonURL: parseGithubCdnURL(repository, 'fastly.jsdelivr.net/gh', 'version.json'),
188
+ * releaseAsarURL: parseGithubCdnURL(repository, cdnPrefix, `download/latest/${name}.asar.gz`),
189
+ * debug: true,
177
190
  * })
178
- * initApp(name, updater)
191
+ * initApp({ name }).setUpdater(updater)
179
192
  * ```
180
- * @example
181
- * **auto** generate updater and set update URL
193
+ */
194
+ declare function initApp(appOptions: AppOption): {
195
+ setUpdater: (updater: Updater) => void;
196
+ };
197
+ /**
198
+ * create updater when init, no need to set productName
182
199
  *
200
+ * @example
183
201
  * ```ts
184
- * import { getReleaseDnsPrefix, initApp } from 'electron-incremental-update'
202
+ * import { initApp } from 'electron-incremental-update'
185
203
  * import { name, repository } from '../package.json'
186
204
  *
187
- * const SIGNATURE_PUB = '' // auto generate RSA public key when start app
205
+ * const SIGNATURE_PUB = '' // auto generate
188
206
  *
189
- * const { urlPrefix } = getReleaseCdnPrefix()[0]
190
- * initApp(name, {
191
- * SIGNATURE_PUB,
192
- * repository,
193
- * updateJsonURL: `https://cdn.jsdelivr.net/gh/${repository.replace('https://github.com', '')}/version.json`,
194
- * releaseAsarURL: `${urlPrefix}/download/latest/${name}.asar.gz`,
195
- * }, {
196
- * // options for main entry
197
- * })
207
+ * initApp({ name }, { SIGNATURE_PUB, repository })
198
208
  * ```
199
- */
200
- declare function initApp(productName: string, updater: Updater | Omit<UpdaterOption, 'productName'> & {
201
- productName?: string;
202
- }, option?: AppOption): any;
209
+ */
210
+ declare function initApp(appOptions: AppOption, updaterOptions: InitUpdaterOptions): undefined;
203
211
 
204
- export { BaseOption, CheckResultType, UpdateJSON, Updater, UpdaterOption, createUpdater, getAppAsarPath, getAppVersion, getEntryVersion, getGithubReleaseCdnGroup, initApp, parseGithubCdnURL, requireNative, restartApp };
212
+ export { AppOption, CheckResultType, DownloadResult, InitUpdaterOptions, UpdateJSON, Updater, UpdaterOption, createUpdater, getAppAsarPath, getAppVersion, getEntryVersion, getGithubReleaseCdnGroup, initApp, parseGithubCdnURL, requireNative, restartApp };
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
- __require
3
- } from "./chunk-AKU6F3WT.mjs";
2
+ __require,
3
+ verify
4
+ } from "./chunk-SSJ6PDMK.mjs";
4
5
 
5
6
  // src/index.ts
6
7
  import { resolve } from "node:path";
@@ -8,7 +9,6 @@ import { app as app3 } from "electron";
8
9
 
9
10
  // src/updater/index.ts
10
11
  import { EventEmitter } from "node:events";
11
- import { createVerify } from "node:crypto";
12
12
  import { createGunzip } from "node:zlib";
13
13
  import { createReadStream, createWriteStream, existsSync } from "node:fs";
14
14
  import { rm, writeFile } from "node:fs/promises";
@@ -25,11 +25,15 @@ function downloadJSONDefault(url, updater, headers) {
25
25
  res.headers = headers;
26
26
  res.on("data", (chunk) => data += chunk);
27
27
  res.on("end", () => {
28
- const json = JSON.parse(data);
29
- if ("signature" in json && "version" in json && "size" in json) {
30
- resolve2(json);
31
- } else {
32
- 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"));
33
37
  }
34
38
  });
35
39
  }).on("error", (e) => {
@@ -38,12 +42,14 @@ function downloadJSONDefault(url, updater, headers) {
38
42
  });
39
43
  }
40
44
  function downloadBufferDefault(url, updater, headers) {
45
+ let progress = 0;
41
46
  return new Promise((resolve2, reject) => {
42
47
  https.get(url, (res) => {
43
48
  let data = [];
44
49
  res.headers = headers;
45
50
  res.on("data", (chunk) => {
46
- updater.emit("downloading", chunk.length);
51
+ progress += chunk.length;
52
+ updater.emit("downloading", progress);
47
53
  data.push(chunk);
48
54
  });
49
55
  res.on("end", () => {
@@ -139,6 +145,7 @@ function createUpdater({
139
145
  }) {
140
146
  const updater = new EventEmitter();
141
147
  let signature = "";
148
+ let version = "";
142
149
  const gzipPath = `../${productName}.asar.gz`;
143
150
  const tmpFile = gzipPath.replace(".asar.gz", ".tmp.gz");
144
151
  const { downloadBuffer, downloadJSON, extraHeader, userAgent } = downloadConfig || {};
@@ -152,12 +159,16 @@ function createUpdater({
152
159
  UserAgent: ua,
153
160
  ...extraHeader
154
161
  };
155
- log(`headers: ${headers}`);
162
+ log(`download headers: ${JSON.stringify(headers, null, 2)}`);
156
163
  const downloadFn = format === "json" ? downloadJSON ?? downloadJSONDefault : downloadBuffer ?? downloadBufferDefault;
157
- return await downloadFn(url, updater, headers);
164
+ log(`download ${format} from ${url}`);
165
+ const ret = await downloadFn(url, updater, headers);
166
+ log(`download ${format} success`);
167
+ return ret;
158
168
  }
159
169
  async function extractFile(gzipFilePath) {
160
170
  if (!gzipFilePath.endsWith(".asar.gz") || !existsSync(gzipFilePath)) {
171
+ log("update .asar.gz file not exist");
161
172
  return;
162
173
  }
163
174
  gzipFilePath = gzipFilePath.replace(".asar.gz", ".tmp.gz");
@@ -169,121 +180,119 @@ function createUpdater({
169
180
  log(`outputFilePath: ${outputFilePath}`);
170
181
  input.pipe(gunzip).pipe(output).on("finish", async () => {
171
182
  await rm(gzipFilePath);
172
- log("finish");
183
+ log(`${gzipFilePath} unzipped`);
173
184
  resolve2(outputFilePath);
174
185
  }).on("error", async (err) => {
175
186
  await rm(gzipFilePath);
176
- log(`error: ${err}`);
177
187
  output.destroy(err);
178
188
  reject(err);
179
189
  });
180
190
  });
181
191
  }
182
- function verify(buffer, signature2) {
183
- log(`signature: ${signature2}`);
184
- return createVerify("RSA-SHA256").update(buffer).verify(SIGNATURE_PUB, signature2, "base64");
185
- }
186
- function needUpdate(version) {
187
- if (!version || !app2.isPackaged) {
192
+ function needUpdate(version2) {
193
+ if (!app2.isPackaged) {
194
+ log("in dev mode, no need to update");
188
195
  return false;
189
196
  }
190
197
  const currentVersion = getEntryVersion();
191
- log(`currentVersion: ${currentVersion}`);
192
- log(`newVersion: ${version}`);
198
+ log(`check update:
199
+ current version is ${currentVersion},
200
+ new version is ${version2}`);
193
201
  const _compare = compareVersion ?? compareVersionDefault;
194
- return _compare(currentVersion, version);
195
- }
196
- async function checkUpdate(url) {
197
- url ??= _update;
198
- if (!url) {
199
- log("no updateJsonURL, use repository");
200
- if (!repository) {
201
- throw new Error("updateJsonURL or repository are not set");
202
- }
203
- url = `${repository.replace("github.com", "raw.githubusercontent.com")}/version.json`;
204
- }
205
- log(`updateJsonURL: ${url}`);
206
- if (existsSync(tmpFile)) {
207
- log(`remove tmp file: ${tmpFile}`);
208
- await rm(tmpFile);
209
- }
210
- const json = await download(url, "json");
211
- const {
212
- signature: _sig,
213
- version,
214
- size
215
- } = json;
216
- log(`UpdateJSON: ${JSON.stringify(json, null, 2)}`);
217
- if (!needUpdate(version)) {
218
- return false;
219
- } else {
220
- signature = _sig;
221
- return { size, version };
222
- }
202
+ return _compare(currentVersion, version2);
223
203
  }
224
- async function downloadUpdate(src) {
225
- if (typeof src !== "object") {
226
- let _url = src ?? _release;
227
- if (!_url) {
228
- log("no releaseAsarURL, use repository");
204
+ updater.checkUpdate = async (url) => {
205
+ try {
206
+ url ??= _update;
207
+ if (!url) {
208
+ log("no updateJsonURL, fallback to use repository");
229
209
  if (!repository) {
230
- throw new Error("releaseAsarURL or repository are not set");
210
+ throw new Error("updateJsonURL or repository are not set");
231
211
  }
232
- _url = `${repository}/releases/download/latest/${productName}.asar.gz`;
212
+ url = `${repository.replace("github.com", "raw.githubusercontent.com")}/master/version.json`;
213
+ }
214
+ if (existsSync(tmpFile)) {
215
+ log(`remove tmp file: ${tmpFile}`);
216
+ await rm(tmpFile);
217
+ }
218
+ if (existsSync(gzipPath)) {
219
+ log(`remove .gz file: ${gzipPath}`);
220
+ await rm(gzipPath);
221
+ }
222
+ const json = await download(url, "json");
223
+ const {
224
+ signature: _sig,
225
+ version: _v,
226
+ size
227
+ } = json;
228
+ log(`update info: ${JSON.stringify(json, null, 2)}`);
229
+ if (!await needUpdate(_v)) {
230
+ log(`update unavailable: ${_v}`);
231
+ return void 0;
232
+ } else {
233
+ log(`update available: ${_v}`);
234
+ signature = _sig;
235
+ version = _v;
236
+ return { size, version };
233
237
  }
234
- log(`releaseAsarURL: ${_url}`);
235
- src = await download(_url, "buffer");
236
- }
237
- log("start verify");
238
- if (!verify(src, signature)) {
239
- throw new Error("file broken, invalid signature!");
240
- }
241
- log(`write file: ${gzipPath}`);
242
- await writeFile(gzipPath, src);
243
- log(`extract file: ${gzipPath}`);
244
- await extractFile(gzipPath);
245
- updater.emit("downloaded");
246
- }
247
- const onCheck = async (url) => {
248
- try {
249
- const result = await checkUpdate(url);
250
- updater.emit("checkResult", result);
251
238
  } catch (error) {
252
239
  log(error);
253
- updater.emit("checkResult", error);
240
+ return error;
254
241
  }
255
242
  };
256
- updater.on("check", onCheck);
257
- updater.checkUpdate = onCheck;
258
- const onDownload = async (src) => {
243
+ updater.downloadUpdate = async (src) => {
259
244
  try {
260
- await downloadUpdate(src);
245
+ if (typeof src !== "object") {
246
+ let _url = src ?? _release;
247
+ if (!_url) {
248
+ log("no releaseAsarURL, fallback to use repository");
249
+ if (!repository) {
250
+ throw new Error("releaseAsarURL or repository are not set");
251
+ }
252
+ _url = `${repository}/releases/download/latest/${productName}.asar.gz`;
253
+ }
254
+ src = await download(_url, "buffer");
255
+ }
256
+ log("verify start");
257
+ if (!verify(src, signature, SIGNATURE_PUB, productName)) {
258
+ log("verify failed");
259
+ throw new Error("invalid signature");
260
+ }
261
+ log("verify success");
262
+ log(`write file: ${gzipPath}`);
263
+ await writeFile(gzipPath, src);
264
+ log(`extract file: ${gzipPath}`);
265
+ await extractFile(gzipPath);
266
+ log(`update success, version: ${version}`);
267
+ return true;
261
268
  } catch (error) {
262
269
  log(error);
263
- updater.emit("donwnloadError", error);
270
+ return error;
264
271
  }
265
272
  };
266
- updater.on("download", onDownload);
267
- updater.downloadUpdate = onDownload;
268
273
  return updater;
269
274
  }
270
275
 
271
276
  // src/index.ts
272
- function initApp(productName, updater, option) {
277
+ function initApp(appOptions, updaterOptions) {
273
278
  const {
279
+ name: productName,
274
280
  electronDistPath = "dist-electron",
275
281
  mainPath = "main/index.js"
276
- } = option ?? {};
282
+ } = appOptions ?? {};
277
283
  const mainDir = app3.isPackaged ? `../${productName}.asar` : electronDistPath;
278
284
  const entry = resolve(__dirname, mainDir, mainPath);
279
- let _updater;
280
- if ("SIGNATURE_PUB" in updater) {
281
- const _option = updater.productName ? updater : { ...updater, productName };
282
- _updater = createUpdater(_option);
285
+ if (updaterOptions) {
286
+ __require(entry)(
287
+ createUpdater({ ...updaterOptions, productName })
288
+ );
283
289
  } else {
284
- _updater = updater;
290
+ return {
291
+ setUpdater(updater) {
292
+ __require(entry)(updater);
293
+ }
294
+ };
285
295
  }
286
- return __require(entry)(_updater);
287
296
  }
288
297
  export {
289
298
  createUpdater,