Bundle your Node app to a single executable for Windows, Linux and OsX

Jochem Stoel - Sep 16 '18 - - Dev Community

A question I get asked so many times by so many people is how to compile a Node app to a single executable. I am surprised because this is actually pretty simple.

Reasons to ask

  • protect source code from being altered or copied - You can't open executable files in a simple text editor.
  • hide API credentials - Same difference as protecting source code.
  • ship to systems without Node or NPM - No need to NPM install dependencies, bundle everything in a single executable.
  • dictate Node version - Force a certain version of Node to guarantee feature support.
  • prevent commercial application from being nulled - It is not as easy anymore as commenting out, replacing or removing the license validation function.
  • increase performance - This is not a valid reason. The bundled executable does not perform better and because it includes a full Node it is a whole lot bigger (22MB) than just the 13kb JavaScript.
  • show off to friends - We all do this at times.
  • learn in general - People with a general interest in how things work under the hood. My favorite reason.
  • see proof that I can - Well, here it is.

There are a few tools out there that do pretty much the same thing. In this post I will focus on using pkg because it is free (open source) and in my experience so far the most pleasant to work with.

PKG

PKG is a command line tool that simplifies the build process of your app. Install it globally by running npm i pkg -g You can also use it programmatically but we will come to that.

Example Node app 'prettyprint.exe'

We will create a Node app that opens a .json input file, add indentation (tabs, spaces) and console log the beautified much more readable JSON. I will extensively describe the process and create a git of these files.

NPM init / package.json

An easy way to create a new Node application with a package.json is to run npm init in an empty directory.

{
  "name": "prettyprint",
  "version": "0.0.1",
  "description": "Pretty print a JSON file.",
  "main": "main.js",
  "author": "anybody",
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

Module that exports our function

For the sake of absolute simplicity let's say main.js contains a single function that looks like this:

/* You might want to check first if the file exists and stuff but this is an example. */
const fs = require('fs')
module.exports = function(filePath) {
    let data = fs.readFileSync(filePath).toString() /* open the file as string */
    let object = JSON.parse(data) /* parse the string to object */
    return JSON.stringify(object, false, 3) /* use 3 spaces of indentation */
}
Enter fullscreen mode Exit fullscreen mode

Yes, @joelnet. We all know that you prefer to write it like this. Thank you.

module.exports = filePath => JSON.stringify(JSON.parse(require('fs').readFileSync(filePath).toString()), false, 3)
Enter fullscreen mode Exit fullscreen mode

Create a bin.js file.

const prettyprint = require('.') /* the current working directory so that means main.js because of package.json */
let theFile = process.argv[2] /* what the user enters as first argument */

console.log(
    prettyprint(theFile)
)
Enter fullscreen mode Exit fullscreen mode

Yes, @joelnet. It is shorter like this:

console.log(require('.')(process.argv[2]))
Enter fullscreen mode Exit fullscreen mode

A dummy JSON file to test if everything works

Or use your own JSON file.

{"user":{"name":"jochem","email":"jochemstoel@gmail.com"}}
Enter fullscreen mode Exit fullscreen mode

Test if you copy/pasted properly

If you run node bin.js file.json you are expected to see this:

{
   "user": {
      "name": "jochem",
      "email": "jochemstoel@gmail.com"
   }
}
Enter fullscreen mode Exit fullscreen mode

Add one property to package.json

Simply add a property "bin" with value "bin.js" to your package json like so:

{
  "name": "prettyprint",
  "version": "0.0.1",
  "description": "Pretty print a JSON file.",
  "main": "main.js",
  "bin": "bin.js", 
  "author": "anybody",
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

Run pkg

Run pkg . from your app directory to build an executable.
By not providing a target it will build for all three platforms. Windows, Linux and OSX.

pkg .
> pkg@4.3.4
> Targets not specified. Assuming:
  node10-linux-x64, node10-macos-x64, node10-win-x64
Enter fullscreen mode Exit fullscreen mode

Done!

Voila. 3 new files will have been created.

prettyprint-win.exe
prettyprint-linux
prettyprint-macos
Enter fullscreen mode Exit fullscreen mode

To see your application in action, run prettyprint-win.exe file.json. On Linux, chmod your binary a+x to make it executable and then run ./prettyprint-linux file.json. Don't know about MacOS.

Extra

Relevant things I could not squeeze in anywhere.

Simple way to build for current platform and version

From your app folder, run pkg -t host .. The -t means target platform and the value host means whatever your system is. The . means current directory.
Obviously, you can run pkg --help for a complete list of arguments.

In package.json, "main" and "bin" need not be different

Although you generally want to separate them, main and bin can both have the same value and do not necessarily need to be two separate files.

Dependencies need to be in package.json

If you NPM install after you created your app, it will automatically add the dependency to package.json for you.

Native modules and assets

To include asset files/directories in your executable and/or to build a node app that depends on native Node modules, read the documentation.

...

Everything we did in this tutorial is not absolutely neccessary. You don't need the whole package.json with a "bin" property and all that stuff. That is just common practive and helps you learn. You can also just build a single JavaScript file.

PKG API

In this example I use the PKG API to build a single JavaScript file without the need of a whole working directory or package.json

/* js2exe.js */
const { exec } = require('pkg')
exec([ process.argv[2], '--target', 'host', '--output', 'app.exe' ]).then(function() {
    console.log('Done!')
}).catch(function(error) {
    console.error(error)
})
Enter fullscreen mode Exit fullscreen mode

Yes, @joelnet. It is shorter like this:

require('pkg').exec([ process.argv[2], '--target', 'host', '--output', 'app.exe' ]).then(console.log).catch(console.error)
Enter fullscreen mode Exit fullscreen mode
Run
node js2exe.js "file.js"
Enter fullscreen mode Exit fullscreen mode
Make your own standalone compiler executable

You can even let it build itself, resulting in a single executable that can build itself and any other JavaScript on its own. A standalone compiler.

node js2exe.js js2exe.js
Enter fullscreen mode Exit fullscreen mode

Now you can use your output executable app.exe as a standalone compiler that does not require Node or NPM anymore.

app.exe myfile.js
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player