Go to main contentGo to page footer

BazaarJS: the Node.js task runner diaspora

This is the first episode of our new BazaarJS series, dedicated to exploring the world of SPAs (single-page applications)... today we'll be talking about build tools and task runners!
Estimate reading 6 minutes

As we stressed in our previous post, the release of Node.js — as a Javascript runtime-environment capable of operating independently from the browser — was undoubtedly a crucial turning point for Javascript. Node.js was certainly not the first experiment of its kind (Rhino was released as long ago as 1997), but it was undoubtedly the first such attempt to enjoy a successful take-off and to be able to make a real difference.

Rake, Make, Gradle, Ant... there isn't a programming language that doesn't come equipped with its own standard build tool, and Node.js was the environment that made it possible to create such instruments for Javascript. This was an important milestone; one which finally made it possible to create processing tasks for front-end Javascript capable of carrying out levels of analysis and introspection that were impossible using previously existing technology.

This being a bazaar, Javascript obviously offers an inordinate number of different tasks runners to choose from. The first of these, Jake, was created in 2010, shortly after the first release of Node.js itself. Jake was swiftly followed by Grunt, then Brunch, Mimosa, Gulp, Broccoli… Absurd? Crazy? You betcha.

In order to maintain a minimum level of coherence, we will limit ourselves to examining three of these competitors that appear, at this moment in time, to be enjoying the highest levels of popularity.

Analysis

##### Grunt

* **Homepage:** http://gruntjs.com/
* **Number of available tasks:** 3.989
* **Release date:** Settembre 2011
* **Github stars:** ★ 8.929
##### Gulp

* **Homepage:** http://gulpjs.com
* **Number of available tasks:** 1.136
* **Release date:** Luglio 2013
* **Github stars:** ★ 10.657
##### Broccoli

* **Homepage:** https://github.com/broccolijs/broccoli
* **Number of available tasks:** ~200
* **Release date:** Maggio 2013
* **Github stars:** ★ 1.787

As can be seen from the stats, Grunt was the first "second generation" task runner to arrive, and for this reason it comes equipped with the largest database of plugins. Gulp is, however, the most popular and widely supported system currently available. We mention Broccoli because of its relative popularity but — despite its being developed in parallel with Gulp — the system has so far failed to shine. The number of tasks Broccoli has available is an order of magnitude smaller those of its two competitors; a shortfall that has led to its being left far behind in the race for supremacy.

The two survivors, Grunt and Gulp, are starkly different from one another both in their philosophies and in their method of writing tasks.

Grunt's motto is "configuration over code"... this is what a classic Gruntfile.js (the equivalent of a Ruby Rakefile) written to concatenate and compress a series of Javascript files looks like:

// Gruntfile.js
module.exports = function(grunt) {
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');

  grunt.initConfig({
    concat: {
      scripts: {
        src: ['src/**/*.js'],
        dest: 'temp/all.js'
      }
    },
    uglify: {
      scripts: {
        src: 'temp/all.js',
        dest: 'build/all.js'
      }
    },
  });

  grunt.registerTask('default', ['concat:scripts', 'uglify:scripts']);
};

As we can see, there are two tasks: the first concatenates the source files and writes them to a temporary file; the second takes the temporary file, compresses it and saves the result to a destination file.

Just from looking at this simple example we can see that setting up the tasks didn't require us to write a single line of code: all that we had to do was supply the necessary configuration parameters for the two tasks using hashes, according to the method grunt.initConfig().

Now let's compare this with the equivalent gulpfile.js:

// gulpfile.js
var gulp   = require('gulp');
var uglify = require('gulp-uglify');
var concat = require('gulp-concat');

gulp.task('default', function() {
  return gulp
         .src('src/**/*.js')
         .pipe(concat('all.js'))
         .pipe(uglify())
         .pipe(gulp.dest('build/'));
});

This is admittedly a rather simplistic example, but it does allow us to distinguish some fundamental differences between the two approaches:

  • Gulp uses streams, one of Node.js's strengths, to its advantage. In contrast to Grunt, instead of generating temporary files at every step, the various processes and files are connected to one another using pipes. This leads to some obvious improvements in performance, but perhaps the most important of these is that we no longer have to worry about assigning names to temporary files, nor about deleting them once tasks have been terminated. One less problem which helps to makes the code easier to understand.

  • Gulp tasks aren't configured, they're programmed. We import Gulp plugins using the require() Node.js idiom and the "body" of the task is expressed in Javascript code. Unlike in Grunt, if we want to add an if statement to determine whether a particular task will be added to the pipe during runtime, we're perfectly free to do so. And if we want to reuse part of the pipeline in a second task? We can just go ahead and refactorize the code using the Extract Method refactoring pattern.

  • Using Grunt, as the number of tasks increases it becomes increasingly complicated to follow the logic of concatenation of the various sub-tasks. This is an extremely unfortunate consequence of Grunt's choice to use hash expressions for configuring and regrouping plug-ins, rather than for tasks. This choice completely overturns the principle of locality, one which forces us to constantly jump from one part of a file to another in order to reconstruct a task in its entirety.

The choice

Having read the above analysis, it probably won't come as much of a surprise to learn which competitor we have chosen: Gulp[^bemo].

[^bemo]: BEMO, our frontend project-starter includes, unfortunately, a Grunt plugin... youth error! We will shortly switch to an equivalent Gulp plugin; meanwhile it is possible to use gulp-grunt to let them speak together.

We have used both of these task runners on different projects and we must admit that Grunt has proven itself to be extremely solid. However, it has also required projects of average complexity to have a hash configuration of more that 500 lines. The number of temporary files this generates in the intermediate steps and the jumps between context that it obliges us to make means that these projects are all but impossible to manage.

Grunt plug-ins that make it possible to somewhat improve this situation by splitting the configuration across multiple files do exist, and we have used them to our advantage in the past: load-grunt-configs is a good example. That said, the problem of plug-in regrouping in Grunt remains an unresolved issue.

Working on similar projects, Gulp has made it possible for us to dramatically reduce the length and complexity of our code, and so made that code easier to maintain over time.

Coming next: module loaders and package managers :)

In this article on task runners, we've only been able to scratch the surface of the magical world of single page applications... Next week we'll go further into their intricacies with a rundown of the principal mechanisms for module loading (AMD and CommonJS) and their related libraries and package managers (npm and Bower).

Follow us on Twitter or subscribe to our RSS feed to keep up-to-date!

Did you find this interesting?Know us better