On abandoning Gulp

Keith Cirkel recently wrote that we should stop using Grunt & Gulp for building frontend code and just use NPM scripts instead. This post is about my experience converting an existing app to use this pattern.

tl;dr: win. People should listen to Keith.

(Cartoon modified from Goober and Cindy, © Daniel Barton, used with permission)

The longer version

For the last couple of months I’ve been working on a personal project called Tamarind, a medium size (10K lines +) CoffeeScript application. I’ve been through a few build systems. I started with Brunch, which is lovely and very fast, but in the end I just couldn’t live with it’s limitations. I moved to Gulp, which is more flexible but a huge step backwards in usability.

I’ve now switched to Keith’s method and am very happy with the results.

The problem

To paraphrase Keith’s article, most frontend build tools – CoffeeScript, browserify, uglify, karma etc – already have a well designed command line API. By using Grunt or Gulp, you’re throwing that away and replacing it with a needlessly complex and often unintuitive JavaScript API.

For example, here’s the command line code to compile and minify some JavaScript for a web application:

browserify main.js --debug --outfile main-debug.js
uglifyjs main-debug.js --compress --source-map main.js.map --output main.js

Pretty readable, and if you don’t understand what any option does then typing browserify --help will print out a manual page.

Here’s the same thing in Gulp:

browserify   = require 'browserify'
gulp         = require 'gulp'
rename       = require 'gulp-rename'
sourcemaps   = require 'gulp-sourcemaps'
transform    = require 'vinyl-transform'
uglify       = require 'gulp-uglify'
# based on: https://github.com/gulpjs/gulp/blob/master/docs/recipes/browserify-uglify-sourcemap.md
gulp.task 'build-scripts', ->
  browserified = transform((filename) ->
    return browserify(entries: filename).bundle()
    .pipe(rename 'main-debug.js')
    .pipe(gulp.dest BUILD_DIR)
    .pipe(sourcemaps.init(loadMaps: true))
    .pipe(rename 'main.js')
    .pipe(sourcemaps.write './')
    .pipe(gulp.dest BUILD_DIR)

Everything in the above code is there for a reason, and once you’ve been using Gulp for a while it all starts to make sense. But, damn it’s ugly. Moreover it’s brittle – the correct behaviour relies on the exact ordering of lines.

The solution

Keith wrote a follow-up post giving more detail on implementation, but it all boils down to: use the command line API for each build tool, and wrap up your commands using npm’s scripts feature.

Here’s the full scripts code for Tamarind’s package.json, and it handles building, testing and running a live-reloading development server. It replaces a 140 line Gulp script and has an extra feature – documentation generation with Codo.

  "scripts": {
    "clean": "rm -rf node_modules build; mkdir build; npm install",
    "build": "mkdir -p build; ./node_modules/.bin/browserify app/tamarind/Tamarind.coffee --debug --standalone Tamarind --outfile build/tamarind-debug.js",
    "minify": "echo 'cd build && ../node_modules/.bin/uglifyjs tamarind-debug.js --compress --source-map tamarind.js.map --output tamarind.js' | sh",
    "doc": "node_modules/codo/bin/codo app/**/*.coffee",
    "copy-assets": "cp -r app/assets/* ./build/",
    "dist:noclean": "npm run build && npm run minify && npm run doc && npm run copy-assets && rm -rf dist && cp -r build dist",
    "dist": "npm run clean && npm run dist:noclean",
    "start": "./node_modules/.bin/beefy app/tamarind/Tamarind.coffee:tamarind.js --live --index app/assets/test/Tamarind-demo.html -- --standalone Tamarind",
    "test": "echo 'NOTE! FOR LIVE TESTS, RUN `npm install -g karma-cli` THEN `karma start`'; node_modules/.bin/karma start --single-run"

Note how the dist tasks use npm run to delegate to other tasks. Simple and effective. The whole thing took about an hour to write including testing.


Gulp uses JavaScript code for configuration, which means that it has complete flexibility and you can use modules to manage complexity. There are no doubt situations where this flexibility is desirable. For example, suppose you have many projects with the same build process. You could create a JavaScript module to configure the build and re-use it between your projects. Then your Gulpfile would look like this:

module.exports = require("my-shared-build-process")

But before using Gulp, ask yourself if all that complexity and flexibility is really required.

8 thoughts on “On abandoning Gulp”

    1. ES6 modules (and the JSPM/system.js projects that will smooth their adoption) are going to be a great boon to the JS community and solve lots of problems with the current state of affairs.

      Still, in a post-ES6 we’ll still need build systems. Your JS in a complex app will still need to be minified and concatenated, and the build handles other tasks like compiling LESS to CSS and generating regular versions of retina images.


      Bernie :o)

  1. You may have discovered this by now but in package.json you can omit the `./node_modules/.bin/` when starting executables that were installed locally with npm.

    I still use gulp, but I don’t want to force others to install gulp globally, so I do this:
    “dependencies”: {
    “gulp”: “latest”
    “scripts”: {
    “start”: “gulp build”

    This will also let me switch to the next shinier gulp replacement without changing my build instructions at all. npm scripts are great, but it doesn’t have to be all or nothing.

  2. It does makes certain sense in simplifying things, however its rather limiting in flexibility and speed.
    a) Can your npm example do watch on files or folders and process only those files that had changed?
    Moreover, in watch mode, Gulp code/tasks are already compiled and ready to run, fast. Npm scripts however runs a new npm each time and is slow.
    b) Gulp pipes uses in-memory node stream which is much faster than disk io.
    c) Gulp is basically javascript code, which gives it unlimited flexibility, including logical operations and running of shell scripts. Npm script is basically just console shell scripts alone.

    1. a) Yes. The example in the post doesn’t use it, but 6 months later I’m still using package scripts, and I’m doing watch mode perfectly happily. I use the watch mode built into many npm packages, e.g. with the –watch flag on the typescript compiler. If there’s no built in watch mode I use wr. I serve from live-server to get browser refresh.

      b) I don’t notice my live builds being slow. One second from changing a file to seeing the changes reflect in the browser.

      c) Yup, glorious, simple shell scripts. Love ’em.

  3. You may continue to love you shell scripts. But to state that the posted package.json is more readable than a Gulp script or in any kind readable at all is just pure nonsense (just look at the highlighting…).

    1. To be clear, I didn’t post the Gulp file that the package.json replaced, but trust me it was hideous! If anything, syntax highlighting in Gulp files just serves to highlight all that unnecessary noise and implementation details, calls to .pipe(), functions etc. And this is just talking about readability – writability of Gulp files is even worse, for example getting calls to .pipe() in the wrong order will totally break things, so you need to understand what’s going on under the hood. Command line interfaces are more of a black box, and generally you can mix arguments around and not break things.

      So yes I’d argue that the package.json was a big improvement, but you’re right that it could be further improved. Embedding shell scripts in JSON isn’t exactly pretty.

Comments are closed.