electron-incremental-update 0.3.0 → 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 +46 -116
- package/dist/index.cjs +54 -33
- package/dist/index.d.ts +64 -55
- package/dist/index.mjs +54 -33
- package/package.json +2 -2
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
|
|
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,
|
|
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
|
-
|
|
86
|
-
updater.on('checkResult', async (result
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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('
|
|
109
|
-
updater.on('downloading',
|
|
110
|
-
|
|
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({
|
|
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
|
@@ -63,11 +63,15 @@ function downloadJSONDefault(url, updater, headers) {
|
|
|
63
63
|
res.headers = headers;
|
|
64
64
|
res.on("data", (chunk) => data += chunk);
|
|
65
65
|
res.on("end", () => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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) => {
|
|
@@ -177,6 +181,7 @@ function createUpdater({
|
|
|
177
181
|
}) {
|
|
178
182
|
const updater = new import_node_events.EventEmitter();
|
|
179
183
|
let signature = "";
|
|
184
|
+
let version = "";
|
|
180
185
|
const gzipPath = `../${productName}.asar.gz`;
|
|
181
186
|
const tmpFile = gzipPath.replace(".asar.gz", ".tmp.gz");
|
|
182
187
|
const { downloadBuffer, downloadJSON, extraHeader, userAgent } = downloadConfig || {};
|
|
@@ -190,12 +195,16 @@ function createUpdater({
|
|
|
190
195
|
UserAgent: ua,
|
|
191
196
|
...extraHeader
|
|
192
197
|
};
|
|
193
|
-
log(`headers: ${headers}`);
|
|
198
|
+
log(`download headers: ${JSON.stringify(headers, null, 2)}`);
|
|
194
199
|
const downloadFn = format === "json" ? downloadJSON ?? downloadJSONDefault : downloadBuffer ?? downloadBufferDefault;
|
|
195
|
-
|
|
200
|
+
log(`download ${format} from ${url}`);
|
|
201
|
+
const ret = await downloadFn(url, updater, headers);
|
|
202
|
+
log(`download ${format} success`);
|
|
203
|
+
return ret;
|
|
196
204
|
}
|
|
197
205
|
async function extractFile(gzipFilePath) {
|
|
198
206
|
if (!gzipFilePath.endsWith(".asar.gz") || !(0, import_node_fs2.existsSync)(gzipFilePath)) {
|
|
207
|
+
log("update .asar.gz file not exist");
|
|
199
208
|
return;
|
|
200
209
|
}
|
|
201
210
|
gzipFilePath = gzipFilePath.replace(".asar.gz", ".tmp.gz");
|
|
@@ -207,55 +216,61 @@ function createUpdater({
|
|
|
207
216
|
log(`outputFilePath: ${outputFilePath}`);
|
|
208
217
|
input.pipe(gunzip).pipe(output).on("finish", async () => {
|
|
209
218
|
await (0, import_promises.rm)(gzipFilePath);
|
|
210
|
-
log(
|
|
219
|
+
log(`${gzipFilePath} unzipped`);
|
|
211
220
|
resolve2(outputFilePath);
|
|
212
221
|
}).on("error", async (err) => {
|
|
213
222
|
await (0, import_promises.rm)(gzipFilePath);
|
|
214
|
-
log(`error: ${err}`);
|
|
215
223
|
output.destroy(err);
|
|
216
224
|
reject(err);
|
|
217
225
|
});
|
|
218
226
|
});
|
|
219
227
|
}
|
|
220
228
|
function verify(buffer, signature2) {
|
|
221
|
-
log(`signature: ${signature2}`);
|
|
222
229
|
return (0, import_node_crypto.createVerify)("RSA-SHA256").update(buffer).verify(SIGNATURE_PUB, signature2, "base64");
|
|
223
230
|
}
|
|
224
|
-
function needUpdate(
|
|
225
|
-
if (!
|
|
231
|
+
function needUpdate(version2) {
|
|
232
|
+
if (!import_electron2.app.isPackaged) {
|
|
233
|
+
log("in dev mode, no need to update");
|
|
226
234
|
return false;
|
|
227
235
|
}
|
|
228
236
|
const currentVersion = getEntryVersion();
|
|
229
|
-
log(`
|
|
230
|
-
|
|
237
|
+
log(`check update:
|
|
238
|
+
current version is ${currentVersion},
|
|
239
|
+
new version is ${version2}`);
|
|
231
240
|
const _compare = compareVersion ?? compareVersionDefault;
|
|
232
|
-
return _compare(currentVersion,
|
|
241
|
+
return _compare(currentVersion, version2);
|
|
233
242
|
}
|
|
234
243
|
async function checkUpdate(url) {
|
|
235
244
|
url ??= _update;
|
|
236
245
|
if (!url) {
|
|
237
|
-
log("no updateJsonURL, use repository");
|
|
246
|
+
log("no updateJsonURL, fallback to use repository");
|
|
238
247
|
if (!repository) {
|
|
239
248
|
throw new Error("updateJsonURL or repository are not set");
|
|
240
249
|
}
|
|
241
|
-
url = `${repository.replace("github.com", "raw.githubusercontent.com")}/version.json`;
|
|
250
|
+
url = `${repository.replace("github.com", "raw.githubusercontent.com")}/master/version.json`;
|
|
242
251
|
}
|
|
243
|
-
log(`updateJsonURL: ${url}`);
|
|
244
252
|
if ((0, import_node_fs2.existsSync)(tmpFile)) {
|
|
245
253
|
log(`remove tmp file: ${tmpFile}`);
|
|
246
254
|
await (0, import_promises.rm)(tmpFile);
|
|
247
255
|
}
|
|
256
|
+
if ((0, import_node_fs2.existsSync)(gzipPath)) {
|
|
257
|
+
log(`remove .gz file: ${gzipPath}`);
|
|
258
|
+
await (0, import_promises.rm)(gzipPath);
|
|
259
|
+
}
|
|
248
260
|
const json = await download(url, "json");
|
|
249
261
|
const {
|
|
250
262
|
signature: _sig,
|
|
251
|
-
version,
|
|
263
|
+
version: _v,
|
|
252
264
|
size
|
|
253
265
|
} = json;
|
|
254
|
-
log(`
|
|
255
|
-
if (!needUpdate(
|
|
266
|
+
log(`update info: ${JSON.stringify(json, null, 2)}`);
|
|
267
|
+
if (!await needUpdate(_v)) {
|
|
268
|
+
log(`update unavailable: ${_v}`);
|
|
256
269
|
return false;
|
|
257
270
|
} else {
|
|
271
|
+
log(`update available: ${_v}`);
|
|
258
272
|
signature = _sig;
|
|
273
|
+
version = _v;
|
|
259
274
|
return { size, version };
|
|
260
275
|
}
|
|
261
276
|
}
|
|
@@ -263,23 +278,25 @@ function createUpdater({
|
|
|
263
278
|
if (typeof src !== "object") {
|
|
264
279
|
let _url = src ?? _release;
|
|
265
280
|
if (!_url) {
|
|
266
|
-
log("no releaseAsarURL, use repository");
|
|
281
|
+
log("no releaseAsarURL, fallback to use repository");
|
|
267
282
|
if (!repository) {
|
|
268
283
|
throw new Error("releaseAsarURL or repository are not set");
|
|
269
284
|
}
|
|
270
285
|
_url = `${repository}/releases/download/latest/${productName}.asar.gz`;
|
|
271
286
|
}
|
|
272
|
-
log(`releaseAsarURL: ${_url}`);
|
|
273
287
|
src = await download(_url, "buffer");
|
|
274
288
|
}
|
|
275
|
-
log("start
|
|
289
|
+
log("verify start");
|
|
276
290
|
if (!verify(src, signature)) {
|
|
277
|
-
|
|
291
|
+
log("verify failed");
|
|
292
|
+
throw new Error("invalid signature");
|
|
278
293
|
}
|
|
294
|
+
log("verify success");
|
|
279
295
|
log(`write file: ${gzipPath}`);
|
|
280
296
|
await (0, import_promises.writeFile)(gzipPath, src);
|
|
281
297
|
log(`extract file: ${gzipPath}`);
|
|
282
298
|
await extractFile(gzipPath);
|
|
299
|
+
log(`update success, version: ${version}`);
|
|
283
300
|
updater.emit("downloaded");
|
|
284
301
|
}
|
|
285
302
|
const onCheck = async (url) => {
|
|
@@ -307,21 +324,25 @@ function createUpdater({
|
|
|
307
324
|
}
|
|
308
325
|
|
|
309
326
|
// src/index.ts
|
|
310
|
-
function initApp(
|
|
327
|
+
function initApp(appOptions, updaterOptions) {
|
|
311
328
|
const {
|
|
329
|
+
name: productName,
|
|
312
330
|
electronDistPath = "dist-electron",
|
|
313
331
|
mainPath = "main/index.js"
|
|
314
|
-
} =
|
|
332
|
+
} = appOptions ?? {};
|
|
315
333
|
const mainDir = import_electron3.app.isPackaged ? `../${productName}.asar` : electronDistPath;
|
|
316
334
|
const entry = (0, import_node_path2.resolve)(__dirname, mainDir, mainPath);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
335
|
+
if (updaterOptions) {
|
|
336
|
+
require(entry)(
|
|
337
|
+
createUpdater({ ...updaterOptions, productName })
|
|
338
|
+
);
|
|
321
339
|
} else {
|
|
322
|
-
|
|
340
|
+
return {
|
|
341
|
+
setUpdater(updater) {
|
|
342
|
+
require(entry)(updater);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
323
345
|
}
|
|
324
|
-
return require(entry)(_updater);
|
|
325
346
|
}
|
|
326
347
|
// Annotate the CommonJS export names for ESM import in node:
|
|
327
348
|
0 && (module.exports = {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { Buffer } from 'node:buffer';
|
|
|
2
2
|
|
|
3
3
|
type CheckResultType = Error | false | Omit<UpdateJSON, 'signature'>;
|
|
4
4
|
type UpdateEvents = {
|
|
5
|
-
check:
|
|
5
|
+
check: [url?: string];
|
|
6
6
|
checkResult: [data: CheckResultType];
|
|
7
|
-
download:
|
|
7
|
+
download: [src?: string | Buffer];
|
|
8
8
|
downloading: [current: number];
|
|
9
9
|
downloaded: null;
|
|
10
10
|
donwnloadError: [error: unknown];
|
|
@@ -16,20 +16,6 @@ type UpdateJSON = {
|
|
|
16
16
|
size: number;
|
|
17
17
|
};
|
|
18
18
|
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
19
|
interface TypedUpdater<T extends Record<string | symbol, MaybeArray<any>>, Event extends Exclude<keyof T, number> = Exclude<keyof T, number>> {
|
|
34
20
|
removeAllListeners<E extends Event>(event?: E): this;
|
|
35
21
|
listeners<E extends Event>(eventName: E): Function[];
|
|
@@ -43,11 +29,11 @@ interface TypedUpdater<T extends Record<string | symbol, MaybeArray<any>>, Event
|
|
|
43
29
|
* - `false`: unavailable
|
|
44
30
|
* - `{size: number, version: string}`: success
|
|
45
31
|
*/
|
|
46
|
-
checkUpdate(url?:
|
|
47
|
-
downloadUpdate(url?:
|
|
32
|
+
checkUpdate(url?: string): Promise<void>;
|
|
33
|
+
downloadUpdate(url?: string | Buffer): Promise<void>;
|
|
48
34
|
}
|
|
49
35
|
type Updater = TypedUpdater<UpdateEvents>;
|
|
50
|
-
interface UpdaterOption
|
|
36
|
+
interface UpdaterOption {
|
|
51
37
|
/**
|
|
52
38
|
* public key of signature
|
|
53
39
|
*
|
|
@@ -65,7 +51,7 @@ interface UpdaterOption extends BaseOption {
|
|
|
65
51
|
*/
|
|
66
52
|
SIGNATURE_PUB: string;
|
|
67
53
|
/**
|
|
68
|
-
*
|
|
54
|
+
* name of your application
|
|
69
55
|
*
|
|
70
56
|
* you can use the `name` in `package.json`
|
|
71
57
|
*/
|
|
@@ -79,8 +65,29 @@ interface UpdaterOption extends BaseOption {
|
|
|
79
65
|
* `repository` will be used to determine the url
|
|
80
66
|
*/
|
|
81
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
|
+
*/
|
|
82
83
|
debug?: boolean;
|
|
83
|
-
|
|
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>;
|
|
84
91
|
downloadConfig?: {
|
|
85
92
|
/**
|
|
86
93
|
* download user agent
|
|
@@ -94,7 +101,7 @@ interface UpdaterOption extends BaseOption {
|
|
|
94
101
|
/**
|
|
95
102
|
* download JSON function
|
|
96
103
|
* @param url download url
|
|
97
|
-
* @param updater updater,
|
|
104
|
+
* @param updater updater, to trigger events
|
|
98
105
|
* @param header download header
|
|
99
106
|
* @returns `UpdateJSON`
|
|
100
107
|
*/
|
|
@@ -102,7 +109,7 @@ interface UpdaterOption extends BaseOption {
|
|
|
102
109
|
/**
|
|
103
110
|
* download buffer function
|
|
104
111
|
* @param url download url
|
|
105
|
-
* @param updater updater,
|
|
112
|
+
* @param updater updater, to trigger events
|
|
106
113
|
* @param header download header
|
|
107
114
|
* @returns `Buffer`
|
|
108
115
|
*/
|
|
@@ -144,61 +151,63 @@ declare function restartApp(): void;
|
|
|
144
151
|
|
|
145
152
|
declare function createUpdater({ SIGNATURE_PUB, repository, productName, releaseAsarURL: _release, updateJsonURL: _update, debug, downloadConfig, compareVersion, }: UpdaterOption): Updater;
|
|
146
153
|
|
|
147
|
-
|
|
154
|
+
type AppOption = {
|
|
155
|
+
/**
|
|
156
|
+
* name of your application
|
|
157
|
+
*
|
|
158
|
+
* you can use the `name` in `package.json`
|
|
159
|
+
*/
|
|
160
|
+
name: string;
|
|
148
161
|
/**
|
|
149
162
|
* path of electron output dist
|
|
150
163
|
* @default 'dist-electron'
|
|
151
|
-
|
|
164
|
+
*/
|
|
152
165
|
electronDistPath?: string;
|
|
153
166
|
/**
|
|
154
167
|
* relative path of main entry in electron dist
|
|
155
168
|
* @default 'main/index.js'
|
|
156
|
-
|
|
169
|
+
*/
|
|
157
170
|
mainPath?: string;
|
|
158
|
-
}
|
|
171
|
+
};
|
|
172
|
+
type OptionalProperty<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
173
|
+
type InitUpdaterOptions = OptionalProperty<UpdaterOption, 'productName'>;
|
|
159
174
|
/**
|
|
160
|
-
*
|
|
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
|
-
*
|
|
175
|
+
* create updater manually
|
|
166
176
|
* @example
|
|
167
|
-
* **manual** generate updater
|
|
168
177
|
* ```ts
|
|
169
|
-
* import { initApp } from 'electron-incremental-
|
|
178
|
+
* import { createUpdater, getGithubReleaseCdnGroup, initApp, parseGithubCdnURL } from 'electron-incremental-update'
|
|
170
179
|
* import { name, repository } from '../package.json'
|
|
171
180
|
*
|
|
172
|
-
* const SIGNATURE_PUB = '' // auto generate
|
|
181
|
+
* const SIGNATURE_PUB = '' // auto generate
|
|
182
|
+
*
|
|
183
|
+
* const { cdnPrefix } = getGithubReleaseCdnGroup()[0]
|
|
173
184
|
* const updater = createUpdater({
|
|
174
185
|
* SIGNATURE_PUB,
|
|
175
186
|
* productName: name,
|
|
176
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,
|
|
177
191
|
* })
|
|
178
|
-
* initApp(name
|
|
192
|
+
* initApp({ name }).setUpdater(updater)
|
|
179
193
|
* ```
|
|
180
|
-
|
|
181
|
-
|
|
194
|
+
*/
|
|
195
|
+
declare function initApp(appOptions: AppOption): {
|
|
196
|
+
setUpdater: (updater: Updater) => void;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* create updater when init, no need to set productName
|
|
182
200
|
*
|
|
201
|
+
* @example
|
|
183
202
|
* ```ts
|
|
184
|
-
* import {
|
|
203
|
+
* import { initApp } from 'electron-incremental-update'
|
|
185
204
|
* import { name, repository } from '../package.json'
|
|
186
205
|
*
|
|
187
|
-
* const SIGNATURE_PUB = '' // auto generate
|
|
206
|
+
* const SIGNATURE_PUB = '' // auto generate
|
|
188
207
|
*
|
|
189
|
-
*
|
|
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
|
-
* })
|
|
208
|
+
* initApp({ name }, { SIGNATURE_PUB, repository })
|
|
198
209
|
* ```
|
|
199
|
-
*/
|
|
200
|
-
declare function initApp(
|
|
201
|
-
productName?: string;
|
|
202
|
-
}, option?: AppOption): any;
|
|
210
|
+
*/
|
|
211
|
+
declare function initApp(appOptions: AppOption, updaterOptions: InitUpdaterOptions): undefined;
|
|
203
212
|
|
|
204
|
-
export {
|
|
213
|
+
export { AppOption, CheckResultType, InitUpdaterOptions, UpdateJSON, Updater, UpdaterOption, createUpdater, getAppAsarPath, getAppVersion, getEntryVersion, getGithubReleaseCdnGroup, initApp, parseGithubCdnURL, requireNative, restartApp };
|
package/dist/index.mjs
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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) => {
|
|
@@ -139,6 +143,7 @@ function createUpdater({
|
|
|
139
143
|
}) {
|
|
140
144
|
const updater = new EventEmitter();
|
|
141
145
|
let signature = "";
|
|
146
|
+
let version = "";
|
|
142
147
|
const gzipPath = `../${productName}.asar.gz`;
|
|
143
148
|
const tmpFile = gzipPath.replace(".asar.gz", ".tmp.gz");
|
|
144
149
|
const { downloadBuffer, downloadJSON, extraHeader, userAgent } = downloadConfig || {};
|
|
@@ -152,12 +157,16 @@ function createUpdater({
|
|
|
152
157
|
UserAgent: ua,
|
|
153
158
|
...extraHeader
|
|
154
159
|
};
|
|
155
|
-
log(`headers: ${headers}`);
|
|
160
|
+
log(`download headers: ${JSON.stringify(headers, null, 2)}`);
|
|
156
161
|
const downloadFn = format === "json" ? downloadJSON ?? downloadJSONDefault : downloadBuffer ?? downloadBufferDefault;
|
|
157
|
-
|
|
162
|
+
log(`download ${format} from ${url}`);
|
|
163
|
+
const ret = await downloadFn(url, updater, headers);
|
|
164
|
+
log(`download ${format} success`);
|
|
165
|
+
return ret;
|
|
158
166
|
}
|
|
159
167
|
async function extractFile(gzipFilePath) {
|
|
160
168
|
if (!gzipFilePath.endsWith(".asar.gz") || !existsSync(gzipFilePath)) {
|
|
169
|
+
log("update .asar.gz file not exist");
|
|
161
170
|
return;
|
|
162
171
|
}
|
|
163
172
|
gzipFilePath = gzipFilePath.replace(".asar.gz", ".tmp.gz");
|
|
@@ -169,55 +178,61 @@ function createUpdater({
|
|
|
169
178
|
log(`outputFilePath: ${outputFilePath}`);
|
|
170
179
|
input.pipe(gunzip).pipe(output).on("finish", async () => {
|
|
171
180
|
await rm(gzipFilePath);
|
|
172
|
-
log(
|
|
181
|
+
log(`${gzipFilePath} unzipped`);
|
|
173
182
|
resolve2(outputFilePath);
|
|
174
183
|
}).on("error", async (err) => {
|
|
175
184
|
await rm(gzipFilePath);
|
|
176
|
-
log(`error: ${err}`);
|
|
177
185
|
output.destroy(err);
|
|
178
186
|
reject(err);
|
|
179
187
|
});
|
|
180
188
|
});
|
|
181
189
|
}
|
|
182
190
|
function verify(buffer, signature2) {
|
|
183
|
-
log(`signature: ${signature2}`);
|
|
184
191
|
return createVerify("RSA-SHA256").update(buffer).verify(SIGNATURE_PUB, signature2, "base64");
|
|
185
192
|
}
|
|
186
|
-
function needUpdate(
|
|
187
|
-
if (!
|
|
193
|
+
function needUpdate(version2) {
|
|
194
|
+
if (!app2.isPackaged) {
|
|
195
|
+
log("in dev mode, no need to update");
|
|
188
196
|
return false;
|
|
189
197
|
}
|
|
190
198
|
const currentVersion = getEntryVersion();
|
|
191
|
-
log(`
|
|
192
|
-
|
|
199
|
+
log(`check update:
|
|
200
|
+
current version is ${currentVersion},
|
|
201
|
+
new version is ${version2}`);
|
|
193
202
|
const _compare = compareVersion ?? compareVersionDefault;
|
|
194
|
-
return _compare(currentVersion,
|
|
203
|
+
return _compare(currentVersion, version2);
|
|
195
204
|
}
|
|
196
205
|
async function checkUpdate(url) {
|
|
197
206
|
url ??= _update;
|
|
198
207
|
if (!url) {
|
|
199
|
-
log("no updateJsonURL, use repository");
|
|
208
|
+
log("no updateJsonURL, fallback to use repository");
|
|
200
209
|
if (!repository) {
|
|
201
210
|
throw new Error("updateJsonURL or repository are not set");
|
|
202
211
|
}
|
|
203
|
-
url = `${repository.replace("github.com", "raw.githubusercontent.com")}/version.json`;
|
|
212
|
+
url = `${repository.replace("github.com", "raw.githubusercontent.com")}/master/version.json`;
|
|
204
213
|
}
|
|
205
|
-
log(`updateJsonURL: ${url}`);
|
|
206
214
|
if (existsSync(tmpFile)) {
|
|
207
215
|
log(`remove tmp file: ${tmpFile}`);
|
|
208
216
|
await rm(tmpFile);
|
|
209
217
|
}
|
|
218
|
+
if (existsSync(gzipPath)) {
|
|
219
|
+
log(`remove .gz file: ${gzipPath}`);
|
|
220
|
+
await rm(gzipPath);
|
|
221
|
+
}
|
|
210
222
|
const json = await download(url, "json");
|
|
211
223
|
const {
|
|
212
224
|
signature: _sig,
|
|
213
|
-
version,
|
|
225
|
+
version: _v,
|
|
214
226
|
size
|
|
215
227
|
} = json;
|
|
216
|
-
log(`
|
|
217
|
-
if (!needUpdate(
|
|
228
|
+
log(`update info: ${JSON.stringify(json, null, 2)}`);
|
|
229
|
+
if (!await needUpdate(_v)) {
|
|
230
|
+
log(`update unavailable: ${_v}`);
|
|
218
231
|
return false;
|
|
219
232
|
} else {
|
|
233
|
+
log(`update available: ${_v}`);
|
|
220
234
|
signature = _sig;
|
|
235
|
+
version = _v;
|
|
221
236
|
return { size, version };
|
|
222
237
|
}
|
|
223
238
|
}
|
|
@@ -225,23 +240,25 @@ function createUpdater({
|
|
|
225
240
|
if (typeof src !== "object") {
|
|
226
241
|
let _url = src ?? _release;
|
|
227
242
|
if (!_url) {
|
|
228
|
-
log("no releaseAsarURL, use repository");
|
|
243
|
+
log("no releaseAsarURL, fallback to use repository");
|
|
229
244
|
if (!repository) {
|
|
230
245
|
throw new Error("releaseAsarURL or repository are not set");
|
|
231
246
|
}
|
|
232
247
|
_url = `${repository}/releases/download/latest/${productName}.asar.gz`;
|
|
233
248
|
}
|
|
234
|
-
log(`releaseAsarURL: ${_url}`);
|
|
235
249
|
src = await download(_url, "buffer");
|
|
236
250
|
}
|
|
237
|
-
log("start
|
|
251
|
+
log("verify start");
|
|
238
252
|
if (!verify(src, signature)) {
|
|
239
|
-
|
|
253
|
+
log("verify failed");
|
|
254
|
+
throw new Error("invalid signature");
|
|
240
255
|
}
|
|
256
|
+
log("verify success");
|
|
241
257
|
log(`write file: ${gzipPath}`);
|
|
242
258
|
await writeFile(gzipPath, src);
|
|
243
259
|
log(`extract file: ${gzipPath}`);
|
|
244
260
|
await extractFile(gzipPath);
|
|
261
|
+
log(`update success, version: ${version}`);
|
|
245
262
|
updater.emit("downloaded");
|
|
246
263
|
}
|
|
247
264
|
const onCheck = async (url) => {
|
|
@@ -269,21 +286,25 @@ function createUpdater({
|
|
|
269
286
|
}
|
|
270
287
|
|
|
271
288
|
// src/index.ts
|
|
272
|
-
function initApp(
|
|
289
|
+
function initApp(appOptions, updaterOptions) {
|
|
273
290
|
const {
|
|
291
|
+
name: productName,
|
|
274
292
|
electronDistPath = "dist-electron",
|
|
275
293
|
mainPath = "main/index.js"
|
|
276
|
-
} =
|
|
294
|
+
} = appOptions ?? {};
|
|
277
295
|
const mainDir = app3.isPackaged ? `../${productName}.asar` : electronDistPath;
|
|
278
296
|
const entry = resolve(__dirname, mainDir, mainPath);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
297
|
+
if (updaterOptions) {
|
|
298
|
+
__require(entry)(
|
|
299
|
+
createUpdater({ ...updaterOptions, productName })
|
|
300
|
+
);
|
|
283
301
|
} else {
|
|
284
|
-
|
|
302
|
+
return {
|
|
303
|
+
setUpdater(updater) {
|
|
304
|
+
__require(entry)(updater);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
285
307
|
}
|
|
286
|
-
return __require(entry)(_updater);
|
|
287
308
|
}
|
|
288
309
|
export {
|
|
289
310
|
createUpdater,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "electron-incremental-update",
|
|
3
|
-
"version": "0.
|
|
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
|
+
}
|