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

Node.jsJavaScriptTypeScript
AngaBlue
AngaBlueSoftware Engineer

6 February, 2022

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

TL;DR: @angablue/exe

Languages such as C++, C# and even 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:

1 npm i @angablue/exe

Then create a config file, exe.json:

1{
2    "entry": "index.js",
3    "out": "My Cool App.exe"
4}
1npx exe exe.json

Alternatively, you can completely customise the appearance of the final output with a few extra fields.

1{
2    "entry": "index.js",
3    "out": "Gameflip Rocket League Items Bot.exe",
4    "skipBundle": false,
5    "version": "{package:version}",
6    "icon": "icon.ico",
7    "executionLevel": "asInvoker",
8    "properties": {
9        "FileDescription": "{package:description}",
10        "ProductName": "Gameflip Rocket League Items Bot",
11        "LegalCopyright": "{package:author.name} https://anga.blue",
12        "OriginalFilename": "{package:name}"
13    }
14}

Notice how we can use values like {package:description}? This will pull the description field from our package.json, meaning we don't have to duplicate information.

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

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

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

1"scripts": {
2  "package": "npx exe exe.json"
3}

For TypeScript users, you can simplify specify your .ts file as the entry and everything will be handled for you.

Under the hood

@angablue/exe is simply an abstraction, inspired by combining the power of two packages; Vercel's pkg and resedit. pkg has since been deprecated, and I've now replaced it with Node's native SEA (Single Executable Applications) and Vercel's ncc.

ncc, while not 100% necessary, makes our lives a lot easier. It bundles all our project files and dependencies into a single minified bundle. While we don't need to do this for single file projects (without dependencies), I really hope this isn't your case. ncc can also handle transpiling TypeScript, which means we don't need to employ tsc as a pre-package script. This is key as we reduce reliance and the need for the end-user to install programs and libraries such as Node.js and the associated modules from npm.

SEA performs the bulk of the work, allowing us to package JavaScript with the nide binary Node.js into one package. We can use this feature to create cross-platform apps, with options to target operating systems such as Windows, macOS and Linux, depending on which platform you are on. Using SEA, we can actually create fully functional executables right away. However, currently SEA 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 SEA 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.