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)</code></pre></p><p>But most importantly, it's super easy to run command line programs:<pre><code>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:
- Installing and building dependencies where no pre-build package was available
- Starting a test server, running cypress tests, and shutting the server down
- Running multiple rollup and webpack builds, only some of which can run simultaneously
Don't litter your package.json
scripts with long lines of chained console commands. Use zx.