DEV Community

Cover image for Create a zx Node.js script as binary with pkg
Matthias Zaunseder
Matthias Zaunseder

Posted on

Create a zx Node.js script as binary with pkg

So there is this really cool library called zx which you can use to create scripts that are replacements for bash scripts.
But one downside of it is, that now you have to have the Node.js runtime installed on the machine where this script should run. That's sad :(

But what if you could create a binary which includes your script AND the Node.js runtime?

pkg to the rescue!

But first things first, let's create a simple zx script. Please make sure that you have Node.js 16+ installed on your machine and then open a shell and type the following commands to create a new directory and initialise a Node.js project.

$ cd /my/projects
$ mkdir my-cli
$ cd my-cli
$ npm init -y
Enter fullscreen mode Exit fullscreen mode

Now you should have a package.json file and in this file you have to add "type": "module" for zx to work correctly:

{
  "name": "my-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
+ "type": "module"
}
Enter fullscreen mode Exit fullscreen mode

Next we can add zx as a dependency:

$ npm install zx
Enter fullscreen mode Exit fullscreen mode

We are ready to write our nice little script! Create a folder src and add a file index.js with this content:

// src/index.js
import { $ } from "zx";

async function main() {
  await $`date`;
}

main().catch((err) => console.log(err));
Enter fullscreen mode Exit fullscreen mode

You can test it now in the shell with

$ node src/index.js
Enter fullscreen mode Exit fullscreen mode

This should output the current date and time on your machine. But as we saw we have to use the node runtime to execute our script (or the zx runtime as they show in their examples). Because this is sometimes not ideal, for example if the machine doesn't have Node.js or zx installed then our script can not run there.

The solution to this problem is to pack the runtime and our script in an executable binary and then you can start your script even if the machine has no runtime installed.

For the packaging we will use the pkg library. But unfortunately pkg is not supporting ES modules which we configured by adding the "type": "module" to the package.json. So before we can use pkg we need to compile our script to a version which is not using ES modules. To do the compilation we will use esbuild. esbuild can also bundle our script into one file, so no dependencies on a node_modules folder are left in the compiled file. So let's install it.

$ npm install --save-dev esbuild
Enter fullscreen mode Exit fullscreen mode

And let's add a npm script in package.json to do the compilation:

{
  "name": "my-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "compile": "esbuild src/index.js --platform=node --target=node16 --bundle --outfile=dist/outfile.cjs"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "dependencies": {
    "zx": "^6.0.1"
  },
  "devDependencies": {
    "esbuild": "^0.14.27"
  }
}
Enter fullscreen mode Exit fullscreen mode

This npm script will execute esbuild, use src/index.js as an entrypoint and configure esbuild to output a Node.js v16+ compatible file to dist/outfile.cjs. The .cjs file ending is important because otherwise pkg tries to load our bundle with ES modules even if we have compiled them away.

So now you can try the compiling script:

$ npm run compile
Enter fullscreen mode Exit fullscreen mode

You will see something like this in the shell:

> my-cli@1.0.0 compile
> esbuild src/index.js --platform=node --target=node16 --bundle --outfile=dist/outfile.cjs

  dist/outfile.cjs  439.1kb

⚡ Done in 246ms
Enter fullscreen mode Exit fullscreen mode

Next up we install the pkg library and also add a npm script to execute it.

$ npm install --save-dev pkg
Enter fullscreen mode Exit fullscreen mode

package.json: (don't forget the comma after the compile script)

{
  "name": "my-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "compile": "esbuild src/index.js --platform=node --target=node16 --bundle --outfile=dist/outfile.cjs",
+   "package": "pkg dist/outfile.cjs --targets node16 --output dist/my-cli --debug"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "dependencies": {
    "zx": "^6.0.1"
  },
  "devDependencies": {
    "esbuild": "^0.14.27",
    "pkg": "^5.5.2"
  }
}
Enter fullscreen mode Exit fullscreen mode

The package script is executing pkg and uses dist/outfile.cjs as entrypoint. Also it configures that we want to have Node.js 16 as a target runtime and it should output a file dist/my-cli.

Now if you execute the package script you should hopefully see a binary in your dist folder.

$ npm run package
Enter fullscreen mode Exit fullscreen mode

This outputs a lot of debugging information which you can ignore for now. But if you have problems you can see there couple of helpful infos to diagnose the issue.

Please keep in mind, that this will output a binary which is only compatible with the same operating system and processor architecture as your developer machine. So if you execute the npm run package command on a Windows machine with x64 processor, the binary will not work on a Linux or macOS machine. To work there, you would either need to change the package command to include more targets (please see the documentation for that) or execute the package command on the same OS/processor architecture.

Now in your file explorer you can already see a file dist/my-cli or dist/my-cli.exe depending on the OS you are working with. And this file is executable in the shell for example with this call:

$ ./dist/my-cli
Enter fullscreen mode Exit fullscreen mode

And if everything worked you should see the current date and time 🥳

This binary file can now be used without any runtime (as long as you execute it on the same OS/processor architecture) Great!

Have fun writing great scripts which are runtime agnostic!

Photo by Markus Spiske on Unsplash

Top comments (0)