NodeJS application to native executable
js2bin is a command line utility that helps package a bundled (think webpack, rollup etc) NodeJS application into a native executable. See How it works for more info.
Shipping a native binary has a number of benefits over shipping a bundled .js file
Benefits for you
- control over NodeJS version your code runs on
- develop for a NodeJS version that best fits your usecase
- stronger code obfuscation than what
UglifyJScan provide - reduced burden on supporting older versions of NodeJS
- decide to upgrade NodeJS based on your app's version
- reduced docs needs showing users how to install
node(on some platforms it can be a challenge)
Benefits for your user/customer:
- quick getting started experience
- reduced burden by not requiring installation/upgrade
node/npm
NOTE:
- all CLI options to
js2bintake the form--optionor--option=value - certain options can be arrays in such case provide the option multiple times, e.g.
--option=a --option=b
Make an executable of your application for MacOS:
js2bin --build --platform=darwin --node=10.16.0 --app=/path/to/my/app.js --name=CoolAppName
this will create a file named CoolAppName-darwin-x64 on your current working directory - if you're on a Mac you'll be able to execute it. Go ahead try it!
Make an executable of your application for MacOS and Linux:
js2bin --build --platform=darwin --platform=linux --node=10.16.0 --app=/path/to/my/app.js --name=CoolAppName
this will create 2 files named CoolAppName-darwin-x64 and CoolAppName-linux-x64 on your current working directory - if you're on Mac or Linux you'll be able to execute either of them. Go ahead try it!
In case where you want to build for a version of Node for which a prebuild binary does not exist you'll need to follow a 2 step process
- create a build with a placeholder (this will download NodeJS source, build it locally and add the build to the local cache)
js2bin --ci --cache --node=10.13.0 --size=2MB
- bundle your application into the just built binary
js2bin --build --cache --node=10.13.0 --app=/path/to/my/app.js --name=CoolAppName
--help: print this help message
--build: embed your application into the precompiled NodeJS binary.
--node: NodeJS version(s) to use, can specify more than one.
e.g. --node=10.16.0 --node=12.4.0
--platform: Platform(s) to build for, can specifiy more than one.
e.g. --platform=linux --plaform=darwin
--app: Path to your (bundled) application.
e.g. --app=/path/to/app/index.js
--name: Application name (optional)
--dir: (opt) Working directory, if not specified use cwd
e.g. --dir=/tmp/js2bin
--cache (opt) Cache any pre-built binaries used, to avoid redownload
--ci: build NodeJS with preallocated space for embedding applications
--node: NodeJS version to build from source, can specify more than one.
e.g. --node=10.16.0
--size: Amount of preallocated space, can specify more than one.
e.g. --size=2MB --size==4MB
--dir: (opt) Working directory, if not specified use cwd
--cache: (opt) whether to keep build in the cache (to be reused by --build)
--upload: (opt) whether to upload node build to github releases
--clean: (opt) whether to clean up after the build
While we've tried to minimize the amount of code changes needed when developing and testing using node and shipping a native binary, there are a few things you need to keep in mind:
process.argv-js2bininserts and fake argument inprocess.argv[1] = cwd()/<app-name>.js. This ensures minimal code changes, but if your application depends on using that file (highly unlikely) that file won't be there.child_process.fork- if you're application uses this NodeJS specific forking method (to spawn more copies of itself) then you'll need to distinguish betweennodemode and bundled app -
if(path.basename(process.execPath) === 'node') {
child_process.fork(....)
} else {
child_process.spawn(process.execPath, ...)
}
NodeJS provides compile time hooks for changing the behavior of the resulting binary, usually node. This is done by allowing users to (a) place their code in lib/_third_party_main.js, (b) modifying node.gyp to include that file in the build and then recompile. Once node is compiled, at startup controll will be passed to lib/_third_party_main.js as early as possible. There are some caveats tho:
- few options that are handled before control is handed over (e.g.
--version) - few modules (like
clustering) that are not set up - contents of
process.argvwould be different when your app is started this way (only relevant if you want to develop usingnodebut ship your app bundled up)
While the above is pretty straightforward it suffers from two problems:
- need to recompile
nodeafter every change to your application - recompiling
nodecan take time - think 20+ minutes (if you're on a laptop) - thus, less than ideal development experience.
Now, imagine if we changed the node build process to be a two step process:
- compile
nodewith some placeholder content, large enought to fit our application and cache it. - build our application by inserting it into the pre-compiled binary from (1).
This is exactly what js2bin does - with the following specs/modifications:
nodebinaries are prebuilt for a number of platforms with placeholder content for 2 and 4MB in size- the contents of the embedded application are compressed and base64 encoded. (why? because when embedding a JS script that has chars outside the ASCII range the entire script is stored using UCS-2/UTF-16 for storing the script, thus doubling in size. This is common when you bundle up char conversion libraries that contain pregenerated tables - e.g. is the popular iconv-lite )