React and be thankful A blog about building (reactive) web applications

Building with Grunt (Part 1)

Hello Grunt

Grunt is (or was) not per se a build automation tool. Or at least, This is how Grunt describe itself: "The JavaScript task runner". So what is exactly Grunt? Since 4.0, Grunt is definitely a task runner... running tasks for automating builds!

Grunt was first release in January 2012 (v0.1.0) and it is with the well known version 0.4.x (first stable release in February 2013) that Grunt became the reference for building front-end source code, thanks to the release of "contribution" packages by the Grunt team (packages named grunt-contrib-xxx) for common tasks like concatenating, copying, watching, serving files etc...

Grunt was a precursor in the wonderful land of JavaScript (and Node) based build tools. Today its package ecosystem and its user base are very large. For many developers working with front-end technologies, Grunt is the first building tool they use, before having ever programmed on the Node platform. On that aspect, Grunt is very accessible thanks to its config-based approach. This is a pro as well as a con, and we will talk more about it later in this article. Grunt has also been adopted by other tools, including a lot of Yeoman generators and project skeletons. Other building tools are based on Grunt. Angus was one of them but later moved to Gulp.

So... Grunt became very popular. Maybe too popular? I have seen many Grunfile.js (either online or in some projects I worked on), and often make the same observation: Grunt tasks could be optimised or managed better by applying more Node style programming.

Getting started with Grunt

First install Grunt globally:

$ npm install -g grunt-cli
$ npm install -g grunt

Add grunt in your package.json and create a Gruntfile.js (see examples on Grunt's website)

$ npm install --save-dev grunt
$ touch Gruntfile.js

Configuration over code

When using a Grunt plugin (for concatenating, linting, etc...), we have to add configuration entry for that task in Grunt's config object and call loadNpmTasks to make Grunt aware of it. A task can contain targets, configure options that targets can extend and a task is generally about using some src files to produce a result in dest. Tasks and targets cam also be configured using files (a map or array) in lieu of src and dest.

Let see a Gruntfile.js example where we have a src folder containing JavaScript files which we want to lint and then concat in a build folder (using grunt-contrib-concat and grunt-contrib-jshint packages).

├── build
|   └── app.js
├── src
|   ├── a.js
|   ├── b.js
|   └── c.js
├── Gruntfile.js
└── package.json
module.exports = function (grunt) {
    grunt.initConfig({
        // Concat task
        concat: {
            files: {
                'build/app.js': 'src/*.js'
            }
        },
        // JsHint task
        jshint: {
            files: ['src/*.js']
        }
    });

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

    grunt.registerTask('build', ['jshint', 'concat'];
};

And we can now run grunt build, grunt concat, grunt jshint or even grunt jshint concat.

Splitting the Gruntfile

The most common issue developers encounter with Grunt is the size its config file. The more a build process requires tasks and targets, the longer the config object. Very quickly a Gruntfile.js can exceed a few hundred lines and this will affect its maintainability. A quick solution for this is to split our Gruntfile using Node modules, and use more of Grunt comprehensive API (surprisingly relatively unknown)!

We create a tasks folder where we are going to create a file per task, keeping our Gruntfile.js very slim:

├── build
|   └── app.js
├── src
|   ├── a.js
|   ├── b.js
|   └── c.js
├── tasks
|   ├── concat.js
|   └── jshint.js
├── Gruntfile.js
└── package.json

concat.js

module.exports = function (grunt) {
    grunt.config.set('concat', {
        files: {
            'build/app.js': 'src/*.js'
        }
    });

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

jshint.js

module.exports = function (grunt) {
    grunt.config.set('jshint', {
        files: {
            files: ['src/*.js']
        }
    });

    grunt.loadNpmTasks('grunt-contrib-jshint');
};

Gruntfile.js

module.exports = function (grunt) {
    require('./tasks/concat')(grunt);
    require('./tasks/jshint')(grunt);

    grunt.registerTask('build', ['jshint', 'concat'];
};

In a second article about Grunt, we will explore Grunt caveats, how we can deal with them and how it inspired other tools.

Something wrong? Fix it on Github!