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

Jochem Stoel @jochemstoel

About: Pellentesque nec neque ex. Aliquam at quam vitae lacus convallis pulvinar. Mauris vitae ullamcorper lacus. Cras nisi dui, faucibus non dolor quis, volutpat euismod massa. Donec et pulvinar erat.

Location:
Inner Earth
Joined:
Sep 30, 2017

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

Publish Date: Sep 16 '18
211 49

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

Comments 49 total

  • Jochem Stoel
    Jochem StoelSep 16, 2018

    What are you saying?

  • Jochem Stoel
    Jochem StoelSep 16, 2018

    I would like to brutally honestly point out that I have not really done much benchmarking to support my claim that there is no difference.

  • Peter Benjamin (they/them)
    Peter Benjamin (they/them)Sep 17, 2018

    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

    It's very easy to examine source code of bundled/packaged node.js applications.

    introspecting packaged/bundled binaries with strings linux command

    • Peter Benjamin (they/them)
      Peter Benjamin (they/them)Sep 17, 2018

      Please, don't recommend pkg, or any bundling/packaging technique, as a security/privacy control.

      If you need to protect sensitive/secret data (e.g. passwords, API tokens), you can use one of many symmetric (e.g. AES-256) or asymmetric (RSA) encryption algorithms.

      Alternatively, there are developer tools that aim to solve this problem in a more developer friendly way than you having to manage public/private keys yourself. I personally like Hashicorp Vault.

      • Jochem Stoel
        Jochem StoelSep 22, 2018

        You are right. Please let me point out that I was not recommending pkg as a security protocol but listing it as one of the reasons people ask me how to use it.

        edit: additionally, is there any Windows equivalent of what you're doing in the example with strings?

        • Peter Benjamin (they/them)
          Peter Benjamin (they/them)Sep 23, 2018

          I was not recommending pkg as a security protocol but listing it as one of the reasons people ask me how to use it.

          The way you're presenting the topic implies that you're suggesting bundling/packaging applications for these use-cases.

          is there any Windows equivalent of what you're doing in the example with strings?

          superuser.com/questions/124081/is-...

          • Jochem Stoel
            Jochem StoelOct 5, 2018

            Say Peter, how would you go about making your code unreadable then if this is not the way? Simply obfuscate it? That does not do a well enough job in my opinion.

            • Peter Benjamin (they/them)
              Peter Benjamin (they/them)Oct 6, 2018

              It depends on what you're trying to accomplish.

              If you're trying to make your code "unreadable", then obfuscation is what you're looking for. Keep in mind, obfuscation does not make your code "secure". There are such thing as deobfuscators.

              If you want to "secure" your source code, well, there is little you can do in this area for the following reasons:

              • Dynamic languages are easily accessible/readable.
              • Compiled languages that compile to intermediate byte-code can be decompiled:
              • Compiled languages that compile to machine native code can be disassembled (i.e. translated to assembly)
              • jsloop42
                jsloop42Mar 8, 2019

                You can open the binary in Ollydbg on Windows and search for strings. It will be visible as plain text. But the source code itself will be in assembly, because we are decompiling a native code.

                On macOS, you can view using the free version of Hopper disassembler.

    • jsloop42
      jsloop42Mar 8, 2019

      String search is not same as looking at plain source code. Strings are preserved as such in any application, be it written in C or AOT JS, unless you mangle using other techniques. You are misleading the reader. Open the native binary in any decompiler and you will get assembly, not bytecode like with Java class files.

  • Sergey Kislyakov
    Sergey KislyakovSep 17, 2018

    What are those "Yes, @joelnet " notes? Is there a reason for them?

    • Avalander
      AvalanderSep 17, 2018

      Yeah, from my perspective I guess it's some sort of inside joke between you both, but it's kind of mean if it isn't.

      Also, we all know that joelnet would start a new line for each chained method :P

      Nice article, for the rest, I didn't know about pkg :)

    • JavaScript Joel
      JavaScript JoelSep 17, 2018

      lol. I think it's a tongue in cheek jab at some of the comment discussions which have been... lengthy. Probably a discussion about my preferences to write function expressions instead of statements.

      If you are curious, check it some of my articles. Most of them are pretty controversial. :)

      I actually wouldn't find anything wrong with the code written here though.

      And you wouldn't want to know how I would write it either. It'd probably involve pipes or compose or a new language spec I have been working on github.com/joelnet/MojiScript

      But those things have their place. When the entire team understands FP. Or in your own personal projects etc. Always code to the team :D

  • frytaz1
    frytaz1Sep 20, 2018

    Can i use pkg not to include node in my binary but just link it from separate file ?

    • Jochem Stoel
      Jochem StoelSep 21, 2018

      I don't understand.

      • frytaz1
        frytaz1Sep 21, 2018

        Lets say i have few executables app1.exe, app2.exe, app3.exe and i want to save up disk space.
        So i would like pkg not to bundle node executable inside each binary, but dynamically link it. Is this possible ?

        • Jochem Stoel
          Jochem StoelOct 3, 2018

          Well I'm still not sure entirely what you mean but you could create a single bundle of Node with a set of dependencies that you need, then let it execute process.argv[2] or start a REPL if none is provided.

          What you basically have then is an executable that behaves just like Node.exe but with a few extra modules already included.

          Hope this helps you.

  • robin marsack barber
    robin marsack barberSep 26, 2018

    I am trying to do this exact thing right now at work and just removed pkg because it doesn't support being behind a proxy - full stop, as far as I can tell. A work around mentioned on their github didn't work (just download the files to your cache, remove the failed file, retry), and I can't find any other solutions. A bummer because it seems like the big game in town.

    • Jochem Stoel
      Jochem StoelSep 26, 2018

      What exactly do you mean by not working behind a proxy?

  • Anshuman Upadhyay
    Anshuman UpadhyayFeb 14, 2019

    I am facing the issue related to this. Updated the question on stackoverflow here :

    stackoverflow.com/questions/546834...

    Can You please help?

    • Jochem Stoel
      Jochem StoelFeb 15, 2019

      The error message seems to be saying it can not execute/find powershell. Check the PKG docs for process.cwd() and how to deal with current working directory.

  • coco-egaliciar
    coco-egaliciarMar 2, 2019

    Its a fraud...
    This is my code, I open the file.exe in Notepad++, scroling I found exactly my source code. jaja... This is not a real option for security. :/

    • Jochem Stoel
      Jochem StoelMar 4, 2019

      We already went over this.

    • artydev
      artydevJan 4, 2021

      Hello,

      Look at this obfuscator.io/, I can assure you, if you choose the right options, it will be very difficult to read the code

      Regards

  • АнонимJun 19, 2019

    [deleted]

    • Jochem Stoel
      Jochem StoelJun 19, 2019

      I don't know, never happened to me. I could have a look with you at your code if you want.

      • АнонимJun 19, 2019

        [deleted]

        • Jochem Stoel
          Jochem StoelJun 21, 2019

          I don't know I'd have to see your code.

          • АнонимOct 3, 2019

            [deleted]

            • Jochem Stoel
              Jochem StoelOct 4, 2019

              Hey Batman, are you on Windows or Linux? How are you building exactly?
              From the docs: Just be sure to call pkg package.json or pkg . to make use of scripts and assets entries.

              Also you might want to look at this Snapshot Filesystem part of the docs because maybe your assets are packaged correctly but you are not using the right path to access them.

              If you want you can send me these files and I will have a look for you to see what is wrong. Skype jochem.stoel or Discord jochemstoel#7529

              • АнонимOct 8, 2019

                [deleted]

                • Jochem Stoel
                  Jochem StoelOct 8, 2019

                  I have offered to take a look at your code several times and you are not answering any of my questions. There is not much I can do for you at this point. Yes it might be that you are using Node 10. No maybe that is not at all the case. I don't know.

  • Filipe Rezende
    Filipe RezendeJun 25, 2019

    Very interesting! How can i embed server dependencies like Express, Mongoose, etc?

    • Jochem Stoel
      Jochem StoelJun 26, 2019

      I wrote this already.

      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.

  • Eduardo Arcentales
    Eduardo ArcentalesAug 28, 2019

    What happens if your function have some environment variables (read from some file). How can you configure it in package.json?

    • Jochem Stoel
      Jochem StoelSep 2, 2019

      You can include assets in your package too.

      • Eduardo Arcentales
        Eduardo ArcentalesSep 3, 2019

        Well, I can reach it, my "only" problem now is if I execute in Windows to create a executable windows file, it works. But if I create my exe in Linux, when I go to Windows Machine it doesn't work.

        • Jochem Stoel
          Jochem StoelSep 3, 2019

          Does it throw an exception file not found when your run it? That might have something to do with the 'virtual' path your assets are stored. Those are not consistent on every platform.

          Packaged files have /snapshot/ prefix in their paths (or C:\snapshot\ in Windows). If you used pkg /path/app.js command line, then __filename value will be likely /snapshot/path/app.js at run time. __dirname will be /snapshot/path as well.

          Possibly useful:
          detecting assets
          snapshot filesystem

  • frytaz1
    frytaz1Nov 13, 2019

    Bundled binary is large, it includes node itself ?
    Is there a way to use pkg so it dynamically links to node instead ?

    • Jochem Stoel
      Jochem StoelNov 13, 2019

      Yes, the bundled binary includes Node. It basically puts your scripts and the Node executable in one file.
      As for loading Node dynamically, I think you are missing the point.

  • Stephen James
    Stephen JamesDec 22, 2019

    I would suggest using npx rather than installing it locally so you get the most up to date version.

  • marlarius
    marlariusJan 10, 2020

    Thanks a lot. This was just what I needed. I need to distribute a cross platform utility including a small webserver, so node was an obvious choice. My only problem was that the users are mostly non-techs, so I didn't like the thought of them having to install node and all the dependencies. pkg works out of the box. I don't even need to create the package.json file and module exports and whatnot. I simply enter "pkg myutil.js" - done! A second after I have three executables, one for Linux, Windows and Mac.

  • Adil ismail
    Adil ismailJun 24, 2020

    Hi, how can i run this exe everytime the user logs in ro windowz? It must run on every reboot.

  • Josiah Bryan
    Josiah BryanOct 13, 2020

    Cross compile? I have a device my company still manufacturers and deploys world-wide, running Ubuntu 14.04.3 ... on an ARMv7 Processor. I have a node app I'm creating for the product family, and I'd like to run it on this device as well. Tried going the whole nvm route to install-and-run node directly on it, but gyphy fails to build some deps from the project locally on the device. I'd really much rather use pkg to build a binary to deploy to the device.

    However, building the examples/express example from the pkg repo with pkg 4.4.9 like pkg . --targets node10.15.3-linux-armv7 --no-bytecode (on a linux box) and scp'ing the resulting binary over to the IOT device running the armv7 / Ubuntu 14 setup, I get the following error when trying to run the binary:

    ./express-example: relocation error: ./express-example: symbol
    _ZTVNSt7__cxx1115basic_stringbufIcSt11char_traitsIcESaIcEEE, 
    version GLIBCXX_3.4.21 not defined in file libstdc++.so.6 with link time reference
    
    Enter fullscreen mode Exit fullscreen mode

    (Line wraps added to break long line)

    Googling the error (specifically with regards to GLIBC and libstdc++.so.6) has gotten me nowhere. I can't figure out if the libstdc++ on the device is too old or too new. Tried updating libstdc++ but it said it was already at the latest version (for that OS.) I've got no clue where to go from here... Is there some way to compile the binary via pkg with different options, or statically link the libraries it needs instead of relying on system libraries?

    Also, when I try to use a newer node version (like 10.21.0, etc) - it fails with an "unable to build" message. I know I can crosscompile regular C/C++ code on that linux box for ARM (we do that currently with Jenkins in the cloud on a linux box), so is there a way to get crosscompile working at buildtime?

    Here's the error for building with 10.21:

    [root@decidr express]# ./node_modules/.bin/pkg . --targets node10-linux-armv7 --no-bytecode
    > pkg@4.4.9
    > Fetching base Node.js binaries to PKG_CACHE_PATH
      fetched-v10.21.0-linux-armv7 [                    ] 0%
    > Error! 404 Not Found
      https://github.com/zeit/pkg-fetch/releases/download/v2.6/uploaded-v2.6-node-v10.21.0-linux-armv7
    > Asset not found by direct link:
      {"tag":"v2.6","name":"uploaded-v2.6-node-v10.21.0-linux-armv7"}
    > Not found in GitHub releases:
      {"tag":"v2.6","name":"uploaded-v2.6-node-v10.21.0-linux-armv7"}
    > Building base binary from source:
      built-v10.21.0-linux-armv7
    > Error! Not able to build for 'armv7' here, only for 'x64'
    
    Enter fullscreen mode Exit fullscreen mode

    I find myself rather stuck - can't run node directly on the device, and the device won't run the pkg-built binary, even though it builds ARMv7 code. No idea how to proceed forward - any assistance or ideas? :)

    • clint hastings
      clint hastingsFeb 1, 2021

      Re: libstdc++.so.6 and GLIBC
      You can see what GLIBC versions are in libstdc++.so like this, with the right path to your libstdc++ file:

      strings /usr/lib64/libstdc++* | grep GLIBC

      The output on my system shows the highest version for C++ is 3.4.24
      When I have had a problem before, it is usually one or two versions behind, with the compiler saying GLIBCXX_3.4.25 or .26 is needed.
      GLIBCXX_3.4.22
      GLIBCXX_3.4.23
      GLIBCXX_3.4.24
      GLIBC_2.2.5
      GLIBC_2.3

      see posts like stackoverflow.com/questions/447732... as sometimes the softlink without a version number points to an older file.

  • Sabeth Kimuyu
    Sabeth KimuyuJun 4, 2021

    index.html not included in package error while running the express example

    Could you help me on the example given at the pkg github page.
    It keeps popping up this error when running the exercutable:

    Error: File or directory '/**/express/views/index.html' was not included into executable at compilation stage. Please recompile adding it as asset or script.
    at error_ENOENT (pkg/prelude/bootstrap.js:539:17)
    at findNativeAddonForStat (pkg/prelude/bootstrap.js:1201:32)
    at statFromSnapshot (pkg/prelude/bootstrap.js:1224:25)
    at Object.stat (pkg/prelude/bootstrap.js:1250:5)
    at SendStream.sendFile (/snapshot/express/node_modules/send/index.js:721:6)
    at SendStream.pipe (/snapshot/express/node_modules/send/index.js:595:8)
    at sendfile (/snapshot/express/node_modules/express/lib/response.js:1103:8)
    at ServerResponse.sendFile (/snapshot/express/node_modules/express/lib/response.js:433:3)
    at /snapshot/express/index.js:21:9
    at Layer.handle as handle_request

    My issue is found here

Add comment