Build automation with JavaScript and zx

zx is an open-source tool by google for creating server-side scripts with JavaScript. In this post I'll talk a bit about what it does, and how and why I use it in several projects to automate builds.

Why JavaScript?

JS may not be the most popular programming language, but it is the most used. And if you're building a web-app, you're probably using JS anyway — might as well write your build scripts in the same language.

Plus, JS is cross-platform. I develop web apps on macOS and Windows, and use Linux for continuous integration. My zx scripts work everywhere.

How-To

To get started, create a JavaScript file with a .mjs extension. .mjs stands for modular JavaScript — using this extension lets you use import instead of require. (If your package.json contains "type": "module", you could also use the .js extension. Though personally, I always use .mjs to keep things simple.)

The first line of a zx script is:

#!/usr/bin/env zx

This tells your environment to run the script with zx, although you don't really need it if you call zx explicitly. More importantly, it tells other developers that they're looking at a zx script. Someone who's never used the tool before might search for this line and find the zx github repo among the first results.

After that, just write some JavaScript. All the good stuff works:

import { promisify } from 'util' // Import statements!
import { randomInt } from 'crypto'
const number = await promisify(randomInt)(0, 10) // Top-level await!!
console.log(number)

zx imports some libraries for you:

await fs.ensureDir('temp')  // fs-extra (https://www.npmjs.com/package/fs-extra)
await console.log(chalk.green('directory temp created'))  // chalk (https://www.npmjs.com/package/chalk)
const res = await fetch('https://jfhr.me')  // node-fetch (https://www.npmjs.com/package/node-fetch)

But most importantly, it's super easy to run command line programs:

await $`npm install`
await $`npm run test`

$ is a shortcut for child_process.spawn(). It returns a promise that resolves when the process completes. If you want to run a process in background, simply don't await it:

// run the server in the background
const server = $`npm run server`
// run the test script
await $`npm run test`
// send a terminate signal to the server process...
server.child.kill('SIGINT')
// ... and wait for it to shut down
await server

If your process ends with a non-zero exit code, zx will throw an error by default. But you can suppress it using nothrow:

nothrow($`touch temp/file1.txt`)

This is just a teaser - you can learn more in the official zx README.

If you want to use zx for build automation, I recommend installing it as a devDependency and adding the scripts to your package.json:

npm install -D zx

package.json

{
  "scripts": {
    "build": "zx build.mjs"
  }
  // ...
}

That way, anyone using your package can simply run

npm install
npm run build

without needing to know about zx.

What for?

I've used zx scripts in several projects to automate such stuff as:

Don't litter your package.json scripts with long lines of chained console commands. Use zx.