Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +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
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
65 changes: 53 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,33 @@ Update NW.js applications for Linux, MacOS and Windows platforms.
## Usage

```js
import {}
import Updater from "@nwutils/updater";

const updater = new Updater(nw.App.manifest);

let updateStatus = "";
let newManifest = "";
updater.checkNewVersion((err, newerVersionExists, remoteManifest) => {
if (err) {
updateStatus = `Error checking for updates: ${err.message}`;
return;
}
if (newerVersionExists) {
updateStatus = "A newer version is available.";
newManifest = remoteManifest;
} else {
updateStatus = "No new version available.";
}
});

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:
Expand All @@ -26,19 +52,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

<a name="new_updater"></a>

#### 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)).
Expand All @@ -49,7 +68,9 @@ Note that compressed apps are assumed to be downloaded in the format produced by
- options `object` - Optional

<a name="updater#checkNewVersion"></a>

#### 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.
Expand All @@ -60,8 +81,10 @@ This function assumes you use [Semantic Versioning](http://semver.org) and enfor
- cb `function` - Callback arguments: error, newerVersionExists (`Boolean`), remoteManifest

<a name="updater#download"></a>

#### updater.download(cb, newManifest)
Downloads the new app to a template folder

Downloads the new app to a temporary folder.

**Params**

Expand All @@ -70,17 +93,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.
<a name="updater#getAppPath"></a>

#### updater.getAppPath()

Returns executed application path

**Returns**: `string`
<a name="updater#getAppExec"></a>

#### updater.getAppExec()

Returns current application executable

**Returns**: `string`
<a name="updater#unpack"></a>

#### 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)).

Expand All @@ -91,7 +120,9 @@ For Windows, [unzip](https://www.mkssoftware.com/docs/man1/unzip.1.asp) is used
- manifest `object`

<a name="updater#runInstaller"></a>

#### updater.runInstaller(appPath, args, options)

Runs installer

**Params**
Expand All @@ -102,7 +133,9 @@ Runs installer

**Returns**: `function`
<a name="updater#install"></a>

#### updater.install(copyPath, cb)

Installs the app (copies current application to `copyPath`)

**Params**
Expand All @@ -111,7 +144,9 @@ Installs the app (copies current application to `copyPath`)
- cb `function` - Callback arguments: error

<a name="updater#run"></a>

#### updater.run(execPath, args, options)

Runs the app from original app executable path.

**Params**
Expand All @@ -122,7 +157,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
Expand Down Expand Up @@ -156,18 +190,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 `<manifest.name>.app`, your Windows executable is `<manifest.name>.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`
Expand All @@ -177,9 +216,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
Expand Down
52 changes: 26 additions & 26 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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 {

/**
Expand Down Expand Up @@ -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;
Loading
Loading