@nwutils/packager 0.6.0 → 0.7.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 +32 -0
- package/package.json +1 -1
- package/src/appImage.js +10 -87
- package/src/nsis.js +105 -23
- package/src/util.js +121 -6
- package/types/nsis.d.ts +12 -2
- package/types/util.d.ts +40 -5
package/README.md
CHANGED
|
@@ -143,6 +143,37 @@ It needs the NSIS compiler, `makensis`, installed on the host. It runs on
|
|
|
143
143
|
Linux (`apt install nsis`) and MacOS (`brew install makensis`) too, so a Windows
|
|
144
144
|
installer can be built from any platform. On Windows, `choco install nsis`.
|
|
145
145
|
|
|
146
|
+
### Self updating NSIS installs
|
|
147
|
+
|
|
148
|
+
Set `publish`, the same way as for [AppImages](#self-updating-appimages):
|
|
149
|
+
|
|
150
|
+
- an `app-update.yml` describing `publish` is installed next to
|
|
151
|
+
`<appName>.exe`, and removed by the uninstaller
|
|
152
|
+
- a `latest.yml` update info file (`latest-<arch>.yml` for architectures other
|
|
153
|
+
than `x64`) with the version, file name, size and SHA-512 of the installer is
|
|
154
|
+
written next to it in `outDir`
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
await packager({
|
|
158
|
+
format: "nsis",
|
|
159
|
+
appDir: "./out",
|
|
160
|
+
appName: "Demo",
|
|
161
|
+
version: "1.2.0",
|
|
162
|
+
outDir: "./dist",
|
|
163
|
+
publish: { provider: "github", owner: "nwutils", repo: "demo" },
|
|
164
|
+
});
|
|
165
|
+
// ./dist/Demo-x64-Setup.exe
|
|
166
|
+
// ./dist/latest.yml
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Upload both the installer and `latest.yml` to every release.
|
|
170
|
+
[`@nwutils/updater`](https://www.npmjs.com/package/@nwutils/updater)'s
|
|
171
|
+
`NsisUpdater` downloads the new installer, checks it, and runs it with
|
|
172
|
+
`/S /UPDATE`. With `/UPDATE`, the installer waits up to 60 seconds for the
|
|
173
|
+
running application to exit instead of asking the user to close it, updates it
|
|
174
|
+
silently, and starts the new version. Because it installs for the current user,
|
|
175
|
+
updating never asks for administrator rights.
|
|
176
|
+
|
|
146
177
|
## API Reference
|
|
147
178
|
|
|
148
179
|
### `packager({ format, ...options })`
|
|
@@ -195,6 +226,7 @@ Options
|
|
|
195
226
|
| desktopShortcut | `boolean` | If true, create a desktop shortcut. Defaults to `true` |
|
|
196
227
|
| startMenuShortcut | `boolean` | If true, create a Start Menu shortcut. Defaults to `true` |
|
|
197
228
|
| makensisPath | `string` | Path to the NSIS compiler. Defaults to `makensis` on the `PATH` |
|
|
229
|
+
| publish | `PublishOptions` | Where releases are published. Installs `app-update.yml` and writes `latest[-<arch>].yml` to `outDir` |
|
|
198
230
|
|
|
199
231
|
Resolves with the path to the resulting installer `.exe` file.
|
|
200
232
|
|
package/package.json
CHANGED
package/src/appImage.js
CHANGED
|
@@ -141,7 +141,7 @@ async function appImage({
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
if (publish !== undefined) {
|
|
144
|
-
validatePublish(publish);
|
|
144
|
+
util.validatePublish(publish);
|
|
145
145
|
if (typeof version !== "string" || version === "") {
|
|
146
146
|
throw new Error(
|
|
147
147
|
`Expected "options.version" to be a non-empty string when "options.publish" is set. Received: ${JSON.stringify(version)}`,
|
|
@@ -280,15 +280,7 @@ async function appImage({
|
|
|
280
280
|
if (publish !== undefined) {
|
|
281
281
|
await fs.promises.writeFile(
|
|
282
282
|
path.resolve(appImageDir, "app-update.yml"),
|
|
283
|
-
util.
|
|
284
|
-
publish.provider === "github"
|
|
285
|
-
? {
|
|
286
|
-
provider: publish.provider,
|
|
287
|
-
owner: publish.owner,
|
|
288
|
-
repo: publish.repo,
|
|
289
|
-
}
|
|
290
|
-
: { provider: publish.provider, url: publish.url },
|
|
291
|
-
),
|
|
283
|
+
util.appUpdateYaml(publish),
|
|
292
284
|
);
|
|
293
285
|
}
|
|
294
286
|
|
|
@@ -367,18 +359,14 @@ async function appImage({
|
|
|
367
359
|
}
|
|
368
360
|
|
|
369
361
|
if (publish !== undefined) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
sha512: appImageSha512,
|
|
379
|
-
releaseDate: new Date().toISOString(),
|
|
380
|
-
}),
|
|
381
|
-
);
|
|
362
|
+
await util.writeUpdateInfo({
|
|
363
|
+
filePath: appImageFilePath,
|
|
364
|
+
updateInfoPath: path.resolve(
|
|
365
|
+
resolvedOutDir,
|
|
366
|
+
util.updateInfoFileName(resolvedArch, "linux"),
|
|
367
|
+
),
|
|
368
|
+
version: /** @type {string} */ (version),
|
|
369
|
+
});
|
|
382
370
|
}
|
|
383
371
|
|
|
384
372
|
return appImageFilePath;
|
|
@@ -387,71 +375,6 @@ async function appImage({
|
|
|
387
375
|
}
|
|
388
376
|
}
|
|
389
377
|
|
|
390
|
-
/**
|
|
391
|
-
* Throw if `publish` isn't a valid `PublishOptions` object.
|
|
392
|
-
* @param {unknown} publish
|
|
393
|
-
* @returns {asserts publish is PublishOptions}
|
|
394
|
-
*/
|
|
395
|
-
function validatePublish(publish) {
|
|
396
|
-
if (typeof publish !== "object" || publish === null) {
|
|
397
|
-
throw new Error(
|
|
398
|
-
`Expected "options.publish" to be an object. Received: ${JSON.stringify(publish)}`,
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const { provider } = /** @type {{provider?: unknown}} */ (publish);
|
|
403
|
-
/** @type {string[]} */
|
|
404
|
-
let requiredKeys;
|
|
405
|
-
if (provider === "github") {
|
|
406
|
-
requiredKeys = ["owner", "repo"];
|
|
407
|
-
} else if (provider === "generic") {
|
|
408
|
-
requiredKeys = ["url"];
|
|
409
|
-
} else {
|
|
410
|
-
throw new Error(
|
|
411
|
-
`Expected "options.publish.provider" to be "github" or "generic". Received: ${JSON.stringify(provider)}`,
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
for (const key of requiredKeys) {
|
|
416
|
-
const value = /** @type {Record<string, unknown>} */ (publish)[key];
|
|
417
|
-
if (typeof value !== "string" || value === "") {
|
|
418
|
-
throw new Error(
|
|
419
|
-
`Expected "options.publish.${key}" to be a non-empty string for provider "${provider}". Received: ${JSON.stringify(value)}`,
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (provider === "github") {
|
|
425
|
-
/*
|
|
426
|
-
* The characters GitHub allows in owner and repository names. Anything
|
|
427
|
-
* else - eg. "|", the update information's field separator - is rejected.
|
|
428
|
-
*/
|
|
429
|
-
for (const key of requiredKeys) {
|
|
430
|
-
const value = /** @type {Record<string, string>} */ (publish)[key];
|
|
431
|
-
if (/^[A-Za-z0-9._-]+$/.test(value) === false) {
|
|
432
|
-
throw new Error(
|
|
433
|
-
`Expected "options.publish.${key}" to be a GitHub ${key} name (letters, digits, ".", "-" and "_"). Received: ${JSON.stringify(value)}`,
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (provider === "generic") {
|
|
440
|
-
const { url } = /** @type {GenericPublishOptions} */ (publish);
|
|
441
|
-
let protocol;
|
|
442
|
-
try {
|
|
443
|
-
protocol = new URL(url).protocol;
|
|
444
|
-
} catch {
|
|
445
|
-
protocol = undefined;
|
|
446
|
-
}
|
|
447
|
-
if (protocol !== "https:" && protocol !== "http:") {
|
|
448
|
-
throw new Error(
|
|
449
|
-
`Expected "options.publish.url" to be an http(s) URL. Received: ${JSON.stringify(url)}`,
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
378
|
/**
|
|
456
379
|
* Build the AppImage update information string for `publish`, in the format
|
|
457
380
|
* zsync based AppImage updaters understand.
|
package/src/nsis.js
CHANGED
|
@@ -21,6 +21,7 @@ import util from "./util.js";
|
|
|
21
21
|
* @property {boolean} [desktopShortcut=true] If true, create a desktop shortcut.
|
|
22
22
|
* @property {boolean} [startMenuShortcut=true] If true, create a Start Menu shortcut.
|
|
23
23
|
* @property {string} [makensisPath="makensis"] Path to the NSIS compiler. Defaults to `makensis` on the `PATH`.
|
|
24
|
+
* @property {import("./appImage.js").PublishOptions} [publish] Where releases are published. When set, an `app-update.yml` pointing at it is installed next to the executable and a `latest[-<arch>].yml` update info file is written next to the installer, for `@nwutils/updater` to self update from.
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -46,6 +47,12 @@ const ARCH_CHECK_KV = {
|
|
|
46
47
|
arm64: [["${IsNativeARM64}"]],
|
|
47
48
|
};
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* How long an update waits for the running application to exit.
|
|
52
|
+
* @type {number}
|
|
53
|
+
*/
|
|
54
|
+
const UPDATE_WAIT_SECONDS = 60;
|
|
55
|
+
|
|
49
56
|
/**
|
|
50
57
|
* Package a built NW.js Windows application as an NSIS installer.
|
|
51
58
|
*
|
|
@@ -55,6 +62,10 @@ const ARCH_CHECK_KV = {
|
|
|
55
62
|
* installing a new version first uninstalls the previous one. Both wait for
|
|
56
63
|
* the application to be closed before touching its files.
|
|
57
64
|
*
|
|
65
|
+
* Run with `/S /UPDATE`, as `@nwutils/updater` does, the installer instead
|
|
66
|
+
* waits for the running application to exit, updates it silently and starts
|
|
67
|
+
* the new version.
|
|
68
|
+
*
|
|
58
69
|
* This composes with `@nwutils/builder`: run it against the `outDir` (and
|
|
59
70
|
* `app.name`) that `@nwutils/builder` produced for `platform: "win"`.
|
|
60
71
|
* `makensis` runs on Linux and macOS too, so the installer can be built on
|
|
@@ -78,6 +89,7 @@ async function nsis({
|
|
|
78
89
|
desktopShortcut = true,
|
|
79
90
|
startMenuShortcut = true,
|
|
80
91
|
makensisPath = "makensis",
|
|
92
|
+
publish,
|
|
81
93
|
}) {
|
|
82
94
|
if (typeof appDir !== "string" || appDir === "") {
|
|
83
95
|
throw new Error(
|
|
@@ -169,6 +181,10 @@ async function nsis({
|
|
|
169
181
|
);
|
|
170
182
|
}
|
|
171
183
|
|
|
184
|
+
if (publish !== undefined) {
|
|
185
|
+
util.validatePublish(publish);
|
|
186
|
+
}
|
|
187
|
+
|
|
172
188
|
const resolvedIconPath =
|
|
173
189
|
icon === undefined
|
|
174
190
|
? undefined
|
|
@@ -193,28 +209,45 @@ async function nsis({
|
|
|
193
209
|
`${appName}-${resolvedArch}-Setup.exe`,
|
|
194
210
|
);
|
|
195
211
|
|
|
196
|
-
const
|
|
197
|
-
appDir: resolvedAppDir,
|
|
198
|
-
appFiles: await listAppFiles(resolvedAppDir),
|
|
199
|
-
appName,
|
|
200
|
-
appId: resolvedAppId,
|
|
201
|
-
version,
|
|
202
|
-
fileVersion,
|
|
203
|
-
arch: resolvedArch,
|
|
204
|
-
installerPath,
|
|
205
|
-
iconPath: resolvedIconPath,
|
|
206
|
-
licensePath: resolvedLicensePath,
|
|
207
|
-
publisher,
|
|
208
|
-
copyright,
|
|
209
|
-
desktopShortcut,
|
|
210
|
-
startMenuShortcut,
|
|
211
|
-
});
|
|
212
|
+
const appFiles = await listAppFiles(resolvedAppDir);
|
|
212
213
|
|
|
213
214
|
const scriptDir = await fs.promises.mkdtemp(
|
|
214
215
|
path.join(os.tmpdir(), "nwutils-packager-nsis-"),
|
|
215
216
|
);
|
|
216
217
|
|
|
217
218
|
try {
|
|
219
|
+
/*
|
|
220
|
+
* Installed next to the NW.js executable, ie. at
|
|
221
|
+
* `path.dirname(process.execPath)` at runtime, which is where
|
|
222
|
+
* `@nwutils/updater` looks for it.
|
|
223
|
+
*/
|
|
224
|
+
let appUpdateYamlPath;
|
|
225
|
+
if (publish !== undefined) {
|
|
226
|
+
appUpdateYamlPath = path.resolve(scriptDir, "app-update.yml");
|
|
227
|
+
await fs.promises.writeFile(
|
|
228
|
+
appUpdateYamlPath,
|
|
229
|
+
util.appUpdateYaml(publish),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const script = nsisScript({
|
|
234
|
+
appDir: resolvedAppDir,
|
|
235
|
+
appFiles,
|
|
236
|
+
appName,
|
|
237
|
+
appId: resolvedAppId,
|
|
238
|
+
version,
|
|
239
|
+
fileVersion,
|
|
240
|
+
arch: resolvedArch,
|
|
241
|
+
installerPath,
|
|
242
|
+
iconPath: resolvedIconPath,
|
|
243
|
+
licensePath: resolvedLicensePath,
|
|
244
|
+
publisher,
|
|
245
|
+
copyright,
|
|
246
|
+
desktopShortcut,
|
|
247
|
+
startMenuShortcut,
|
|
248
|
+
appUpdateYamlPath,
|
|
249
|
+
});
|
|
250
|
+
|
|
218
251
|
const scriptPath = path.resolve(scriptDir, "installer.nsi");
|
|
219
252
|
await fs.promises.writeFile(scriptPath, script, "utf-8");
|
|
220
253
|
|
|
@@ -234,6 +267,17 @@ async function nsis({
|
|
|
234
267
|
throw error;
|
|
235
268
|
}
|
|
236
269
|
|
|
270
|
+
if (publish !== undefined) {
|
|
271
|
+
await util.writeUpdateInfo({
|
|
272
|
+
filePath: installerPath,
|
|
273
|
+
updateInfoPath: path.resolve(
|
|
274
|
+
resolvedOutDir,
|
|
275
|
+
util.updateInfoFileName(resolvedArch, "win"),
|
|
276
|
+
),
|
|
277
|
+
version,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
237
281
|
return installerPath;
|
|
238
282
|
} finally {
|
|
239
283
|
await fs.promises.rm(scriptDir, { recursive: true, force: true });
|
|
@@ -325,6 +369,7 @@ async function listAppFiles(appDir) {
|
|
|
325
369
|
* @param {string} options.copyright
|
|
326
370
|
* @param {boolean} options.desktopShortcut
|
|
327
371
|
* @param {boolean} options.startMenuShortcut
|
|
372
|
+
* @param {string} [options.appUpdateYamlPath] Absolute path to an `app-update.yml` file to install next to the executable.
|
|
328
373
|
* @returns {string}
|
|
329
374
|
*/
|
|
330
375
|
function nsisScript({
|
|
@@ -342,6 +387,7 @@ function nsisScript({
|
|
|
342
387
|
copyright,
|
|
343
388
|
desktopShortcut,
|
|
344
389
|
startMenuShortcut,
|
|
390
|
+
appUpdateYamlPath,
|
|
345
391
|
}) {
|
|
346
392
|
const e = util.nsisEscape;
|
|
347
393
|
const p = util.nsisPath;
|
|
@@ -363,6 +409,10 @@ function nsisScript({
|
|
|
363
409
|
'!include "LogicLib.nsh"',
|
|
364
410
|
'!include "x64.nsh"',
|
|
365
411
|
'!include "WinVer.nsh"',
|
|
412
|
+
'!include "FileFunc.nsh"',
|
|
413
|
+
"",
|
|
414
|
+
/* "1" when run with `/UPDATE`, ie. by `@nwutils/updater`. */
|
|
415
|
+
"Var IsUpdate",
|
|
366
416
|
"",
|
|
367
417
|
`Name "${e(appName)}"`,
|
|
368
418
|
`OutFile "${p(installerPath)}"`,
|
|
@@ -405,7 +455,17 @@ function nsisScript({
|
|
|
405
455
|
if (publisher !== undefined) {
|
|
406
456
|
lines.push(`VIAddVersionKey "CompanyName" "${e(publisher)}"`);
|
|
407
457
|
}
|
|
408
|
-
lines.push(
|
|
458
|
+
lines.push(
|
|
459
|
+
"",
|
|
460
|
+
"Function .onInit",
|
|
461
|
+
" SetShellVarContext current",
|
|
462
|
+
" ${GetParameters} $R0",
|
|
463
|
+
" ClearErrors",
|
|
464
|
+
' ${GetOptions} $R0 "/UPDATE" $R1',
|
|
465
|
+
" ${IfNot} ${Errors}",
|
|
466
|
+
' StrCpy $IsUpdate "1"',
|
|
467
|
+
" ${EndIf}",
|
|
468
|
+
);
|
|
409
469
|
if (archChecks.length > 0) {
|
|
410
470
|
/* Passing branches are left empty: only the final `${Else}` does anything. */
|
|
411
471
|
lines.push(
|
|
@@ -471,11 +531,11 @@ function nsisScript({
|
|
|
471
531
|
}
|
|
472
532
|
}
|
|
473
533
|
|
|
474
|
-
lines.push(
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
);
|
|
534
|
+
lines.push(' SetOutPath "$INSTDIR"');
|
|
535
|
+
if (appUpdateYamlPath !== undefined) {
|
|
536
|
+
lines.push(` File "/oname=app-update.yml" "${p(appUpdateYamlPath)}"`);
|
|
537
|
+
}
|
|
538
|
+
lines.push(' WriteUninstaller "$INSTDIR\\Uninstall.exe"', "");
|
|
479
539
|
if (startMenuShortcut) {
|
|
480
540
|
lines.push(` CreateShortcut "$SMPROGRAMS\\${e(appName)}.lnk" ${exe}`);
|
|
481
541
|
}
|
|
@@ -500,6 +560,11 @@ function nsisScript({
|
|
|
500
560
|
);
|
|
501
561
|
}
|
|
502
562
|
lines.push(
|
|
563
|
+
"",
|
|
564
|
+
/* An update runs silently, so the finish page's "Run" checkbox never shows. */
|
|
565
|
+
' ${If} $IsUpdate == "1"',
|
|
566
|
+
` Exec '${exe}'`,
|
|
567
|
+
" ${EndIf}",
|
|
503
568
|
"SectionEnd",
|
|
504
569
|
"",
|
|
505
570
|
"Function un.onInit",
|
|
@@ -516,6 +581,9 @@ function nsisScript({
|
|
|
516
581
|
for (const dir of [...appFiles.dirs].reverse()) {
|
|
517
582
|
lines.push(` RMDir ${installed(dir)}`);
|
|
518
583
|
}
|
|
584
|
+
if (appUpdateYamlPath !== undefined) {
|
|
585
|
+
lines.push(' Delete "$INSTDIR\\app-update.yml"');
|
|
586
|
+
}
|
|
519
587
|
lines.push(
|
|
520
588
|
' Delete "$INSTDIR\\Uninstall.exe"',
|
|
521
589
|
' RMDir "$INSTDIR"',
|
|
@@ -534,7 +602,12 @@ function nsisScript({
|
|
|
534
602
|
* the user to close it, so its files can be replaced or deleted. Windows
|
|
535
603
|
* refuses to open a running executable for writing, which is how a running
|
|
536
604
|
* copy is detected. Cancelling - which silent mode does straight away -
|
|
537
|
-
* aborts the installer or uninstaller.
|
|
605
|
+
* aborts the installer or uninstaller.
|
|
606
|
+
*
|
|
607
|
+
* During an update (`$IsUpdate` is "1") nobody is there to answer the prompt:
|
|
608
|
+
* `@nwutils/updater` starts the installer and then quits the application, so
|
|
609
|
+
* it waits for up to `UPDATE_WAIT_SECONDS` instead, then aborts. Uses `$2`
|
|
610
|
+
* and `$3`.
|
|
538
611
|
* @param {string} exePath Quoted NSIS string with the executable's path, eg. `"$INSTDIR\Demo.exe"`.
|
|
539
612
|
* @param {string} appName Application name, shown in the prompt.
|
|
540
613
|
* @param {string} indent Indentation of the generated lines.
|
|
@@ -542,6 +615,7 @@ function nsisScript({
|
|
|
542
615
|
*/
|
|
543
616
|
function waitForAppToClose(exePath, appName, indent) {
|
|
544
617
|
return [
|
|
618
|
+
"StrCpy $3 0",
|
|
545
619
|
"${Do}",
|
|
546
620
|
` \${IfNot} \${FileExists} ${exePath}`,
|
|
547
621
|
" ${Break}",
|
|
@@ -552,6 +626,14 @@ function waitForAppToClose(exePath, appName, indent) {
|
|
|
552
626
|
" FileClose $2",
|
|
553
627
|
" ${Break}",
|
|
554
628
|
" ${EndIf}",
|
|
629
|
+
' ${If} $IsUpdate == "1"',
|
|
630
|
+
" IntOp $3 $3 + 1",
|
|
631
|
+
` \${If} $3 > ${UPDATE_WAIT_SECONDS * 2}`,
|
|
632
|
+
" Abort",
|
|
633
|
+
" ${EndIf}",
|
|
634
|
+
" Sleep 500",
|
|
635
|
+
" ${Continue}",
|
|
636
|
+
" ${EndIf}",
|
|
555
637
|
` \${If} \${Cmd} \`MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "Close ${util.nsisEscape(appName)}, then select Retry." /SD IDCANCEL IDCANCEL\``,
|
|
556
638
|
" Abort",
|
|
557
639
|
" ${EndIf}",
|
package/src/util.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Map nw-builder's architecture identifiers to the identifiers used in
|
|
@@ -114,14 +115,17 @@ function desktopExecArg(value) {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/**
|
|
117
|
-
* Name of the update info file `@nwutils/updater` downloads
|
|
118
|
-
*
|
|
119
|
-
* `latest-linux.yml
|
|
120
|
-
*
|
|
118
|
+
* Name of the update info file `@nwutils/updater` downloads for `arch` on
|
|
119
|
+
* `platform`, matching electron-builder: `x64` gets the bare
|
|
120
|
+
* `latest-linux.yml` (Linux) or `latest.yml` (Windows), every other
|
|
121
|
+
* architecture gets an `-<arch>` suffix.
|
|
122
|
+
* @param {string} arch
|
|
123
|
+
* @param {"linux" | "win"} [platform="linux"]
|
|
121
124
|
* @returns {string}
|
|
122
125
|
*/
|
|
123
|
-
function updateInfoFileName(arch) {
|
|
124
|
-
|
|
126
|
+
function updateInfoFileName(arch, platform = "linux") {
|
|
127
|
+
const base = platform === "win" ? "latest" : "latest-linux";
|
|
128
|
+
return arch === "x64" ? `${base}.yml` : `${base}-${arch}.yml`;
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
/**
|
|
@@ -269,8 +273,117 @@ function windowsVersion(version) {
|
|
|
269
273
|
return parts.join(".");
|
|
270
274
|
}
|
|
271
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Throw if `publish` isn't a valid `PublishOptions` object.
|
|
278
|
+
* @param {unknown} publish
|
|
279
|
+
* @returns {void}
|
|
280
|
+
*/
|
|
281
|
+
function validatePublish(publish) {
|
|
282
|
+
if (typeof publish !== "object" || publish === null) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Expected "options.publish" to be an object. Received: ${JSON.stringify(publish)}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const { provider } = /** @type {{provider?: unknown}} */ (publish);
|
|
289
|
+
/** @type {string[]} */
|
|
290
|
+
let requiredKeys;
|
|
291
|
+
if (provider === "github") {
|
|
292
|
+
requiredKeys = ["owner", "repo"];
|
|
293
|
+
} else if (provider === "generic") {
|
|
294
|
+
requiredKeys = ["url"];
|
|
295
|
+
} else {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Expected "options.publish.provider" to be "github" or "generic". Received: ${JSON.stringify(provider)}`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
for (const key of requiredKeys) {
|
|
302
|
+
const value = /** @type {Record<string, unknown>} */ (publish)[key];
|
|
303
|
+
if (typeof value !== "string" || value === "") {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Expected "options.publish.${key}" to be a non-empty string for provider "${provider}". Received: ${JSON.stringify(value)}`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (provider === "github") {
|
|
311
|
+
/*
|
|
312
|
+
* The characters GitHub allows in owner and repository names. Anything
|
|
313
|
+
* else - eg. "|", the update information's field separator - is rejected.
|
|
314
|
+
*/
|
|
315
|
+
for (const key of requiredKeys) {
|
|
316
|
+
const value = /** @type {Record<string, string>} */ (publish)[key];
|
|
317
|
+
if (/^[A-Za-z0-9._-]+$/.test(value) === false) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Expected "options.publish.${key}" to be a GitHub ${key} name (letters, digits, ".", "-" and "_"). Received: ${JSON.stringify(value)}`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (provider === "generic") {
|
|
326
|
+
const { url } =
|
|
327
|
+
/** @type {import("./appImage.js").GenericPublishOptions} */ (publish);
|
|
328
|
+
let protocol;
|
|
329
|
+
try {
|
|
330
|
+
protocol = new URL(url).protocol;
|
|
331
|
+
} catch {
|
|
332
|
+
protocol = undefined;
|
|
333
|
+
}
|
|
334
|
+
if (protocol !== "https:" && protocol !== "http:") {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`Expected "options.publish.url" to be an http(s) URL. Received: ${JSON.stringify(url)}`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Contents of the `app-update.yml` file `@nwutils/updater` reads to find out
|
|
344
|
+
* where releases are published. It is shipped next to the NW.js executable.
|
|
345
|
+
* @param {import("./appImage.js").PublishOptions} publish
|
|
346
|
+
* @returns {string}
|
|
347
|
+
*/
|
|
348
|
+
function appUpdateYaml(publish) {
|
|
349
|
+
return stringifyYaml(
|
|
350
|
+
publish.provider === "github"
|
|
351
|
+
? { provider: publish.provider, owner: publish.owner, repo: publish.repo }
|
|
352
|
+
: { provider: publish.provider, url: publish.url },
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Write the update info file `@nwutils/updater` compares the running version
|
|
358
|
+
* against, describing the packaged artifact at `filePath`. It must be uploaded
|
|
359
|
+
* to every release next to the artifact.
|
|
360
|
+
* @async
|
|
361
|
+
* @function
|
|
362
|
+
* @param {object} options
|
|
363
|
+
* @param {string} options.filePath Path to the packaged artifact, eg. the AppImage or installer.
|
|
364
|
+
* @param {string} options.updateInfoPath Path the update info file is written to.
|
|
365
|
+
* @param {string} options.version Version of the application.
|
|
366
|
+
* @returns {Promise<void>}
|
|
367
|
+
*/
|
|
368
|
+
async function writeUpdateInfo({ filePath, updateInfoPath, version }) {
|
|
369
|
+
const fileName = path.basename(filePath);
|
|
370
|
+
const sha512Digest = await sha512(filePath);
|
|
371
|
+
const { size } = await fs.promises.stat(filePath);
|
|
372
|
+
await fs.promises.writeFile(
|
|
373
|
+
updateInfoPath,
|
|
374
|
+
stringifyYaml({
|
|
375
|
+
version,
|
|
376
|
+
files: [{ url: fileName, sha512: sha512Digest, size }],
|
|
377
|
+
path: fileName,
|
|
378
|
+
sha512: sha512Digest,
|
|
379
|
+
releaseDate: new Date().toISOString(),
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
272
384
|
export default {
|
|
273
385
|
APPIMAGE_ARCH_KV,
|
|
386
|
+
appUpdateYaml,
|
|
274
387
|
desktopExecArg,
|
|
275
388
|
fileExists,
|
|
276
389
|
isWindowsFileName,
|
|
@@ -282,5 +395,7 @@ export default {
|
|
|
282
395
|
stringifyDesktopEntry,
|
|
283
396
|
stringifyYaml,
|
|
284
397
|
updateInfoFileName,
|
|
398
|
+
validatePublish,
|
|
285
399
|
windowsVersion,
|
|
400
|
+
writeUpdateInfo,
|
|
286
401
|
};
|
package/types/nsis.d.ts
CHANGED
|
@@ -52,6 +52,10 @@ export type NsisOptions = {
|
|
|
52
52
|
* Path to the NSIS compiler. Defaults to `makensis` on the `PATH`.
|
|
53
53
|
*/
|
|
54
54
|
makensisPath?: string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Where releases are published. When set, an `app-update.yml` pointing at it is installed next to the executable and a `latest[-<arch>].yml` update info file is written next to the installer, for `@nwutils/updater` to self update from.
|
|
57
|
+
*/
|
|
58
|
+
publish?: import("./appImage.js").PublishOptions | undefined;
|
|
55
59
|
};
|
|
56
60
|
/**
|
|
57
61
|
* The files and directories of a built application, relative to its root.
|
|
@@ -79,6 +83,10 @@ export type AppFiles = {
|
|
|
79
83
|
* installing a new version first uninstalls the previous one. Both wait for
|
|
80
84
|
* the application to be closed before touching its files.
|
|
81
85
|
*
|
|
86
|
+
* Run with `/S /UPDATE`, as `@nwutils/updater` does, the installer instead
|
|
87
|
+
* waits for the running application to exit, updates it silently and starts
|
|
88
|
+
* the new version.
|
|
89
|
+
*
|
|
82
90
|
* This composes with `@nwutils/builder`: run it against the `outDir` (and
|
|
83
91
|
* `app.name`) that `@nwutils/builder` produced for `platform: "win"`.
|
|
84
92
|
* `makensis` runs on Linux and macOS too, so the installer can be built on
|
|
@@ -88,7 +96,7 @@ export type AppFiles = {
|
|
|
88
96
|
* @param {NsisOptions} options
|
|
89
97
|
* @returns {Promise<string>} Path to the resulting installer `.exe` file.
|
|
90
98
|
*/
|
|
91
|
-
declare function nsis({ appDir, appName, version, arch, outDir, icon, appId, publisher, copyright, license, desktopShortcut, startMenuShortcut, makensisPath, }: NsisOptions): Promise<string>;
|
|
99
|
+
declare function nsis({ appDir, appName, version, arch, outDir, icon, appId, publisher, copyright, license, desktopShortcut, startMenuShortcut, makensisPath, publish, }: NsisOptions): Promise<string>;
|
|
92
100
|
/**
|
|
93
101
|
* List the files and directories under `appDir`. Symbolic links are followed,
|
|
94
102
|
* since NSIS copies what they point at.
|
|
@@ -120,9 +128,10 @@ export function listAppFiles(appDir: string): Promise<AppFiles>;
|
|
|
120
128
|
* @param {string} options.copyright
|
|
121
129
|
* @param {boolean} options.desktopShortcut
|
|
122
130
|
* @param {boolean} options.startMenuShortcut
|
|
131
|
+
* @param {string} [options.appUpdateYamlPath] Absolute path to an `app-update.yml` file to install next to the executable.
|
|
123
132
|
* @returns {string}
|
|
124
133
|
*/
|
|
125
|
-
export function nsisScript({ appDir, appFiles, appName, appId, version, fileVersion, arch, installerPath, iconPath, licensePath, publisher, copyright, desktopShortcut, startMenuShortcut, }: {
|
|
134
|
+
export function nsisScript({ appDir, appFiles, appName, appId, version, fileVersion, arch, installerPath, iconPath, licensePath, publisher, copyright, desktopShortcut, startMenuShortcut, appUpdateYamlPath, }: {
|
|
126
135
|
appDir: string;
|
|
127
136
|
appFiles: AppFiles;
|
|
128
137
|
appName: string;
|
|
@@ -137,4 +146,5 @@ export function nsisScript({ appDir, appFiles, appName, appId, version, fileVers
|
|
|
137
146
|
copyright: string;
|
|
138
147
|
desktopShortcut: boolean;
|
|
139
148
|
startMenuShortcut: boolean;
|
|
149
|
+
appUpdateYamlPath?: string | undefined;
|
|
140
150
|
}): string;
|
package/types/util.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
declare namespace _default {
|
|
2
2
|
export { APPIMAGE_ARCH_KV };
|
|
3
|
+
export { appUpdateYaml };
|
|
3
4
|
export { desktopExecArg };
|
|
4
5
|
export { fileExists };
|
|
5
6
|
export { isWindowsFileName };
|
|
@@ -11,7 +12,9 @@ declare namespace _default {
|
|
|
11
12
|
export { stringifyDesktopEntry };
|
|
12
13
|
export { stringifyYaml };
|
|
13
14
|
export { updateInfoFileName };
|
|
15
|
+
export { validatePublish };
|
|
14
16
|
export { windowsVersion };
|
|
17
|
+
export { writeUpdateInfo };
|
|
15
18
|
}
|
|
16
19
|
export default _default;
|
|
17
20
|
/**
|
|
@@ -20,6 +23,13 @@ export default _default;
|
|
|
20
23
|
* @type {Record<"ia32" | "x64" | "arm64", string>}
|
|
21
24
|
*/
|
|
22
25
|
declare const APPIMAGE_ARCH_KV: Record<"ia32" | "x64" | "arm64", string>;
|
|
26
|
+
/**
|
|
27
|
+
* Contents of the `app-update.yml` file `@nwutils/updater` reads to find out
|
|
28
|
+
* where releases are published. It is shipped next to the NW.js executable.
|
|
29
|
+
* @param {import("./appImage.js").PublishOptions} publish
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
declare function appUpdateYaml(publish: import("./appImage.js").PublishOptions): string;
|
|
23
33
|
/**
|
|
24
34
|
* Quote `value` as a single argument of a desktop entry's `Exec` key, per the
|
|
25
35
|
* Desktop Entry Specification: `%` is doubled so it isn't read as a field
|
|
@@ -105,13 +115,21 @@ declare function stringifyDesktopEntry(entries: Record<string, string | undefine
|
|
|
105
115
|
*/
|
|
106
116
|
declare function stringifyYaml(entries: Record<string, string | number | boolean | Record<string, string | number | boolean>[] | undefined>): string;
|
|
107
117
|
/**
|
|
108
|
-
* Name of the update info file `@nwutils/updater` downloads
|
|
109
|
-
*
|
|
110
|
-
* `latest-linux.yml
|
|
111
|
-
*
|
|
118
|
+
* Name of the update info file `@nwutils/updater` downloads for `arch` on
|
|
119
|
+
* `platform`, matching electron-builder: `x64` gets the bare
|
|
120
|
+
* `latest-linux.yml` (Linux) or `latest.yml` (Windows), every other
|
|
121
|
+
* architecture gets an `-<arch>` suffix.
|
|
122
|
+
* @param {string} arch
|
|
123
|
+
* @param {"linux" | "win"} [platform="linux"]
|
|
112
124
|
* @returns {string}
|
|
113
125
|
*/
|
|
114
|
-
declare function updateInfoFileName(arch:
|
|
126
|
+
declare function updateInfoFileName(arch: string, platform?: "linux" | "win"): string;
|
|
127
|
+
/**
|
|
128
|
+
* Throw if `publish` isn't a valid `PublishOptions` object.
|
|
129
|
+
* @param {unknown} publish
|
|
130
|
+
* @returns {void}
|
|
131
|
+
*/
|
|
132
|
+
declare function validatePublish(publish: unknown): void;
|
|
115
133
|
/**
|
|
116
134
|
* Convert a version such as `"1.2.3"` or `"1.2.3-beta.1"` into the
|
|
117
135
|
* `X.X.X.X` form Windows version resources (and NSIS' `VIProductVersion`)
|
|
@@ -121,3 +139,20 @@ declare function updateInfoFileName(arch: "ia32" | "x64" | "arm64"): string;
|
|
|
121
139
|
* @returns {string}
|
|
122
140
|
*/
|
|
123
141
|
declare function windowsVersion(version: string): string;
|
|
142
|
+
/**
|
|
143
|
+
* Write the update info file `@nwutils/updater` compares the running version
|
|
144
|
+
* against, describing the packaged artifact at `filePath`. It must be uploaded
|
|
145
|
+
* to every release next to the artifact.
|
|
146
|
+
* @async
|
|
147
|
+
* @function
|
|
148
|
+
* @param {object} options
|
|
149
|
+
* @param {string} options.filePath Path to the packaged artifact, eg. the AppImage or installer.
|
|
150
|
+
* @param {string} options.updateInfoPath Path the update info file is written to.
|
|
151
|
+
* @param {string} options.version Version of the application.
|
|
152
|
+
* @returns {Promise<void>}
|
|
153
|
+
*/
|
|
154
|
+
declare function writeUpdateInfo({ filePath, updateInfoPath, version }: {
|
|
155
|
+
filePath: string;
|
|
156
|
+
updateInfoPath: string;
|
|
157
|
+
version: string;
|
|
158
|
+
}): Promise<void>;
|