@nwutils/packager 0.5.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
@@ -12,7 +12,7 @@ Supported output formats, selected via `format`:
12
12
  | `deb` | Linux | Planned |
13
13
  | `rpm` | Linux | Planned |
14
14
  | `msix` | Windows | Planned |
15
- | `nsis` | Windows | Planned |
15
+ | `nsis` | Windows | Implemented |
16
16
  | `dmg` | MacOS | Planned |
17
17
 
18
18
  ## Getting Started
@@ -106,14 +106,82 @@ managers. `true` derives it from `publish`
106
106
  `zsyncmake` (usually packaged as `zsync`) installed on the host, and the
107
107
  `.zsync` file must be uploaded alongside the AppImage.
108
108
 
109
+ ### NSIS installer
110
+
111
+ Build the application with `platform: "win"` first, then:
112
+
113
+ ```js
114
+ const installerPath = await packager({
115
+ format: "nsis",
116
+ appDir: "./out",
117
+ appName: "Demo",
118
+ version: "1.2.0",
119
+ outDir: "./dist",
120
+ icon: "./icon.ico",
121
+ publisher: "ACME",
122
+ license: "./LICENSE.txt",
123
+ });
124
+ // ./dist/Demo-x64-Setup.exe
125
+ ```
126
+
127
+ The installer walks the user through a wizard and installs for the current
128
+ user only, into `%LOCALAPPDATA%\Programs\<appName>`, so it never asks for
129
+ administrator rights. It adds Start Menu and desktop shortcuts, and registers
130
+ an uninstaller in Windows' installed apps list. The uninstaller removes exactly
131
+ the files that were installed. Installing a new version first uninstalls the
132
+ previous one, matched by `appId`, and stops if that fails. If the application
133
+ is running, the installer and uninstaller ask the user to close it first;
134
+ in silent mode (`/S`) they exit with an error instead.
135
+
136
+ The installer refuses to run on Windows machines that can't run `arch`: x64
137
+ builds need x64 Windows, or ARM64 Windows 11, which emulates x64.
138
+
139
+ Run the installer with `/S` to install silently, and `/D=<dir>` (last, unquoted)
140
+ to choose the install directory.
141
+
142
+ It needs the NSIS compiler, `makensis`, installed on the host. It runs on
143
+ Linux (`apt install nsis`) and MacOS (`brew install makensis`) too, so a Windows
144
+ installer can be built from any platform. On Windows, `choco install nsis`.
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
+
109
177
  ## API Reference
110
178
 
111
179
  ### `packager({ format, ...options })`
112
180
 
113
- | Name | Type | Description |
114
- | ------ | -------------------------------------------------- | ---------------------------------------------------------------------------- |
115
- | format | `"AppImage" \| "deb" \| "rpm" \| "MSIX" \| "NSIS"` | Which packager to run. Only `"AppImage"` is implemented today. |
116
- | ... | - | The remaining options are passed through to the packager for `format` as-is. |
181
+ | Name | Type | Description |
182
+ | ------ | ----------------------------------------------------------- | ---------------------------------------------------------------------------- |
183
+ | format | `"AppImage" \| "deb" \| "rpm" \| "msix" \| "nsis" \| "dmg"` | Which packager to run. `"AppImage"` and `"nsis"` are implemented today. |
184
+ | ... | - | The remaining options are passed through to the packager for `format` as-is. |
117
185
 
118
186
  Resolves with the path to the resulting packaged artifact.
119
187
 
@@ -139,6 +207,29 @@ Options
139
207
 
140
208
  Resolves with the path to the resulting `.AppImage` file.
141
209
 
210
+ ### `nsis(options)`
211
+
212
+ Options
213
+
214
+ | Name | Type | Description |
215
+ | ----------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |
216
+ | appDir | `string` | Path to a built NW.js Windows application, ie. the `outDir` produced by `@nwutils/builder` for `platform: "win"` |
217
+ | appName | `string` | Name of the application. Must match the `app.name` value used to build `appDir`. Must be a valid Windows file name |
218
+ | version | `string` | Version of the application, eg. `"1.2.0"`. Required |
219
+ | arch | `"ia32" \| "x64" \| "arm64"` | Architecture `appDir` was built for. Defaults to the host architecture |
220
+ | outDir | `string` | Directory the resulting `<appName>-<arch>-Setup.exe` file is written to. Defaults to the parent directory of `appDir` |
221
+ | icon | `string` | Path to a `.ico` icon for the installer and uninstaller. Defaults to NSIS' own icon |
222
+ | appId | `string` | Name of the registry key the installation is recorded under. Defaults to `appName`. Keep it the same across releases |
223
+ | publisher | `string` | Publisher shown in Windows' installed apps list |
224
+ | copyright | `string` | Copyright notice written into the installer's version information |
225
+ | license | `string` | Path to a `.txt` or `.rtf` license file the user has to accept |
226
+ | desktopShortcut | `boolean` | If true, create a desktop shortcut. Defaults to `true` |
227
+ | startMenuShortcut | `boolean` | If true, create a Start Menu shortcut. Defaults to `true` |
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` |
230
+
231
+ Resolves with the path to the resulting installer `.exe` file.
232
+
142
233
  ## Contributing
143
234
 
144
235
  ### External contributor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nwutils/packager",
3
- "version": "0.5.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",
@@ -43,8 +43,8 @@
43
43
  "lint": "eslint . ../../tests/specs/packager ../../tests/fixtures/packager",
44
44
  "lint:fix": "eslint . ../../tests/specs/packager ../../tests/fixtures/packager --fix",
45
45
  "type": "tsc",
46
- "test": "node --test --test-concurrency=1 \"../../tests/specs/packager/appImage.test.js\" \"../../tests/specs/packager/request.test.js\" \"../../tests/specs/packager/util.test.js\"",
47
- "test:real": "node --test \"../../tests/specs/packager/appImage.real.test.js\""
46
+ "test": "node --test --test-concurrency=1 \"../../tests/specs/packager/appImage.test.js\" \"../../tests/specs/packager/nsis.test.js\" \"../../tests/specs/packager/request.test.js\" \"../../tests/specs/packager/util.test.js\"",
47
+ "test:real": "node --test --test-concurrency=1 \"../../tests/specs/packager/appImage.real.test.js\" \"../../tests/specs/packager/nsis.real.test.js\""
48
48
  },
49
49
  "dependencies": {},
50
50
  "devEngines": {
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/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import appImage from "./appImage.js";
2
+ import nsis from "./nsis.js";
2
3
 
3
4
  /**
4
5
  * Supported packaged output formats.
5
6
  *
6
7
  * `AppImage`, `deb` and `rpm` package a Linux build. `MSIX` and `NSIS`
7
- * package a Windows build. Only `"AppImage"` is implemented today - the
8
- * others are reserved so this option doesn't need to change shape once they
9
- * land.
8
+ * package a Windows build. `"AppImage"` and `"nsis"` are implemented today -
9
+ * the others are reserved so this option doesn't need to change shape once
10
+ * they land.
10
11
  * @typedef {"AppImage" | "deb" | "rpm" | "msix" | "nsis" | "dmg"} PackageFormat
11
12
  */
12
13
 
@@ -19,20 +20,21 @@ import appImage from "./appImage.js";
19
20
  * Package a built NW.js application.
20
21
  * @async
21
22
  * @function
22
- * @param {{format: PackageFormat} & Record<string, any>} options `format`, plus the options the matching packager (eg. `appImage`) expects.
23
+ * @param {{format: PackageFormat} & Record<string, any>} options `format`, plus the options the matching packager (eg. `appImage` or `nsis`) expects.
23
24
  * @returns {Promise<string>} Path to the resulting packaged artifact.
24
25
  */
25
26
  async function packager({ format, ...options }) {
26
27
  switch (format) {
27
28
  case "AppImage":
28
29
  return appImage(/** @type {any} */ (options));
30
+ case "nsis":
31
+ return nsis(/** @type {any} */ (options));
29
32
  case "deb":
30
33
  case "rpm":
31
34
  case "msix":
32
- case "nsis":
33
35
  case "dmg":
34
36
  throw new Error(
35
- `options.format ${JSON.stringify(format)} is not implemented yet. Currently only "AppImage" is supported.`,
37
+ `options.format ${JSON.stringify(format)} is not implemented yet. Currently only "AppImage" and "nsis" are supported.`,
36
38
  );
37
39
  default:
38
40
  throw new Error(