Build Automation with Vanilla JavaScript

In any software project, there are tones of tedious tasks that can be easily automated — building, scaffolding, testing, publishing etc. I guess, if you make a pause, you can think of a few such tasks that you’re still doing manually ¯\_(ツ)_/¯. Ideally, you want to automate everything in a simple way possible and do what you really like — developing software like a real ninja, right?

Traditionally this sort of tasks was being handled by Bash scripting, Make files on macOS/Linux or CMD/PowerShell scripting on Windows. But if you’re building a Node.js/JavaScript app, wouldn’t it be better to use vanilla JavaScript for automation scripts as well? This way you would shrink the tech stack used in your project, making it more friendly for other developers in your team (cross-latform, universal). Also keep in mind, that the modern syntax of JavaScript (ES2017+) is very good and real fun to work with. After-all, JavaScript is super easy to debug.

Surely, if your automation tasks are small enough, something like rm -rf build/* && babel --out-dir build src , you can just put those scripts into package.json file in the root of your project’s source tree an execute them via yarn run <command> (see yarn run). But realistically, do you think that most of your tasks will be that small? Do you prefer using as little terminal sessions opened at any given time running your automation commands (preferably just one)? Or, it’s not an issue for you? As en example, you may want to launch Browsersync with Webpack middleware but only after this middleware was instantiated and completed the initial round of compilation of the source files. Good luck automating that with CLI commands :)

Yes, you could use Gulp. It work’s great for stream transforms but do you really want to use it for all your automation needs? Is this the right tool for the job? For example, in most of my projects there are very little automation tasks related to piping and transforming streams. Also the benefits of minimalistic (vanilla JS) scripts clearly outweighs the pros of using a 3rd party task runner (such as Gulp) for me an my team.

Let’s see how it looks like on practice! In your project, you create scripts folder with a bunch of .js files, where each file represents an automation task. For example: scripts/build.js, scripts/scaffold.js, scripts/deploy.js etc. Each of these files is written in vanilla JavaScript and exports a single function, for example (scripts/hello.js):

module.exports = function hello() {
console.log('Hello world!');
};

You can execute it by running node scripts/hello (assuming you have Node.js installed). WAIT! The are a couple problems with the example above — the exported function is not fired up when you run the script with node and it doesn’t log completion time in ms. Also you need to make sure that these scripts can be easily composed to build complex automation tasks from smaller functions.

All these issues are easily solved by writing a custom task runner, somewhat similar to Gulp but without streams, in ~10 lines of code (scripts/task.js):

All you need to do now, is decorating your scripts with task(<name>, ...) higher-order function, for example (scripts/hello.js):

const task = require('./task');
module.exports = task('hello', () => {
setTimeout(() => {
console.log('Hello world!');
}, 3000);
});

Now when you run your script (node scripts/hello ) it should print this:

Starting 'hello'...
Hello world!
Finished 'hello' in 3000ms

Also, now you can easily chain your tasks, for example:

const task = require('./task');
const build = require('./build');
const test = require('./test');
module.exports = task('publish', () => Promise.resolve()
.then(build)
.then(test)
.then(() => {
// TODO: Build Docker image and push it to a private registry
})
);

You can also use async/await syntax right now (with Node.js 7) by running your scripts with --harmony flag, e.g. node --harmony scripts/publish:

const task = require('./task');
const build = require('./build');
const test = require('./test');
module.exports = task('publish', async () => {
await build();
await test();
await ...;
});

Also you can pass arguments into your “task” functions, for example:

module.exports = task('start', () =>
build({ watch: true, onComplete: ... }));

For a working example, please visit Node.js API Starter Kit on GitHub.

Summary

  • Task automation is easy, easy to debug and fun to do with vanilla JavaScript
  • It’s great when all of your automation tasks are unified under the same API / interface (as opposed to mixing different approaches and tools)
  • Find the right balance between minimizing the number of external dependencies used in your project and depending on too many external libraries