@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nwutils/packager",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Package NW.js applications for Linux, MacOS and Windows.",
5
5
  "keywords": [
6
6
  "NW.js",
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.stringifyYaml(
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
- const appImageSha512 = await util.sha512(appImageFilePath);
371
- const { size } = await fs.promises.stat(appImageFilePath);
372
- await fs.promises.writeFile(
373
- path.resolve(resolvedOutDir, util.updateInfoFileName(resolvedArch)),
374
- util.stringifyYaml({
375
- version: /** @type {string} */ (version),
376
- files: [{ url: appImageFileName, sha512: appImageSha512, size }],
377
- path: appImageFileName,
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 script = nsisScript({
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("", "Function .onInit", " SetShellVarContext current");
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
- ' SetOutPath "$INSTDIR"',
476
- ' WriteUninstaller "$INSTDIR\\Uninstall.exe"',
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. Uses `$2`.
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 to find the
118
- * latest release, following electron-builder's naming: `x64` gets the bare
119
- * `latest-linux.yml`, every other architecture gets an `-<arch>` suffix.
120
- * @param {"ia32" | "x64" | "arm64"} arch
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
- return arch === "x64" ? "latest-linux.yml" : `latest-linux-${arch}.yml`;
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 to find the
109
- * latest release, following electron-builder's naming: `x64` gets the bare
110
- * `latest-linux.yml`, every other architecture gets an `-<arch>` suffix.
111
- * @param {"ia32" | "x64" | "arm64"} arch
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: "ia32" | "x64" | "arm64"): string;
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>;