Creating a Windows Executable File (.exe) from a Node.js app

Node.jsJavaScript
AngaBlue
AngaBlueFull Stack Web Developer

6 February, 2022

Creating a Windows Executable File (.exe) from a Node.js app

TL;DR: @angablue/exe

Languages such as C++, C# and Python have long been used to create .exe applications for Windows. However, this isn't commonplace for Node applications written in JavaScript. Here's how and why I combined existing tools to create .exe files for Node.js using @angablue/exe.

What are .exe applications?

Files with the extension .exe (i.e. ending with .exe) are executable applications for the Windows platform. They contain a set of low-level CPU instructions thatare sequentially executed once a user opens the file by double-clicking it.

The problem with distributing JavaScript apps

I originally ran into this problem when trying to distribute my Node.js Gameflip bots to customers. I used to transpile my TypeScript code to JavaScript, then instruct customers to install Node.js and Python (remember some packages need node-gyp to build when installing). I would then send a zipped folder, containing the app.

This caused many headaches. Sometimes Node.js wouldn't be added to the user's PATH, meaning node index.js would throw an error:

'node' is not recognized as an internal or external command, operable program or batch file.

Other times, install scripts would fail, they would have 2 versions of Python etc... All in all, what should be a double click to start, could take nearly an hour of setup to get going.

The solution: compiled binaries

Compiled binaries have a few advantages over zipped bundles of JavaScript.

  • It's a standalone file. No need to zip anything or make sure files are in the right folder; once downloaded, you're good to go. Users don't care about how the app works, they just want your app to work. This cuts out all the associated problems with a fragmented app.
  • Source code obfuscation. While JavaScript can be easily read, modified and reverse-engineered by anyone with the requisite knowledge, compiled binaries are far harder to read and edit. After being transformed to bytecode, the inner workings of your app become unobtainable knowledge to everyone bar the few modders and reverse engineers.
  • System agnostic. Aside from some basic requirements such as a recent-ish install of Windows (7, 10 or 11), compiled apps will work regardless of where they are given they don't rely on external dependencies. They also have added bonus of working on any hardware, just like JavaScript running on Node.
  • Smaller build sizes. Yes, that's right, smaller build sizes. Despite the fact that Node.js is bundled into the executable, the final build size for a moderately sized app ends up around 30MB - 40MB. Simply the node_modules folder for this project is substantially larger, sitting at 200MB, and that's not even including the size of Node.js.

This was the solution I needed. Ever since implementing the extra step to build to .exe in my build script, I haven't once thought of going back.

Compiling to .exe

To easily compile to an .exe, I've built an npm package, @angablue/exe:

 npm i @angablue/exe

Then create a build script, build.js:

1// build.js
2const exe = require('@angablue/exe');
3
4const build = exe({
5    entry: './index.js',
6    out: './build/My Cool App.exe',
7    target: 'latest-win-x64'
8});
9
10build.then(() => console.log('Build completed!'));

You can customise the appearance of the final output with a few extra fields.

1// build.js
2const exe = require('@angablue/exe');
3
4const build = exe({
5    entry: './index.js',
6    out: './build/Gameflip Rocket League Items Bot.exe',
7    pkg: ['-C', 'GZip'], // Specify extra pkg arguments
8    productVersion: '2.4.2',
9    fileVersion: '2.4.2',
10    target: 'latest-win-x64',
11    icon: './assets/icon.ico', // Application icons must be in .ico format
12    properties: {
13        FileDescription: 'Gameflip Rocket League Items Bot',
14        ProductName: 'Gameflip Rocket League Items Bot',
15        LegalCopyright: 'AngaBlue https://anga.blue',
16        OriginalFilename: 'Gameflip Rocket League Items Bot.exe'
17    }
18});
19
20build.then(() => console.log('Build completed!'));

Application Properties
The resultant output looks professional and includes useful information.

I also recommend adding this script to your package.json:

1"scripts": {
2  "package": "node ./build.js"
3}

...or for TypeScript users:

1"scripts": {
2  "build": "tsc",
3  "package": "npm run build && node ./build.js"
4}

You can tweak these values how you like to personalise the output .exe.

Under the hood

@angablue/exe is simply an abstraction, combining the power of two packages; Vercel's pkg and resedit.

pkg performs the bulk of the work, allowing us to compile JavaScript to bytecode and bundle assets and Node.js into one package. pkg has many useful features and applications such as:

  • Compiling cross-platform apps, with options to target many versions of Node.js as well as operating systems such as Windows, macOS and Linux.
  • Bundling assets into a single package, whether they be code, images or data.
  • Centralizing dependencies, reducing reliance and the need for the end-user to install programs and libraries such as Node.js and the associated modules from npm.

Using pkg, we can actually create fully functional executables right away. However, currently pkg has no support for changing the appearance or properties of the output file. This is where resedit comes in.

resedit is a tool that allows us to directly edit certain properties of any executable file. In this case, we're modifying the output of our pkg build, adding additional info such as an author, copyright notice, version number and an icon. Using resedit we can make our app not only function, but also look the part too.