Keith Cirkel

Software Cyber Shepherd

Why we should stop using Grunt & Gulp

If you're building a modern JavaScript application or website, there's a good chance you've seen the JavaScript Build Tool Landscape. The heavyweights of this world are Grunt and Gulp, although many, many others exist too. Grunt receives a whopping 30,000 downloads most days, and Gulp has a respectable 15,000 daily downloads. They must be doing something right, right? Build systems seem to be the tool du jour of the JavaScript community. I've used Gulp and Grunt in several projects (mostly closed-source projects, but some open-source, such as Tempus).

For me, especially more recently, I feel like many of these tools are solving the problem badly. All of them do, to varying degrees. Reading between the lines, we can see tools like Gulp attempting to solve the problems of Grunt, and Broccoli attempting to solve the problems of Gulp - covering up the inadequecies of the previous tool while also surfacing their own. Let me cover some of the reasons why I think these tools are bad choices.

Bloat

All of these task runners (or build systems if you want to call them that) try to abstract some kind of task paradigm away into their own siloed incantations. Grunt uses configuration files and plugins to help you, through the centralised "Gruntfile" which declares your settings, and as veteran users of Grunt will attest to, the configuration is far from terse. You can install plugins, like grunt-contrib-jshint which let you use your favourite tools to work with your code. "What does a configuration look like?", you might ask. Well, if we were to have a Gruntfile that simply ran jshint, it would look a little something like this:

module.exports = function(grunt) {
grunt.initConfig({
jshint: {
files: ['**.js'],
options: JSON.parse(require('fs').readFileSync('./.jshinrc'))
}
});
grunt.loadNpmTasks('grunt-contrib-jshint');
};

And then of course, you'd need to install the grunt and grunt-contrib-jshint dependencies. All of this is meant to abstract away the difficulties of running jshint **.js in your terminal. Of course, to get this Grunt configuration to work you'd still have to run grunt jshint in the terminal, which as it turns out is no shorter than jshint **.js. This might seem a little contrived, but looking at Grunt's website, you can see it advertises compatibility with lots of your favourite plugins, such as CoffeeScript, Handlebars, Jade and more. All of those tools already install binaries on your system (here's CoffeeScript's, Handlebars' and Jades'), and the wonderful irony of it all, is that plugins bundle the dependency with them, so grunt-contrib-jshint depends on jshint which downloads the binary to your system anyway!

I'm not picking on Grunt intentionally here, Gulp is just as guilty of this kind of bloat. With Gulp I still need gulp-jshint and a Gulpfile.js on my filesystem, with the following code:

var jshint = require('gulp-jshint');
var gulp = require('gulp');
gulp.task('jshint', function() {
return gulp.src('**.js')
.pipe(jshint())
.pipe(jshint.reporter('default'));
});

Gulp's version only clocks in at 7 lines of code, but still requires 2 dependencies (gulp itself, and gulp-jshint) and I still need to run gulp jshint (which is only 1 keypress less than jshint **.js). Gulp isn't the only culprit either; Jake, Broccoli, Brunch and Mimosa all require plugins to install as well, which means when using any of these task runners, you're essentially just installing 1 more dependency (the task runner) than if you had no task runner, and just used each of the projects own binaries.

All this talk of plugins leads me nicely to my next point…

Relying on plugins

None of these build tools work without plugins. Just found an awesome new tool which will revolutionise your build process? Great! Now just wait for someone to write a wrapper for Grunt/Gulp/Brocolli, or write it yourself. Rather than just learning the command line for the tool you want to use, you now have to learn its programatic API, plus the API for your build tool. Sindre Sorhus can only write so many of these tools for you.

But this is where it can get crazy. You could find that for certain tasks you need to use Grunt, and for others you need to use Gulp or Brunch, so enter the ridiculous world of using build tools to manage your build tools, with such great hits as grunt-brunch and gulp-grunt, which, quoting from the readme says:

What if your favorite grunt plugin isn't available for gulp yet? Don't fret, there is nothing to worry about! Why don't you just hook in your grunt configuration?

So instead of relying on one build tool's community to consider your favourite new linter notable enough to make a plugin, you can just pile on build tools into your project until one of them hits the mark.

Separate pain in updating

With all of these plugins and tools, things can start to become a pain to update. When I was using Grunt fairly extensively (around the time of 0.3 and 0.4), Grunt undertook a fairly large refactoring, and as such, any Grunt 0.3 config no longer worked with Grunt 0.4, and so, users had to migrate their configs. Plugin authors also had to migrate their plugins. The project I was working on at the time had some build scripts that were badly built by inexperienced Node.js devs - using a custom built task runner - so I decided to switch it all to Grunt. Just around the time I finished, Grunt 0.4 came out and I spent another 2 weeks waiting for each plugin to migrate to 0.4, and slowly updating our Gruntfile. Granted, Grunt has matured a lot since those days - the API tends to be relatively stable by now. I was just unlucky.

I've also spoken to developers who have migrated from Grunt to the-next-best-thing (e.g. Gulp), because of the touted amazing new benefits, proudly proclaiming that after 2 weeks work their 30 second build time went down to 3 seconds. Of course, this conviniently allows me to segway to my next point:

False Promises

I was using Grunt fairly extensively when Gulp was released. Gulp really signalled the war cry to Grunt - proudly proclaiming it doesn't do any of that naff configuration stuff, everything is written imperitively, with code! Also, everything was streaming and asynchronous - nothing like Grunt, just grunting along all synchronously. So I tried it. I used the imperative API to load up a bunch of file streams and pass them to gulp-uglify, at which point I got this message:

[gulp] Error in plugin 'gulp-uglify': Streaming not supported

Turns out one of the main purported benefits of Gulp - its streaming capabilities - doesn't actually work with all of the available plugins. I tried several more plugins (JSHint also didn't work), but at the time many of them didn't support streaming. Gulp has plugins to mitigate this problem - but it just feels like piling more tools on top of a badly solved problem.

Gulp also promises to have a really simple API - "just learn 5 commands and you know it". Unfortunately the reality is far from the truth. Those 5 API commands hide the complexity of Gulps file management utility - VinylFS - and its task runner framework - Orchestrator. Both of these (at the time I used them) had virtually no documentation, and so trying to work out how to use these resulting in looking through code.

Bad behaviours

Speaking of my experiences with Gulp, when I finally got JSHint to work, and generated a couple of errors to make it fail, I noticed something. It didn't actually fail. The same was true for Gulp's Mocha plugin. When given an Error, Gulp - at the time - would exit with a 0 exit code, meaning putting this into a continuous integration environment such as Travis or Jenkins it would show a bunch of failures, then pass. The CI just sees log output and a friendly exit code and assumes everything was A-OK. Overcoming these kinds of hurdles when all you want to do is set up some tooling can be quite a souring experience. Gulp actually has plugins to make it exit properly.

Speaking of bad behaviours - reading into how to get gulp working with browserify - apparently Gulp blacklisted the gulp-browserify plugin from its registry because it didn't fit its world view (it uses streams, not Gulp's vinyl-fs). A good post goes into detail here.

The solution

It would be lazy of me to simply complain about these tools, without actually prividing a real alternative to them. And there definitely is one (in my opinion). There is a tool which can run build scripts, carry configuration values, is streaming, has an incredibly simple API, and comes free with every Node.js installation: npm.

Whilst building Hive I laid the gauntlet down and said to my (very smart) colleagues:

We're not going to use Grunt. We're going to use NPM to manage our build scripts. If it becomes too complex to manage, then we'll switch to Grunt and I won't speak a single word of complaint.

We built the project from the ground up, all the way to release, all the while using NPM without a hitch. We never made a switch to Grunt - and no one complained. We had CSS Preprocessors, Browserify, Karma, Mocha, JSHint, Srcy, WD all running from our npm scripts object, in about 15 lines of code. Compare this to my job prior to that where we had hundreds of lines of Grunt config and tonnes of plugins including custom ones to do a similar set of tasks.

npm's scripts object lives inside package.json, meaning there is no new files to add to your project. The object has properties, which are the task names, and values which are the commands. Its so unbelievably simple that it beggars belief why we ever needed other build tools in the first place. Let's take our contrived JSHint example and port it to NPM:

"devDependencies": {
"jshint": "latest",
},
"scripts": {
"lint": "jshint **.js"
}

That's it! You introduce 1 extra dependency, and 1 line of code, per tool you wish to use. Then just call npm run lint and voila! A bad result will provide a non-zero exit code, and the results that come out are streamable! You can even redirect logged contents to file, so for example to produce a CheckStyle report in JSHint is as simple as:

"scripts": {
"lint": "jshint **.js --reporter checkstyle > checkstyle.xml"
}

What if you want to have both checkstyle reports (for ci) and a developer reports for developer usage? There are no rules for the task names you have, other than your own conventions - so we could steal a convention from Grunt, and namespace our tasks with :, like so:

"scripts": {
"lint": "jshint **.js",
"lint:checkstyle": "npm run lint -- --reporter checkstyle > checkstyle.xml"
}

In a follow up post, I've detailed the ins and outs of npm and how to use it effectively, showing how you can have an extendable config, multiple tasks, streaming tasks, and more.

Summary

I realise that someone, somewhere will have a valid use-case for build tools like Grunt and Gulp. I believe, however, that npm can handle 99% of use-cases elegantly, with less code, less maintainence, and less overhead than these tools. Since working with Grunt and Gulp and trying to overcome the problems they have, I can say with confidence that npm is an excellent alternative. On your next project, I encourage you to keep things simple - start with npm as your build tool, and switch only when you see your package.json becoming unweildy. I think the results might just give you a pleasant surprise.

Disagree? Totally agree? Fancy talking about something else? Feel free to tweet me - I'm @keithamus on Twitter.

Oh, and if you liked this post, I did a follow up on how to use npm as a build tool.