JavaScript Task Runners

Feb 21, 2020 • ☕️ 6 min read

Task runners are tools to orchestrate those kinds of tasks in an efficient way, the workflow made easy with the ecosystem of plugins to automate all kinds of tasks you can imagine, the speed can be optimized behind the scene to leverage multi-threads, and can be used as standalone or integrated into a more complicated pipeline.

Task is the heart of task runners, often available as plugin packages, most popular task runners will provide some official plugins and allow you to create custom plugins of your own. Think of using task runners when you’re dealing with many repetitive mundane painful tasks like following:

- Compiling preprocessor stylesheets (Less, Sass, CSS) into raw CSS
- Run Autoprefixer on the new CSS to catch any vendor prefixes we may have missed
- Optimize any JPG, GIF, or PNG to make the file size smaller
- Transpile, minify, uglify, concatenate JavaScript files
- Update our CSS banner with new timestamp information
- Watch files for changes and rerun tasks as needed
- Run BrowserSync for testing in multiple browsers and devices at once

This post will focus only on truly defined task runners (Grunt, Gulp, Brunch, etc.) and skip those can behave similar like bash scripts, npm scripts, makefile or popular Webpack.

Grunt

Grunt is an open-source JavaScript task runner created in 2012, known for highly customizable extensive configuration, available as a command-line tool, and has thousands of plugins.

This tool takes an imperative approach to configuring different tasks, building out deeply nested objects and calling a few methods. Getting started with Grunt seems pretty straightforward when Grunt and Grunt plugins are installed and managed via npm.

Step 1 — install grunt-cli which is an cli tool to run the version of Grunt which has been installed next to a Gruntfile, this allows multiple versions of Grunt to be installed on the same machine simultaneously.

npm install -g grunt-cli

Step 2 — install grunt and grunt plugins in current working directory as dev dependencies

//package.json
{
  "name": "awesome-project",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-contrib-jshint": "~0.10.0",
    "grunt-contrib-nodeunit": "~0.4.1",
    "grunt-contrib-uglify": "~0.5.0"
  }
}

Step 3 — configure Grunt via Gruntfile.js, which should exist at the root of your project alongside package.json. Tasks are grunt’s bread and butter, we can use default tasks provided by Grunt (“grunt-contrib” prefixed tasks are maintained by the Grunt team). Let’s look at a sample Gruntfile that covers usual needs of a simple project:

// Gruntfile.js
module.exports = function (grunt) {
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      options: {
        separator: ';'
      },
      dist: {
        src: ['src/**/*.js'],
        dest: 'dist/<%= pkg.name %>.js'
      }
    },
    uglify: {
      options: {
        banner:
          '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
      },
      dist: {
        files: {
          'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
        }
      }
    },
    qunit: {
      files: ['test/**/*.html']
    },
    jshint: {
      files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
      options: {
        // options here to override JSHint defaults
        globals: {
          jQuery: true,
          console: true,
          module: true,
          document: true
        }
      }
    },
    watch: {
      files: ['<%= jshint.files %>'],
      tasks: ['jshint', 'qunit']
    }
  })

  grunt.loadNpmTasks('grunt-contrib-uglify')
  grunt.loadNpmTasks('grunt-contrib-jshint')
  grunt.loadNpmTasks('grunt-contrib-qunit')
  grunt.loadNpmTasks('grunt-contrib-watch')
  grunt.loadNpmTasks('grunt-contrib-concat')

  grunt.registerTask('test', ['jshint', 'qunit'])
  grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify'])
}

Optional — if those default grunt plugins can’t fullfil your needs then you can create custom tasks which can be both asynchronous or asynchronous:

grunt.registerTask('asyncTask', 'My asynchronous task.', function () {
  var done = this.async()
  doSomethingAsync(done)
})

At the time of writing, Grunt has 6250 plugins which both officially maintained by Grunt team and community.

Gulp

Gulp is a free JavaScript task runner, known for flexible code over configuration, faster than Grunt, available as a command-line tool, has more than 4000 plugins.

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. The idea behind code over configuration is that code is much more expressive and flexible than the modification of endless config files.

Gulp’s use of Node.js streams is the main reason it’s faster than Grunt. Using streams means that, instead of using the file system as the “database” for file transformations, Gulp uses in-memory transformations.

Getting started with Gulp is very similar to Grunt:

Step 1 — install the gulp command line utility

npm install --global gulp-cli

Step 2 — install the gulp and plugin packages in your devDependencies

//package.json
{
  "name": "awesome-project",
  "version": "0.1.0",
  "devDependencies": {
    "gulp": "latest",
    "gulp-pug": "latest",
    "gulp-less": "latest",
    "gulp-csso": "latest",
    "gulp-concat": "latest"
  }
}

Step 3 - config gulp using gulpfile.js in your project directory, each task can be split into its own file, we can even use a directory named gulpfile.js that contains an index.js file which is treated as a gulpfile.js.

// gulpfile.js
const {src, dest, parallel} = require('gulp')
const pug = require('gulp-pug')
const less = require('gulp-less')
const minifyCSS = require('gulp-csso')
const concat = require('gulp-concat')

function html() {
  return src('client/templates/*.pug').pipe(pug()).pipe(dest('build/html'))
}

function css() {
  return src('client/templates/*.less')
    .pipe(less())
    .pipe(minifyCSS())
    .pipe(dest('build/css'))
}

function js() {
  return src('client/javascript/*.js', {sourcemaps: true})
    .pipe(concat('app.min.js'))
    .pipe(dest('build/js', {sourcemaps: true}))
}

exports.js = js
exports.css = css
exports.html = html
exports.default = parallel(html, css, js)

Optional — write and maintain custom plugins or inline plugins if public available ones can’t fulfil your needs. Gulp plugins are Node Transform Streams that encapsulate common behavior to transform files in a pipeline. They can change the filename, metadata, or contents of every file that passes through the stream.

const {src, dest} = require('gulp')
const uglify = require('gulp-uglify')
const rename = require('gulp-rename')

exports.default = function () {
  return (
    src('src/*.js')
      // The gulp-uglify plugin won't update the filename
      .pipe(uglify())
      // So use gulp-rename to change the extension
      .pipe(rename({extname: '.min.js'}))
      .pipe(dest('output/'))
  )
}

By using the power of node streams, gulp gives you fast builds that don’t write intermediary files to disk.

Something in Between

Grunt and Gulp are two generic task runners with huge plugins ecosystem. Other less capable alternatives like Broccoli.js and Brunch are so far behind in number of plugins and features.

There are other tools not defined as truly general-purpose task runners, but they can help us achieve the same result in some situations but they often have limitation in terms of orchestrating tasks and ecosystem of common tasks.

Webpack is specifically more interested in building frontend assets, highly modular and we can achieve similar results, and has its own philosophical differences from the aforementioned tools.

If you think the above task runners are overkill, you can make other tools like bash scripts, npm scripts or Makefile to behave like task runners with trade-offs of implementing all tasks and task orchestration by yourself.

Summary

Task runners are tools to orchestrate those kinds of repetitive mundane tasks in an efficient way, those popular ones like Grunt and Gulp have extensive ecosystem of plugins allowing us to do almost anything.

Grunt is strictly file-oriented and creates temporary local files during the execution of the tasks. Gulp, on the other hand, handles the processes via the memory and writes them in the target file immediately, giving the program a speed advantage.

The less work you have to do when performing repetitive tasks, the easier your job becomes

The bigger a project is, the more Gulp’s strengths come into play, which is why the new task runner is now the first choice for many people. Thanks to the lower requirements, however, Grunt is still a valuable tool for smaller, manageable projects.

More reading

Cracking The Frontend Interview, Part 5: React

React is the hottest technology in web development nowadays, working on it is exciting but preparing for interviews is really a pain in the neck

JavaScript Package Managers

Package managers are tools that help you manage packages as dependencies and might also provide a global package registry

JavaScript Static Type Checkers

Using static type checkers like Flow or TypeScript can greatly improve the safety of your JavaScript code

On Incompleteness of Prerendering

Prerendering is a rendering strategy when you pre-render your client-side rendered applications to feed crawlers better