This is the second post in “Expanding Your Toolset” series (previous post was Expanding Your Toolset: Bower - Package Management For The Web). Last time we’ve looked at Bower - a package manager for the web. Bower is useful for quickly resolving dependencies for your project (e.g. if you need a specific version of jQuery, AngularJS, UnderscoreJS and etc.).

GruntJS

GruntJS is a task runner. It’s primary purpose is to automate the tedious stuff we as developers have to do. For example, when developing for the web it’s usually a good idea to minify and combine CSS and Javascript. Do you do this by hand? Or maybe you have a custom shell script to do that for you? Been there, done that. In any case using GruntJS will make it easier and quicker to accomplish such tasks. The community has written many plugins for it (e.g. JSHint, Sass, Less, CoffeeScript, RequireJS and etc.), thus it may be really beneficial for your current (or future) projects.

Installing Grunt

GruntJS is a Javascript library and it runs on NodeJS. It’s distributed via Npm, so to install it globally simply run:

npm install -g grunt-cli

As usual, on Linux you may have to prepend sudo command if you’re getting EACCESS errors.

Setting up Grunt

For a new project we will need to create two files: package.json and Gruntfile.js (or Gruntfile.coffee if you use CoffeeScript).

For our example we will automate concatenation of a bunch of files.

First let’s set up our package.json. grunt is needed to be defined as a dependency and we’ll use grunt-contrib-concat and grunt-contrib-watch plugins for this example. We don’t provide the version so that Npm would install the latest ones.

{
    "name": "testproj",
    "version": "0.1.0",
    "dependencies": {
        "grunt": "",
        "grunt-contrib-concat": "",
        "grunt-contrib-watch": "",
    }
}

Now run npm install to install all dependencies for our little example. Next let’s set up our Gruntfile.js.

module.exports = function(grunt) {

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        concat: {
            options: {
                banner: "/* <%= pkg.name %> v<%= pkg.version %>*/\n"
            },
            dist: {
                src: ['app/dev/aaa.js', 'app/dev/bbb.js', 'app/dev/ccc.js'],
                dest: 'app/app.js'
            },
            dev: {
                src: ['app/dev/*.js'],
                dest: 'app/dev.js'
            }
        }
    });

    grunt.loadNpmTasks('grunt-contrib-concat');

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

module.exports = function(grunt) { ... is the wrapping function that will be executed once you run grunt command. This is the basic format, that’s where all of your Grunt code should go to.

Configuration is passed into grunt.initConfig(configuration). Configuration should describe what tasks are available and configuration of those tasks. You can store any arbitrary data in this configuration object as well. In our example we load configuration information from package.json and will use it to construct the banner. <% %> are template tags (yes, Grunt supports templates).

concat defines configurations for the concatenation task. We can add more tasks and will do so in later examples. We can specify an options object to override the default configuration for that particular task. dist and dev are targets. Unless specified otherwise, if we run grunt concat it will run concat task on both targets. But if we run grunt concat:dist, then concat will only be run for dist target. dist object has to have a src attribute which takes an array of file paths (e.g. in target dist). Grunt supports globbing (e.g. target dev), which means any javascript file in app/dev/ will be concatenated into app/dev.js.

grunt.loadNpmTasks('grunt-contrib-concat'); enables you to use grunt-contrib-concat task. Though it must be noted, that grunt-contrib-concat must be specified as a dependency in your package.json file and installed.

grunt.registerTask('default', ['concat']); tells Grunt which tasks it should run if you don’t specify which task should be run. So if you’d run grunt - it would run the task concat. If default task is not specified and grunt command is run - it will only return Warning: Task "default" not found. Use --force to continue. and exit.

Next let’s create three files: app/dev/aaa.js, app/dev/bbb.js and app/dev/ccc.js with their contents respectively:

(function() { console.log('File aaa.js'); })();
(function() { console.log('File bbb.js'); })();
(function() { console.log('File ccc.js'); })();

Now if we run grunt we should see this:

  % grunt
Running "concat:dist" (concat) task
File "app/app.js" created.

Running "concat:dev" (concat) task
File "app/dev.js" created.

Done, without errors.

And if we check app/app.js:

/* testproj v0.1.0*/
(function() { console.log('File aaa.js'); })();

(function() { console.log('File bbb.js'); })();

(function() { console.log('File ccc.js'); })();

Wonderful, so it is indeed working. Sure, you may not want to use this exact method to combine your javascript (using grunt-contrib-uglify is much better). But let’s take it one step further. Let’s use grunt-contrib-watch to watch for changes in any of our app/dev/*.js files and reconcatenate them as needed.

Watching for changes

We’ll need to do a few alterations. We’ve already added grunt-contrib-watch dependency to package.json. So let’s add grunt.loadNpmTasks('grunt-contrib-watch'); to the Gruntfile.js.

Then we’ll set up the watching. Add this to the configuration object:

watch: {
    scripts: {
        files: ['app/dev/*.js'],
        tasks: ['concat']
    }
}

So we’re adding a new task watch with target scripts (the target name is arbitrary - you can pick one yourself if you wish). Then we define which files it should watch and which tasks should be executed if any of the watched files change. The end file should look like this:

module.exports = function(grunt) {

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        concat: {
            options: {
                banner: "/* <%= pkg.name %> v<%= pkg.version %>*/\n"
            },
            dist: {
                src: ['app/dev/aaa.js', 'app/dev/bbb.js', 'app/dev/ccc.js'],
                dest: 'app/app.js'
            },
            dev: {
                src: ['app/dev/*.js'],
                dest: 'app/dev.js'
            }
        },
        watch: {
            scripts: {
                files: ['app/dev/*.js'],
                tasks: ['concat']
            }
        }
    });

    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-watch');

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

Now if we run grunt watch and change app/dev/ccc.js, then we should see this:

  % grunt watch
Running "watch" task
Waiting...OK
>> File "app/dev/ccc.js" changed.

Running "concat:dist" (concat) task
File "app/app.js" created.

Running "concat:dev" (concat) task
File "app/dev.js" created.

Done, without errors.
Completed in 0.382s at Fri Mar 07 2014 22:58:49 GMT+0200 (EET) - Waiting...

As you can see it reruns concat task on each change as expected.

You can really leverage watch task in certain situations. For example it was used for grunt-contrib-livereload (and apparently it’s now baked in directly into grunt-contrib-watch!) to detect code changes and refresh your browser automatically. And since Grunt can play with other languages well, I’ve used it with grunt-shell to do some rapid prototyping with Python (nothing fancy, it detects any changes and reruns the application, yet it sped up the working process).

Usage ideas

The example above should give you the basic picture of how Grunt works. I’ve realized that there are many use cases for this tool and it probably be pointless to describe each one step by step, so I’ll just provide you with some ideas how Grunt could help your workflow and point to some plugins. You can read their wiki pages for integration details.

Last thoughts

I haven’t provided as many examples as I wanted to, but I hope that this one example gave you an idea of how this works and whether it would fit into your workflow. Personally, I really like Grunt so far and I’ve already started to integrate it into my projects. Currently it’s limited to minifying code and running shell commands, but I hope to expand the usage as I explore.

Grunt can help a lot if you allow it to do so. Community has already created 2443 plugins (at the time of writing), so I bet there’s a lot that can be automated simply by installing a plugin. And if there’s no plugin for your use case - you can simply create one and publish it yourself.