This is a basic guide for building a desktop application using NW.js and React. It should work in both Windows and Linux and most of it will possibly also work in macOS. This is not meant to be an end-to-end build solution, but more as a guide to getting started with using NW.js together with React.
This repository can be cloned, for a copy of the nw-react-example
app generated by following the instructions below.
The following is expected to already be installed/configured before starting this guide.
- Node.js - The latest Long-Term Support (LTS) release of Node.js should be installed and in your PATH.
- Visual Studio Code - A decent JavaScript editor/IDE should be installed. Some steps in this guide may assume Visual Studio Code is being used but other options are available.
The following steps will result in a development environment, where your React application will be running in NW.js and automatically reload for any changes. Note that all instances of nw-react-example
should be replaced with the name of your application.
-
Open a terminal, navigate to a directory where you have write permission, then run the following commands:
npx create-react-app nw-react-example cd nw-react-example npm i concurrently wait-on react-devtools nw-builder cross-env npm i --save-exact [email protected]
Note #1: The latest available version of NW.js should be installed above.
Note #2: The above NPM commands would normally be
--save-dev
to make themdevDependencies
. However,create-react-app
incorrectly marks all of its dependencies asdependencies
. So, we'll be renaming the entire section in step #2 below.Note #3: When using macOS on Apple Silicon, installation of
nw
will fail. As a workaround until an ARM build of NW.js is available, set the NPM environment variable to force the x64 build to be used (e.g.npm_config_nwjs_process_arch=x64 npm i
). First launch of the application will be slower as Rosetta 2 translates the binary. -
Open the file
nw-react-example/package.json
and make the following changes:- Rename
dependencies
todevDependencies
. - Add the following:
Note #1: Both
"main": "main.js", "homepage": ".", "node-remote": [ "http://127.0.0.1:3042", "file://*" ], "build": { "manifestProps": [ "name", "version", "main", "node-remote" ], "osTypes": [ "windows" ] }, "eslintConfig": { "globals": { "nw": true } }, "scripts": { "dev": "concurrently \"npm start\" \"wait-on http://127.0.0.1:3042 && cross-env NWJS_START_URL=http://127.0.0.1:3042 nw --enable-logging=stderr .\"", "dev-tools": "concurrently \"react-devtools\" \"cross-env REACT_APP_DEVTOOLS=enabled npm start\" \"wait-on http://127.0.0.1:3042 && cross-env NWJS_START_URL=http://127.0.0.1:3042 nw --enable-logging=stderr .\"", "predist": "cross-env GENERATE_SOURCEMAP=false BUILD_PATH=./dist/app/build/ npm run build", "dist": "node dist.mjs" }
eslintConfig
andscripts
should already exist. The above items should be added to the existing sections.
- Rename
-
Add the following to
nw-react-example\.env
(new file):PORT=3042 BROWSER=none
-
Add the following to
nw-react-example\main.js
(new file):const url = require('node:url'); const baseUri = url.pathToFileURL(__dirname).toString(); const interfaceUri = process.env.NWJS_START_URL ? process.env.NWJS_START_URL.trim() : `${baseUri}/build/`; const startUri = `${interfaceUri}/index.html`; nw.Window.open(startUri);
-
Add the following to
nw-react-example\dist.mjs
(new file):import { copyFile, readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { nwbuild } from 'nw-builder'; const packageManifest = JSON.parse(await readFile('./package.json')); const appBaseDir = path.resolve('./dist/app/'); const defaultBuildCfg = { manifestProps: [ 'name', 'version', 'main', ], osTypes: [ 'windows', 'linux', ], nwVersion: '0.70.1', }; // Copy main.js to dist/app/ directory for packaging // NOTE: The predist script should run webpack (or something similar) after `npm run build`, to bundle main.js and any Node.js dependencies into a single file. // NOTE: If this isn't done, the following will need to be modified to copy all necessary files/dependencies to the dist/app/ directory. console.log(`Copying Node-context script to ${appBaseDir}`); try { await copyFile('main.js', path.resolve(appBaseDir, 'main.js')); } catch (error) { console.error('Unable to copy Node-context script to app directory:', error); } // Create production package.json console.log('Generating application manifest (package.json)...'); const manifestProps = packageManifest.build?.manifestProps || defaultBuildCfg.manifestProps; const prodManifest = {}; for (const propName of manifestProps) { prodManifest[propName] = packageManifest[propName]; } try { await writeFile(path.resolve(appBaseDir, 'package.json'), JSON.stringify(prodManifest, null, 4)); } catch (error) { console.error('Unable to generate application manifest:', error); } // Build package for each OS type const appOsTypes = packageManifest.build?.osTypes || defaultBuildCfg.osTypes; const appName = packageManifest.build?.appName || packageManifest.name; const appVersion = packageManifest.version || '1.0.0'; for (const osType of appOsTypes) { console.log(`Building package for ${osType}...`); const platform = osType === 'windows' ? 'win' : osType; const nwVersion = packageManifest.devDependencies.nw.split('-')[0] || defaultBuildCfg.nwVersion; const outDir = path.resolve(`./dist/${appName}-${appVersion}-${osType}/`); const nwBuildArgs = { srcDir: appBaseDir, version: nwVersion, flavour: 'normal', platform: platform, arch: 'x64', outDir, run: false, zip: true }; try { await nwbuild(nwBuildArgs); } catch (error) { console.error(`Error building package for ${osType}`); } console.log(`Finished building package for ${osType}`); }
-
Add the following at the top of the
<head>
block innw-react-example\public\index.html
:<script>if ('%REACT_APP_DEVTOOLS%'.trim() === 'enabled') document.write('<script src="http:\/\/127.0.0.1:8097"><\/script>')</script>
-
At this point, you can run
npm run dev
. The React development "live" server will be started and NW.js will be launched, connecting to that "live" server. Any updates to your React application will automatically be reflected in the NW.js window. -
To access Chrome developer tools, right-click on the window that opens. Selecting "Inspect" will show DevTools for the current window. Selecting "Inspect background page" shows DevTools for the Node.js process running
main.js
. -
Running
npm run dev-tools
will behave the same as above, but will also start a standalone version of React DevTools which the React application will connect to. -
Any NPM packages used with the React portion of your application should be installed as
devDependencies
(npm install --save-dev <package>
). Any NPM packages use bymain.js
(or any other Node.js-context scripts) that need to be included in the "production" application, should be installed asdependencies
(npm install <package>
). This will be further-clarified in the "Production Build" section below. -
Attempting to install the NW.js NPM package on an Apple Silicon system will fail unless you set the architecture:
npm_config_nwjs_process_arch=x64 npm i [email protected]
-
NOTE: There is currently an issue (nwjs/nw.js#7852) where "zombie" NW.js processes stick around and chew up system memory, when the application is closed by pressing CTRL-C in the terminal where
npm run dev
was executed. A decent workaround for this behavior would be much appreciated!
To automatically create a "production" build of your application, run the command npm run dist
. This will use nw-builder to create ZIP files for all specified operating systems, which contain a fully-functional and distributable application.
The following properties are available for the build
configuration object in package.json
. These control behavior of the dist.mjs
script.
"build": {
"appName": "ApplicationName",
"manifestProps": [
"name",
"version",
"main",
"node-remote"
],
"osTypes": [
"windows",
"linux",
"osx"
]
}
appName
: Optional property which will be used to name the directories and ZIP files generated bydist.mjs
. If not specified, thename
property frompackage.json
will be used.manifestProps
: Array of property names which will be copied from the developmentpackage.json
into the productionpackage.json
. If not specified, will default to "name", "version", and "main".osTypes
: Array of operating systems for which distribution packages should be built. If not specified, defaults to "windows" and "linux".
Notes:
- The
version
property frompackage.json
will be used in the generated directories and ZIP files. - To use Node.js or NW.js APIs inside the React application, the
node-remote
property must be included in the productionpackage.json
. - The generated ZIP/directory can be used to create an installation package (see below for suggestions).
- The
nw-builder
package is undergoing a significant updates with v4 and functionality may change or break behavior of this build process. Current issues (as of 4.0.1):- Linux builds will fail: nwutils/nw-builder#699
- MacOS (osx) builds will fail: nwutils/nw-builder#705
- The
nw.exe
file included in the production file is not renamed: nwutils/nw-builder#695
- No testing has been done on Linux or macOS. Please report any observed issues.
The following steps can be followed to manually create a "production" build of your application. This will use the "built" version of your React application and the "normal" (non-SDK) build of NW.js (disabling DevTools).
- Run the command
npm run build
inside the application development directory (nw-react-example
from the example above). This will generate anw-react-example/build/
directory, which is the production version of the React application. - Download the "Normal" (non-SDK) build of NW.js (https://nwjs.io/downloads/), which matches the version being used for development (0.70.1 in the example above). Extract the files into a new directory (e.g.
nw-react-example/dist/
). Note that the NW.js zip/tgz contains a directory similar tonwjs-v0.70.1-win-x64
. It's the content of that directory, which need to be extracted tonw-react-example/dist/
(resulting in/nw-react-example/dist/nw.exe
). - Rename the NW.js executable file to match your application name (Windows:
nw.exe
tonw-react-example.exe
| Linux:nw
tonw-react-example
). This can be any name. - Create a directory named
package.nw
inside the directory created in step #2 above (e.g.nw-react-example/dist/package.nw/
). - Copy the following files to this new
package.nw
directory:nw-react-example/main.js
,nw-react-example/package.json
,nw-react-example/build/
(the entire directory). - Edit the new copy of
package.json
(in thenw-react-example/dist/
directory) and remove all of the following properties/sections:private
,devDependencies
,scripts
,eslintConfig
, andbrowserslist
. - If the Node.js context of your application uses any NPM packages (anything in
dependencies
), these need to be installed into thepackage.nw
directory. To do this, runnpm install --no-save
insidenw-react-example/dist/package.nw/
.
The main directory from step #2 will now include your fully-functional application. It can be zipped and/or copied anywhere and used without needing anything else pre-installed. Ideally, this directory would now be turned into an "installer" for easy distribution. This can be done with tools like InnoSetup for Windows or building a DEB/RPM for Linux.
Often, it's fairly-trivial at this point to write a custom "build system" that automatically runs through the above steps (as well as any additional customization steps needed), and generates installers for all supported operating systems. There are some existing packages for this task, but most have not been maintained:
- nw-builder - Currently, the only actively-maintained NW.js build tool.
- nwjs-builder-phoenix - This was an excellent set of build scripts, but it has not been maintained. It's still a good reference, if building a custom build system.
- battery-app-workshop - While some of the details are outdated, this is another excellent resource for manual builds.
- create-nw-react-app - A highly opinionated NW.js/React boilerplate based around Create React App (CRA) and Webpack. Has lots of choices and additional tooling already made for you, set up and installed.