From 011c96c60eef53cfe318bb84d43f6e520fd7b21c Mon Sep 17 00:00:00 2001 From: Ayushman Chhabra <14110965+ayushmanchhabra@users.noreply.github.com> Date: Sun, 10 May 2026 01:12:47 +0530 Subject: [PATCH 1/9] chore(docs): update README --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b165f6b..4594113 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,26 @@ Update NW.js applications for Linux, MacOS and Windows platforms. ## Usage ```js -import {} +import Updater from "@nwutils/updater"; +const Manifest = nw.require("./package.json"); + +const updater = new Updater(Manifest); + +let status = ""; +updater.checkNewVersion((err, newerVersionExists, remoteManifest) => { + if (err) { + status = `Error checking for updates: ${err.message}`; + return; + } + if (newerVersionExists) { + status = "A newer version is available."; + } else { + status = "No new version available."; + } +}); + +updater.download(); + ``` It gives you low-level API to: @@ -26,19 +45,12 @@ It gives you low-level API to: 5. The new app (in temp) will copy itself to the original folder, overwriting the old app. 6. The new app will run itself from original folder and exit the process. -You should build this logic by yourself though. As a reference you can use [example](app/index.html). - -Covered by tests and works for [linux](http://screencast.com/t/Je2ptbHhP), [windows](http://screencast.com/t/MSTKqVS3) and [mac](http://screencast.com/t/OXyC5xoA). - -## Examples - -- [Basic](examples/basic.js) - - ## API + #### new updater(manifest, options) + Creates new instance of updater. Manifest could be a `package.json` of project. Note that compressed apps are assumed to be downloaded in the format produced by [nw-builder](https://github.com/nwutils/nw-builder) (or [grunt-nw-builder](https://github.com/nwjs/grunt-nw-builder)). @@ -49,7 +61,9 @@ Note that compressed apps are assumed to be downloaded in the format produced by - options `object` - Optional + #### updater.checkNewVersion(cb) + Will check the latest available version of the application by requesting the manifest specified in `manifestUrl`. The callback will always be called; the second parameter indicates whether or not there's a newer version. @@ -60,7 +74,9 @@ This function assumes you use [Semantic Versioning](http://semver.org) and enfor - cb `function` - Callback arguments: error, newerVersionExists (`Boolean`), remoteManifest + #### updater.download(cb, newManifest) + Downloads the new app to a template folder **Params** @@ -70,17 +86,23 @@ Downloads the new app to a template folder **Returns**: `Request` - Request - stream, the stream contains `manifest` property with new manifest and 'content-length' property with the size of package. + #### updater.getAppPath() + Returns executed application path **Returns**: `string` + #### updater.getAppExec() + Returns current application executable **Returns**: `string` + #### updater.unpack(filename, cb, manifest) + Will unpack the `filename` in temporary folder. For Windows, [unzip](https://www.mkssoftware.com/docs/man1/unzip.1.asp) is used (which is [not signed](https://github.com/edjafarov/node-webkit-updater/issues/68)). @@ -91,7 +113,9 @@ For Windows, [unzip](https://www.mkssoftware.com/docs/man1/unzip.1.asp) is used - manifest `object` + #### updater.runInstaller(appPath, args, options) + Runs installer **Params** @@ -102,7 +126,9 @@ Runs installer **Returns**: `function` + #### updater.install(copyPath, cb) + Installs the app (copies current application to `copyPath`) **Params** @@ -111,7 +137,9 @@ Installs the app (copies current application to `copyPath`) - cb `function` - Callback arguments: error + #### updater.run(execPath, args, options) + Runs the app from original app executable path. **Params** @@ -122,7 +150,6 @@ Runs the app from original app executable path. Note: if this doesn't work, try `gui.Shell.openItem(execPath)` (see [node-webkit Shell](https://github.com/rogerwang/node-webkit/wiki/Shell)). - --- ## Manifest Schema @@ -156,18 +183,23 @@ The manifest could be a `package.json` of project, but doesn't have to be. The name of your app. From time, it is assumed your Mac app is called `.app`, your Windows executable is `.exe`, etc. ### manifest.version + [semver](http://semver.org) version of your app. ### manifest.manifestUrl + The URL where your latest manifest is hosted; where node-webkit-updater looks to check if there is a newer version of your app available. ### manifest.packages + An "object" containing an object for each OS your app (at least this version of your app) supports; `mac`, `win`, `linux32`, `linux64`. ### manifest.packages.{mac, win, linux32, linux64}.url + Each package has to contain a `url` property pointing to where the app (for the version & OS in question) can be downloaded. ### manifest.packages.{mac, win, linux32, linux64}.execPath (Optional) + It's assumed your app is stored at the root of your package, use this to override that and specify a path (relative to the root of your package). This can also be used to override `manifest.name`; e.g. if your `manifest.name` is `helloWorld` (therefore `helloWorld.app` on Mac) but your Windows executable is named `nw.exe`. Then you'd set `execPath` to `nw.exe` @@ -177,9 +209,11 @@ This can also be used to override `manifest.name`; e.g. if your `manifest.name` ## Troubleshooting ### Mac + If you get an error on Mac about too many files being open, run `ulimit -n 10240` ### Windows + On Windows, there is no "unzip" command built in by default. As a result, this project uses a third party "unzip.exe" in order to extract the downloaded update. On the NWJS site, in the "How to package and distribute your apps" file, one of the recommended methods of distribution is using EnigmaVirtualBox to package the app, nw.exe, and required DLLs into a single EXE file. This method works great for distribution, but unfortunately breaks node-webkit-updater, because it wraps the required unzip.exe file inside of the created EnigmaVirtualBox EXE. As a result, *it is not possible to use EnigmaVirtualBox to distribute your app if you plan on using node-webkit-updater*. Try using InnoSetup instead. ## Contributing From 990cd9ac6f0723cdd059a4cb95ad12120b0041c2 Mon Sep 17 00:00:00 2001 From: Ayushman Chhabra <14110965+ayushmanchhabra@users.noreply.github.com> Date: Sun, 10 May 2026 01:16:35 +0530 Subject: [PATCH 2/9] chore(test): use nw.App.manifest instead of nw.require(package.json) --- src/main.js | 49 +++++++++++++++++++++++++++++ tests/fixtures/app-current/index.js | 3 +- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index c1710aa..fa46138 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,7 @@ +const fs = await import('node:fs'); const os = await import('node:os'); +const path = await import('node:path'); +const stream = await import('node:stream'); function semverGt(v1, v2) { const [major1, minor1, patch1] = v1.replace(/^v/i, '').split('.').map(Number); @@ -83,6 +86,52 @@ class Updater { cb(error, false, null); }); } + + /** + * Downloads the new app to a temporary folder. + * + * @async + * @method + * @param {(error: Error|null, filepath: string|null) => void} cb + * @param {Manifest} newManifest + * @returns {void} + */ + download(cb, newManifest) { + const manifest = newManifest ?? this.manifest; + const url = manifest.packages[platform].url; + + const filename = decodeURI(path.basename(url)); + + const destinationPath = path.resolve( + this.options.temporaryDirectory, + filename + ); + + const writeStream = fs.createWriteStream(destinationPath); + + fetch(url) + .then((response) => { + if (!response.ok) { + throw new Error( + `Failed to download update: ${response.status} ${response.statusText}` + ); + } + + if (!response.body) { + throw new Error('Response body is not readable'); + } + + // Web ReadableStream -> Node.js Readable + const readable = stream.Readable.fromWeb(response.body); + return stream.promises.pipeline(readable, writeStream); + }) + .then(() => { + cb(null, destinationPath); + }) + .catch((err) => { + cb(err, null); + }); + } } export default Updater; diff --git a/tests/fixtures/app-current/index.js b/tests/fixtures/app-current/index.js index b06a155..092fe97 100644 --- a/tests/fixtures/app-current/index.js +++ b/tests/fixtures/app-current/index.js @@ -1,9 +1,8 @@ import Updater from "./updater.js"; -const manifest = nw.require("./package.json"); let updater; document.addEventListener("DOMContentLoaded", () => { - updater = new Updater(manifest); + updater = new Updater(nw.App.manifest); document.getElementById("check-for-updates-button").addEventListener("click", handleCheckForUpdates); }); From 5de57ea4528ea653a040568860752420422d0989 Mon Sep 17 00:00:00 2001 From: Ayushman Chhabra <14110965+ayushmanchhabra@users.noreply.github.com> Date: Sun, 10 May 2026 01:17:35 +0530 Subject: [PATCH 3/9] chore(docs): update LICENSE year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 53298f1..c918d8b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2023 NW.js Utils +Copyright (c) 2026 NW.js Utils Copyright (c) 2014 Eldar Djafarov Permission is hereby granted, free of charge, to any person obtaining a copy From 4dd904525fe42bce147dd835345733715c3da2b1 Mon Sep 17 00:00:00 2001 From: Ayushman Chhabra <14110965+ayushmanchhabra@users.noreply.github.com> Date: Sun, 10 May 2026 02:54:19 +0530 Subject: [PATCH 4/9] chore(test): add test case for updater.download function --- README.md | 2 +- src/main.js | 36 +++++++++++-- tests/fixtures/app-current/index.html | 33 +++++++++++- tests/fixtures/app-current/index.js | 21 +++++++- tests/fixtures/app-latest/index.html | 33 +++++++++++- tests/fixtures/app-latest/index.js | 22 +++++++- tests/fixtures/app-latest/updater.js | 77 +++++++++++++++++++++++++++ tests/fixtures/releases/manifest.json | 4 +- tests/specs/main.test.js | 22 ++++++-- types/src/main.d.ts | 10 ++++ 10 files changed, 241 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4594113..50df231 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ This function assumes you use [Semantic Versioning](http://semver.org) and enfor #### updater.download(cb, newManifest) -Downloads the new app to a template folder +Downloads the new app to a temporary folder. **Params** diff --git a/src/main.js b/src/main.js index fa46138..749691c 100644 --- a/src/main.js +++ b/src/main.js @@ -43,6 +43,32 @@ function semverGt(v1, v2) { * @property {string} temporaryDirectory - The path to a directory to download the updates to and unpack them in. Defaults to [`os.tmpdir()`](https://nodejs.org/api/os.html#os_os_tmpdir) */ +function getHost() { + let platform; + + switch (process.platform) { + case 'win32': + platform = 'windows'; + break; + + case 'darwin': + platform = 'macos'; + break; + + case 'linux': + platform = 'linux'; + break; + + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + + const arch = process.arch; + + return `${platform}-${arch}`; + +} + class Updater { /** @@ -98,10 +124,11 @@ class Updater { */ download(cb, newManifest) { const manifest = newManifest ?? this.manifest; - const url = manifest.packages[platform].url; + const url = manifest.packages[getHost()].url; const filename = decodeURI(path.basename(url)); + fs.mkdirSync(this.options.temporaryDirectory, { recursive: true }); const destinationPath = path.resolve( this.options.temporaryDirectory, filename @@ -121,9 +148,10 @@ class Updater { throw new Error('Response body is not readable'); } - // Web ReadableStream -> Node.js Readable - const readable = stream.Readable.fromWeb(response.body); - return stream.promises.pipeline(readable, writeStream); + return stream.promises.pipeline( + response.body, + writeStream + ); }) .then(() => { cb(null, destinationPath); diff --git a/tests/fixtures/app-current/index.html b/tests/fixtures/app-current/index.html index 97d4e5d..02d2480 100644 --- a/tests/fixtures/app-current/index.html +++ b/tests/fixtures/app-current/index.html @@ -1,13 +1,42 @@ + Demo + -
- + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunctionActionStatusData
checkNewVersion
download
+ + diff --git a/tests/fixtures/app-current/index.js b/tests/fixtures/app-current/index.js index 092fe97..18558a8 100644 --- a/tests/fixtures/app-current/index.js +++ b/tests/fixtures/app-current/index.js @@ -1,9 +1,13 @@ +const path = await import("node:path"); +const process = await import("node:process"); + import Updater from "./updater.js"; let updater; document.addEventListener("DOMContentLoaded", () => { - updater = new Updater(nw.App.manifest); + updater = new Updater(nw.App.manifest, { temporaryDirectory: path.resolve(nw.App.dataPath, "tmpDir") }); document.getElementById("check-for-updates-button").addEventListener("click", handleCheckForUpdates); + document.getElementById("download-button").addEventListener("click", handleDownload); }); function handleCheckForUpdates() { @@ -16,8 +20,23 @@ function handleCheckForUpdates() { } if (newerVersionExists) { updateStatus.textContent = "A newer version is available."; + document.getElementById("update-data").textContent = JSON.stringify(remoteManifest, null, 2); } else { updateStatus.textContent = "No new version available."; } }); } + +function handleDownload() { + const downloadStatus = document.getElementById("download-status"); + const newManifest = document.getElementById("update-data").textContent ? JSON.parse(document.getElementById("update-data").textContent) : null; + downloadStatus.textContent = "Downloading update..."; + updater.download((err, filePath) => { + if (err) { + downloadStatus.textContent = `Error downloading update: ${err.message}`; + return; + } + downloadStatus.textContent = "Update downloaded successfully at " + filePath; + document.getElementById("download-data").textContent = filePath; + }, newManifest); +} diff --git a/tests/fixtures/app-latest/index.html b/tests/fixtures/app-latest/index.html index 97d4e5d..02d2480 100644 --- a/tests/fixtures/app-latest/index.html +++ b/tests/fixtures/app-latest/index.html @@ -1,13 +1,42 @@ + Demo + -
- + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunctionActionStatusData
checkNewVersion
download
+ + diff --git a/tests/fixtures/app-latest/index.js b/tests/fixtures/app-latest/index.js index b06a155..18558a8 100644 --- a/tests/fixtures/app-latest/index.js +++ b/tests/fixtures/app-latest/index.js @@ -1,10 +1,13 @@ +const path = await import("node:path"); +const process = await import("node:process"); + import Updater from "./updater.js"; -const manifest = nw.require("./package.json"); let updater; document.addEventListener("DOMContentLoaded", () => { - updater = new Updater(manifest); + updater = new Updater(nw.App.manifest, { temporaryDirectory: path.resolve(nw.App.dataPath, "tmpDir") }); document.getElementById("check-for-updates-button").addEventListener("click", handleCheckForUpdates); + document.getElementById("download-button").addEventListener("click", handleDownload); }); function handleCheckForUpdates() { @@ -17,8 +20,23 @@ function handleCheckForUpdates() { } if (newerVersionExists) { updateStatus.textContent = "A newer version is available."; + document.getElementById("update-data").textContent = JSON.stringify(remoteManifest, null, 2); } else { updateStatus.textContent = "No new version available."; } }); } + +function handleDownload() { + const downloadStatus = document.getElementById("download-status"); + const newManifest = document.getElementById("update-data").textContent ? JSON.parse(document.getElementById("update-data").textContent) : null; + downloadStatus.textContent = "Downloading update..."; + updater.download((err, filePath) => { + if (err) { + downloadStatus.textContent = `Error downloading update: ${err.message}`; + return; + } + downloadStatus.textContent = "Update downloaded successfully at " + filePath; + document.getElementById("download-data").textContent = filePath; + }, newManifest); +} diff --git a/tests/fixtures/app-latest/updater.js b/tests/fixtures/app-latest/updater.js index c1710aa..749691c 100644 --- a/tests/fixtures/app-latest/updater.js +++ b/tests/fixtures/app-latest/updater.js @@ -1,4 +1,7 @@ +const fs = await import('node:fs'); const os = await import('node:os'); +const path = await import('node:path'); +const stream = await import('node:stream'); function semverGt(v1, v2) { const [major1, minor1, patch1] = v1.replace(/^v/i, '').split('.').map(Number); @@ -40,6 +43,32 @@ function semverGt(v1, v2) { * @property {string} temporaryDirectory - The path to a directory to download the updates to and unpack them in. Defaults to [`os.tmpdir()`](https://nodejs.org/api/os.html#os_os_tmpdir) */ +function getHost() { + let platform; + + switch (process.platform) { + case 'win32': + platform = 'windows'; + break; + + case 'darwin': + platform = 'macos'; + break; + + case 'linux': + platform = 'linux'; + break; + + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + + const arch = process.arch; + + return `${platform}-${arch}`; + +} + class Updater { /** @@ -83,6 +112,54 @@ class Updater { cb(error, false, null); }); } + + /** + * Downloads the new app to a temporary folder. + * + * @async + * @method + * @param {(error: Error|null, filepath: string|null) => void} cb + * @param {Manifest} newManifest + * @returns {void} + */ + download(cb, newManifest) { + const manifest = newManifest ?? this.manifest; + const url = manifest.packages[getHost()].url; + + const filename = decodeURI(path.basename(url)); + + fs.mkdirSync(this.options.temporaryDirectory, { recursive: true }); + const destinationPath = path.resolve( + this.options.temporaryDirectory, + filename + ); + + const writeStream = fs.createWriteStream(destinationPath); + + fetch(url) + .then((response) => { + if (!response.ok) { + throw new Error( + `Failed to download update: ${response.status} ${response.statusText}` + ); + } + + if (!response.body) { + throw new Error('Response body is not readable'); + } + + return stream.promises.pipeline( + response.body, + writeStream + ); + }) + .then(() => { + cb(null, destinationPath); + }) + .catch((err) => { + cb(err, null); + }); + } } export default Updater; diff --git a/tests/fixtures/releases/manifest.json b/tests/fixtures/releases/manifest.json index fe9c30e..524c8d5 100644 --- a/tests/fixtures/releases/manifest.json +++ b/tests/fixtures/releases/manifest.json @@ -2,10 +2,10 @@ "name": "app", "version": "0.0.2", "author": "NW.js Utils", - "manifestUrl": "http://localhost:3000/releases/manifest.json", + "manifestUrl": "http://localhost:3000/manifest.json", "packages": { "linux-x64": { - "url": "http://localhost:3000/releases/app-0.0.2-linux-x64.zip" + "url": "http://localhost:3000/app-0.0.2-linux-x64.zip" } } } diff --git a/tests/specs/main.test.js b/tests/specs/main.test.js index e28457e..036e988 100644 --- a/tests/specs/main.test.js +++ b/tests/specs/main.test.js @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import http from "node:http"; import path from "node:path"; +import os from "node:os"; import { after, before, describe, it } from "node:test"; import express from "express"; @@ -79,15 +80,26 @@ describe("updater test suite", function () { const button = await driver.findElement(selenium.By.id("check-for-updates-button")); await button.click(); - // const postButtonClick = await driver.findElement(statusLocator).getText(); - // await driver.sleep(1000); - // assert.strictEqual(postButtonClick, "Checking for updates..."); - - // await driver.sleep(5000); const finalText = await driver.findElement(statusLocator).getText(); assert.strictEqual(finalText, "A newer version is available."); }); + it("runs the application and downloads the update", async function () { + const downloadLocator = selenium.By.id("download-status"); + const initialText = await driver.findElement(downloadLocator).getText(); + assert.strictEqual(initialText, ""); + + const button = await driver.findElement(selenium.By.id("download-button")); + await button.click(); + await driver.sleep(5000); + + const finalText = await driver.findElement(downloadLocator).getText(); + assert.ok(finalText.startsWith("Update downloaded successfully"), "Expected download success message."); + + const downloadFilePath = await driver.findElement(selenium.By.id("download-data")).getText(); + assert.strictEqual(downloadFilePath, path.resolve(os.homedir(), ".config", "demo", "tmpDir", "app-0.0.2-linux-x64.zip")); + }); + after(async function () { await new Promise((resolve, reject) => { server.close((err) => { diff --git a/types/src/main.d.ts b/types/src/main.d.ts index a54ac80..061ca09 100644 --- a/types/src/main.d.ts +++ b/types/src/main.d.ts @@ -96,4 +96,14 @@ declare class Updater { * @returns {void} */ checkNewVersion(cb: (error: Error | null, newerVersionExists: boolean, remoteManifest: object | null) => void): void; + /** + * Downloads the new app to a temporary folder. + * + * @async + * @method + * @param {(error: Error|null, filepath: string|null) => void} cb + * @param {Manifest} newManifest + * @returns {void} + */ + download(cb: (error: Error | null, filepath: string | null) => void, newManifest: Manifest): void; } From b24c87a3689c09bf3e389e3a2c484bd7d54f5649 Mon Sep 17 00:00:00 2001 From: Ayushman Chhabra <14110965+ayushmanchhabra@users.noreply.github.com> Date: Sun, 10 May 2026 03:03:54 +0530 Subject: [PATCH 5/9] chore(test): use --user-data-dir for testing purpose --- .gitignore | 1 + tests/specs/main.test.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 77fb328..6c50099 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ cache tests/fixtures/releases/app-0.0.1-linux-x64 tests/fixtures/app-current/updater.js +tests/fixtures/nwjs-user-data diff --git a/tests/specs/main.test.js b/tests/specs/main.test.js index 036e988..6b636ce 100644 --- a/tests/specs/main.test.js +++ b/tests/specs/main.test.js @@ -21,7 +21,8 @@ describe("updater test suite", function () { let driver = undefined; const options = new chrome.Options(); const seleniumArguments = [ - "nwapp=" + path.resolve("tests", "fixtures", "app-current") + "nwapp=" + path.resolve("tests", "fixtures", "app-current"), + "user-data-dir=" + path.resolve("tests", "fixtures", "nwjs-user-data"), ]; seleniumArguments.push("headless=new"); options.addArguments(seleniumArguments); @@ -97,7 +98,7 @@ describe("updater test suite", function () { assert.ok(finalText.startsWith("Update downloaded successfully"), "Expected download success message."); const downloadFilePath = await driver.findElement(selenium.By.id("download-data")).getText(); - assert.strictEqual(downloadFilePath, path.resolve(os.homedir(), ".config", "demo", "tmpDir", "app-0.0.2-linux-x64.zip")); + assert.strictEqual(downloadFilePath, path.resolve(process.cwd(), "tests", "fixtures", "nwjs-user-data", "Default", "tmpDir", "app-0.0.2-linux-x64.zip")); }); after(async function () { From f19fb273b086e43857844cbb44d98b05f4e5ee6d Mon Sep 17 00:00:00 2001 From: Ayushman Chhabra <14110965+ayushmanchhabra@users.noreply.github.com> Date: Sun, 10 May 2026 03:07:46 +0530 Subject: [PATCH 6/9] chore(test): git ignore app-latest --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6c50099..8ed43ec 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ cache tests/fixtures/releases/app-0.0.1-linux-x64 tests/fixtures/app-current/updater.js +tests/fixtures/app-latest tests/fixtures/nwjs-user-data From 62d46b9465e18d69afe844d93ca8c4b209e2681c Mon Sep 17 00:00:00 2001 From: Ayushman Chhabra <14110965+ayushmanchhabra@users.noreply.github.com> Date: Sun, 10 May 2026 03:09:43 +0530 Subject: [PATCH 7/9] chore(test): git ignore app-latest --- tests/fixtures/app-latest/index.html | 42 ------- tests/fixtures/app-latest/index.js | 42 ------- tests/fixtures/app-latest/package.json | 14 --- tests/fixtures/app-latest/updater.js | 165 ------------------------- 4 files changed, 263 deletions(-) delete mode 100644 tests/fixtures/app-latest/index.html delete mode 100644 tests/fixtures/app-latest/index.js delete mode 100644 tests/fixtures/app-latest/package.json delete mode 100644 tests/fixtures/app-latest/updater.js diff --git a/tests/fixtures/app-latest/index.html b/tests/fixtures/app-latest/index.html deleted file mode 100644 index 02d2480..0000000 --- a/tests/fixtures/app-latest/index.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - Demo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FunctionActionStatusData
checkNewVersion
download
- - - - - diff --git a/tests/fixtures/app-latest/index.js b/tests/fixtures/app-latest/index.js deleted file mode 100644 index 18558a8..0000000 --- a/tests/fixtures/app-latest/index.js +++ /dev/null @@ -1,42 +0,0 @@ -const path = await import("node:path"); -const process = await import("node:process"); - -import Updater from "./updater.js"; - -let updater; -document.addEventListener("DOMContentLoaded", () => { - updater = new Updater(nw.App.manifest, { temporaryDirectory: path.resolve(nw.App.dataPath, "tmpDir") }); - document.getElementById("check-for-updates-button").addEventListener("click", handleCheckForUpdates); - document.getElementById("download-button").addEventListener("click", handleDownload); -}); - -function handleCheckForUpdates() { - const updateStatus = document.getElementById("update-status"); - updateStatus.textContent = "Checking for updates..."; - updater.checkNewVersion((err, newerVersionExists, remoteManifest) => { - if (err) { - updateStatus.textContent = `Error checking for updates: ${err.message}`; - return; - } - if (newerVersionExists) { - updateStatus.textContent = "A newer version is available."; - document.getElementById("update-data").textContent = JSON.stringify(remoteManifest, null, 2); - } else { - updateStatus.textContent = "No new version available."; - } - }); -} - -function handleDownload() { - const downloadStatus = document.getElementById("download-status"); - const newManifest = document.getElementById("update-data").textContent ? JSON.parse(document.getElementById("update-data").textContent) : null; - downloadStatus.textContent = "Downloading update..."; - updater.download((err, filePath) => { - if (err) { - downloadStatus.textContent = `Error downloading update: ${err.message}`; - return; - } - downloadStatus.textContent = "Update downloaded successfully at " + filePath; - document.getElementById("download-data").textContent = filePath; - }, newManifest); -} diff --git a/tests/fixtures/app-latest/package.json b/tests/fixtures/app-latest/package.json deleted file mode 100644 index 4ccc516..0000000 --- a/tests/fixtures/app-latest/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "demo", - "version": "0.0.2", - "main": "index.html", - "chromium-args": "--enable-features=NWESM,NWChainImportNode", - "author": "NW.js Utils ", - "license": "MIT", - "manifestUrl": "http://localhost:3000/manifest.json", - "packages": { - "linux-x64": { - "url": "http://localhost:3000/app-0.0.1-linux-x64.zip" - } - } -} \ No newline at end of file diff --git a/tests/fixtures/app-latest/updater.js b/tests/fixtures/app-latest/updater.js deleted file mode 100644 index 749691c..0000000 --- a/tests/fixtures/app-latest/updater.js +++ /dev/null @@ -1,165 +0,0 @@ -const fs = await import('node:fs'); -const os = await import('node:os'); -const path = await import('node:path'); -const stream = await import('node:stream'); - -function semverGt(v1, v2) { - const [major1, minor1, patch1] = v1.replace(/^v/i, '').split('.').map(Number); - const [major2, minor2, patch2] = v2.replace(/^v/i, '').split('.').map(Number); - - if (major1 !== major2) { - return major1 > major2; - } - if (minor1 !== minor2) { - return minor1 > minor2; - } - return patch1 > patch2; -} - -/** - * @typedef {object} Platform - * @property {string} url - The URL to the package - * @property {string} execPath - The path to the executable - */ - -/** - * @typedef {object} Packages - * @property {Platform} win - The Windows package - * @property {Platform} mac - The macOS package - * @property {Platform} linux32 - The Linux 32-bit package - * @property {Platform} linux64 - The Linux 64-bit package - */ - -/** - * @typedef {object} Manifest - * @property {string} name - The name of the application - * @property {string} version - The current version of the application - * @property {string} manifestUrl - The URL to the remote manifest file - * @property {Packages} packages - The packages for the application - */ - -/** - * @typedef {object} UpdaterOptions - * @property {string} temporaryDirectory - The path to a directory to download the updates to and unpack them in. Defaults to [`os.tmpdir()`](https://nodejs.org/api/os.html#os_os_tmpdir) - */ - -function getHost() { - let platform; - - switch (process.platform) { - case 'win32': - platform = 'windows'; - break; - - case 'darwin': - platform = 'macos'; - break; - - case 'linux': - platform = 'linux'; - break; - - default: - throw new Error(`Unsupported platform: ${process.platform}`); - } - - const arch = process.arch; - - return `${platform}-${arch}`; - -} - -class Updater { - - /** - * Creates new instance of Updater. - * - * @constructor - * @param {Manifest} manifest - See the [manifest schema](https://github.com/nwutils/updater?tab=readme-ov-file#manifest-schema). - * @param {UpdaterOptions} options - Optional - */ - constructor(manifest, options) { - this.manifest = manifest; - this.options = { - temporaryDirectory: options && options.temporaryDirectory || os.tmpdir(), - }; - } - - /** - * Check the latest available version of the application by requesting the manifest specified in `manifestUrl`. - * - * @async - * @method - * @param {(error: Error|null, newerVersionExists: boolean, remoteManifest: object|null) => void} cb - * @returns {void} - */ - checkNewVersion(cb) { - const currentVersion = this.manifest.version; - - fetch(this.manifest.manifestUrl) - .then((response) => { - if (!response.ok) { - throw new Error(`HTTP error: ${response.status}`); - } - return response.json(); - }) - .then((data) => { - const latestVersion = data.version; - - cb(null, semverGt(latestVersion, currentVersion), data); - }) - .catch((error) => { - cb(error, false, null); - }); - } - - /** - * Downloads the new app to a temporary folder. - * - * @async - * @method - * @param {(error: Error|null, filepath: string|null) => void} cb - * @param {Manifest} newManifest - * @returns {void} - */ - download(cb, newManifest) { - const manifest = newManifest ?? this.manifest; - const url = manifest.packages[getHost()].url; - - const filename = decodeURI(path.basename(url)); - - fs.mkdirSync(this.options.temporaryDirectory, { recursive: true }); - const destinationPath = path.resolve( - this.options.temporaryDirectory, - filename - ); - - const writeStream = fs.createWriteStream(destinationPath); - - fetch(url) - .then((response) => { - if (!response.ok) { - throw new Error( - `Failed to download update: ${response.status} ${response.statusText}` - ); - } - - if (!response.body) { - throw new Error('Response body is not readable'); - } - - return stream.promises.pipeline( - response.body, - writeStream - ); - }) - .then(() => { - cb(null, destinationPath); - }) - .catch((err) => { - cb(err, null); - }); - } -} - -export default Updater; From 66b8af27b375764c617c4f3a1f0abc96b5bdad6e Mon Sep 17 00:00:00 2001 From: Ayushman Chhabra <14110965+ayushmanchhabra@users.noreply.github.com> Date: Sun, 10 May 2026 03:10:50 +0530 Subject: [PATCH 8/9] chore: update types --- types/src/main.d.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/types/src/main.d.ts b/types/src/main.d.ts index 061ca09..140325a 100644 --- a/types/src/main.d.ts +++ b/types/src/main.d.ts @@ -51,29 +51,6 @@ export type UpdaterOptions = { */ temporaryDirectory: string; }; -/** - * @typedef {object} Platform - * @property {string} url - The URL to the package - * @property {string} execPath - The path to the executable - */ -/** - * @typedef {object} Packages - * @property {Platform} win - The Windows package - * @property {Platform} mac - The macOS package - * @property {Platform} linux32 - The Linux 32-bit package - * @property {Platform} linux64 - The Linux 64-bit package - */ -/** - * @typedef {object} Manifest - * @property {string} name - The name of the application - * @property {string} version - The current version of the application - * @property {string} manifestUrl - The URL to the remote manifest file - * @property {Packages} packages - The packages for the application - */ -/** - * @typedef {object} UpdaterOptions - * @property {string} temporaryDirectory - The path to a directory to download the updates to and unpack them in. Defaults to [`os.tmpdir()`](https://nodejs.org/api/os.html#os_os_tmpdir) - */ declare class Updater { /** * Creates new instance of Updater. From 42f95f712ade54bf90b52df65711f2c910616003 Mon Sep 17 00:00:00 2001 From: Ayushman Chhabra <14110965+ayushmanchhabra@users.noreply.github.com> Date: Sun, 10 May 2026 03:20:06 +0530 Subject: [PATCH 9/9] chore(ci): add npm audit check --- .github/workflows/ci.yml | 5 +++- README.md | 23 +++++++++++------- package-lock.json | 52 ++++++++++++++++++++-------------------- 3 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a4c774..191401b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,9 +31,12 @@ jobs: - name: Setup Volta uses: volta-cli/action@v4.2.1 - - name: Install dependencies + - name: Install third party dependencies run: npm ci + - name: Check for third party vulnerabilities + run: npm audit + - name: Check for linting errors run: npm run lint diff --git a/README.md b/README.md index 50df231..72ada51 100644 --- a/README.md +++ b/README.md @@ -15,25 +15,32 @@ Update NW.js applications for Linux, MacOS and Windows platforms. ```js import Updater from "@nwutils/updater"; -const Manifest = nw.require("./package.json"); -const updater = new Updater(Manifest); +const updater = new Updater(nw.App.manifest); -let status = ""; +let updateStatus = ""; +let newManifest = ""; updater.checkNewVersion((err, newerVersionExists, remoteManifest) => { if (err) { - status = `Error checking for updates: ${err.message}`; + updateStatus = `Error checking for updates: ${err.message}`; return; } if (newerVersionExists) { - status = "A newer version is available."; + updateStatus = "A newer version is available."; + newManifest = remoteManifest; } else { - status = "No new version available."; + updateStatus = "No new version available."; } }); -updater.download(); - +let downloadStatus = ""; +updater.download((err, filePath) => { + if (err) { + downloadStatus = `Error downloading update: ${err.message}`; + return; + } + downloadStatus = "Update downloaded successfully at " + filePath; +}, newManifest); ``` It gives you low-level API to: diff --git a/package-lock.json b/package-lock.json index b232b1f..7606aa8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -882,6 +882,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1935,9 +1945,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -2032,6 +2042,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -2727,29 +2750,6 @@ "minimatch": "^5.1.0" } }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/resedit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/resedit/-/resedit-3.0.2.tgz",