Blog

Grunt.js: Custom Tasks

Previously, I talked about some tips and tricks I use with Grunt.js. If you haven't heard of Grunt.js you may want to check out my article on the basics of grunt. This time I want to wrap things up by talking about custom tasks in grunt. While there are a lot of great plugins for grunt sometimes you need something custom, lucky for us grunt makes it easy for us to define our own tasks.

Types of Tasks

Grunt.js provides two types of tasks: Basic Tasks and Multi-Tasks. Basic Tasks are little more than a named javascript function you can run from the command line. Multi-Tasks are what most plugins provide, you provide a namespace and code to run those tasks, then the user creates instances of that tasks by placing options under a config object of the same name.

A Basic Task

Let's say we want a task to load a database dump into postgresql. This might be handy to seed data on development machines. The shell command to do this is pretty simple just psql thedb < seed.sql, but we want to integrate it with some of our other grunt tasks.

We could add our task right in the Gruntfile.js, but custom tasks can start to clutter our gruntfile. Instead let's create a folder called tasks. To our Gruntfile.js we add the line grunt.loadTasks('tasks'). Much like the command grunt.loadNpmTasks loads javascript files from plugins, the loadTasks command loads the javascript files in a directory.

Now let's add our custom task, in the file tasks/seed.js we have:

var fs = require('fs');
var childProcess = require('child_process');
module.exports = function(grunt) {
    grunt.registerTask('seed', function() {
        var done = this.async();

        var seed = fs.createReadStream('seed.sql');

        seed.on('open', function() {
            var child = childProcess.spawn('psql', ['thedb'], {
                stdio: [seed, process.stdout, process.stderr] 
            });

            child.on('exit', done);
        });

        seed.on('error', done);
    });
};

The first interesting line is var done = this.async();. Grunt assumes that when the tasks function returns it's done. For really simple things that may hold, but in node.js most things need to be done async. The this.async() call informs grunt that this is an async task, grunt gives us back a function to call when the task is done. The next chunk of code opens our sql file and executes psql, passing an argument, and using the file as it's STDIN. Finally once the child process exits we tell grunt the task is done.

That's all there is too it. Now we can run our seed task with grunt seed, or make it part of a more complex process by including it in a list of tasks (e.g. grunt.registerTask('default', ['build', 'seed']);).

Mutli-Tasks

For most of the one off jobs you may need to do during your build basic tasks may be just what you need. But what if you have a more repetitive task? Mutli-Tasks are very similar to basic tasks, the differences are that you register them with grunt.registerMultiTask and you get some addition data as part of the this object in your function.

Let's write a multi-task to gzip files. Again we will put our task in it's own file, so in tasks/gzip.js we have:

var zlib = require('zlib');
module.exports = function(grunt) {
    grunt.registerMultiTask('gzip', function() {
        var done = this.async();

        var files = this.files.slice();

        function process() {
            if(files.length <= 0) {
                done();
                return;
            }

            var file = files.pop();

            grunt.log.writeln("Compressing " + file.src[0] + "...");
            var content = grunt.file.read(file.src[0], { encoding: null });

            zlib.gzip(content, function(err, compressed) {
                grunt.file.write(file.dest, compressed);
                grunt.log.ok("Compressed file written to " + file.dest);
                process();
            });
        }

        process();
    });
};

Grunt provides a normalized list of src/destination files in this.files. We copy the this.files array into a local variable files using slice since we're going to be modifying the array. We want to process each file, but can't use a normal loop since our processing is async. Instead, we define the process function which, if the files array is empty tells grunt we're done and returns. Otherwise it reads the first source, runs it through zlib to compress it, and writes it back out. After the file is compressed we call call process again to compress the next file.

One thing to note is that rather than using node's file function we use functions like grunt.file.write. These sometimes provide nice sugar on top of node, but they also allow grunt to implement certain features like not writing files if grunt is run with --no-write. So if you start writing your own task, and especially if you want to redistribute them, familiarize yourself with the grunt API docs, and use the helpers it gives you.

Further Reference

Grunt's documentation is pretty amazing. Some relevant bits for writing custom tasks:

  • Inside Tasks: Describes the this object inside your tasks.
  • Grunt API: Lists the various utility functions grunt makes available. Worth skimming over to help avoid reinventing the wheel.

There are a lot of greate grunt plugins out there, so don't be afraid to take a peak under the hood. Reading the source of the grunt-contrib plugins in particular can help give you ideas of how to implement more complicated plugins.