This post will cover the following topics:
- The history and philosophy behind Node.js
- Why task runners were developed for Node.js
- Different approaches taken by popular task runners
- How bash may be used as a simpler alternative
A brief history of Node.js
Node.js is heralded as being fast, approachable, and perhaps most alluring of all, simple. It began syphoning users from other platforms. One such platform is PHP — a language created to generate dynamic websites. PHP has perhaps thousands of global functions available at any time and requires a stack of configuration files.
Node.js allowed developers to migrate to the platform and get a fresh start. Being so new it hadn’t yet developed the “batteries included” frameworks of other languages. One of the guiding principles of Node.js is to keep the core simple. You won’t find built-in tools for connecting to MySQL, generating a UUID, or calculating Levenshtein distance.
The rise of the task runner
Two popular Node.js task runners came to the rescue. The first is Grunt, with a first commit made in September 2011. This tool takes an imperative approach to configuring different tasks, building out deeply nested objects and calling a few methods. The second one is Gulp, having an initial commit in July, 2013. This tool takes a different approach, more functional in nature, piping the output of one function into the input of another function, streaming the results around.
Let’s consider a simple web application we’d like to mockup using a subset of these technologies. This application depends on multiple SASS and JS files. We’d like to convert the SASS files into CSS, concatenating the result. For sake of brevity, we’ll also simply concatenate the JS files together, and assume the module pattern, instead of using CommonJS require statements. Let’s see how such a configuration might look using these different task runners:
This approach requires the following modules be installed:
grunt-contrib-clean. With this approach, we can run
grunt script, or
grunt build to do the work of both.
The equivalent Gulp version of the previous Gulp example is as follows. This requires we have
node-sass installed. With this approach, we can run
gulp script, or
gulp build to do the work of both.
As you can see, the Gulp example is a little more terse than the Grunt example.
Philosophically, the two tools take different approaches to implementing runnable tasks, but ultimately they allow you to do similar things. Again, Grunt was introduced before Gulp. They’ve both have had comparable popularity throughout their lifespans:
Both projects are highly modular, allowing developers to create specialized plugins. These plugins allow an external tool, such as
eslint or sass or
browserify, to easily integrate into the task runner. We actually have an example of this in the code we looked at earlier: the popular SASS tool has both a
grunt-contrib-sass module, and a
gulp-sass module available.
The first commit to Webpack happened in March 2012, between the first commits to Grunt and Gulp. As of this article being written, it is still under very active development and its last contribution occurred a few hours ago. Whereas Grunt and Gulp aide in performing many types of generic tasks, Webpack is specifically more interested in building frontend assets.
Webpack can also be configured in a manner similar to Grunt and Gulp using a file called
webpack.config.js. It is also highly modular and we can achieve similar results using plugins like
Task runner alternatives
For the most complex of build systems, it makes total sense to use a Node.js Task Runner. There’s a tipping point where the build process can get so complex that maintaining it in a language other than the one the application is written in just doesn’t make sense. However, for many projects, these Task Runners end up being overkill. They are an additional tool that we need to add to a project and keep up to date. The complexity of Task Runners is easy to overlook when they’re so readily available via
The approach used by Gulp of taking outputs from one operation and piping them into another operation should sound familiar. The same piping system is also available to us via the command line, which we can automate by use of bash scripts. Such scripting features are already available to users of macOS and Linux computers (WSL can help with Windows).
We can use the following three bash scripts to achieve what our Grunt and Gulp examples are doing:
When we use this approach we’ll only need a 2.5MB
sass binary (executable). The time it takes to perform the entire build operation is also lessened: on my machine the operation only takes 25ms. This means we’re using about ~1/12 the disk space running 10x as fast. The difference will likely be even higher with more complex build steps.
They can even be in-lined inside of your package.json file. Then commands can be executed via
npm run style,
npm run script, and
npm run build.
Another shortcoming is that bash scripts require that some sort of executable is available for each operation we want to incorporate. Luckily for us they usually are. Browserify, a tool for resolving CommonJS requires and concatenating output, offers an executable. Babel, the go-to transpiler, also offers an executable. Sass, Less, Coffeescript, JSX: each of these tools has an executable available. If one isn’t available we can write it ourselves, however, once we reach that point we might want to consider using a task runner.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.