← Back to all posts

Development workflow automation with Gruntjs

26 January 2014

Written by Nadeem Shabir

Your job as a developer isn’t to just develop, it’s to continually learn how to develop better ~ Paul Irish

Automating repetitive tasks is fun and reduces the time and errors associated with tasks that must be performed over and over again, consequently freeing up time for you to focus on the problems you really want to solve.


We’ve been doing Continuous Integration at Talis for as long as I can remember, and the defacto tool we’ve been using for automating our builds is Ant, we don’t just use ant on our build servers we also use it locally to automate repetetive tasks. Our Ant scripts often contain targets we can run locally to setup our development environment, load data, minify css/js, run unit/integration tests etc.

Grunt.jsGrunt.js is a Javascript based task runner and like Ant can be used to automate repetitive tasks in your development workflow. You can use it to automate tasks like compilation, minification, concatenation, versioning, cache-busting, linting, unit testing, etc. I’ve been using Grunt.js for a while now, and we are starting to use it more at Talis to help automate more of the development workflow and build automation on some of our projects. Being JavaScript based, and using JSON for configuration, Grunt is extremely simple to pick up and get started with and consequently this means its pretty easy to maintain your gruntfiles.

Just to illustrate this simplicity compare these two implementations of minify:

ANT

<target name="js-compile-all" description="Compile JavaScript files with Closure" unless="skip-js-compile">
    <echo>Compiling JS files in ${input.scripts.dir} in closure...</echo>
    <apply executable="java" dest="${output.scripts.dir}">
        <arg value="-jar"/>
        <arg path="${jar.lib.dir}/closure-compiler.jar"/>
        <arg line="--js"/>
        <srcfile/>
        <arg line="--js_output_file"/>
        <targetfile/>
        <fileset dir="${output.scripts.dir}" includes="**/*-main.debug.js" />
        <mapper type="glob" from="*-main.debug.js" to="*-main.min.js"/>
    </apply>
    <echo>Finished compiling JS files</echo>
</target>

Grunt.js

grunt.initConfig({
  min: {
    dist: {
      src: ['dist/built.js'],
      dest: 'dist/built.min.js'
    }
  }
});

The rest of this post will describe how to setup Grunt.js locally and how to configure several useful tasks and automate them. The tasks will:

  • run jshint over your source code and warn when you aren’t complying to code quality rules that have been defined for the project

  • run unit tests using mocha

  • run jsbeautify over your source code to ensure that code is always beautifed based on rules defined for your project.

Getting started with Grunt

If you have Node.js installed, it is relatively easy to install Grunt. You need to do the following:

npm install -g grunt-cli
cd <your_project_dir>
npm install grunt --save-dev
npm install grunt-contrib-jshint grunt-contrib-watch grunt-mocha-test grunt-jsbeautifier --save-dev

This will do several important things:

  1. It installs the Grunt command line module ( -g ) globally. Which means you can execute grunt from the command line e.g. grunt --version.

  2. It installs the Grunt module locally within your project; and by adding the --save-dev param, it will add Grunt to the list of development dependencies in your package.json file.

  3. It installs several grunt plugins which we’ll be using shortly.

Once the install completes your project should contain a package.json file that has a devDependencies block that looks like this:

"devDependencies": {
  "grunt": "~0.4.2",
  "grunt-contrib-watch": "~0.5.3",
  "grunt-jsbeautifier": "~0.2.6",
  "grunt-mocha-test": "~0.8.2",
  "grunt-contrib-jshint": "~0.8.0"
}

Now we can create our projects gruntfile.js which will define the workflow and tasks for Grunt to execute. Inside the grunt file, all configuration is done by passing an object literal to grunt.initConfig(). Save the following into your projects gruntfile.

module.exports = function(grunt){
  "use strict";

  grunt.initConfig({
    /* All grunt tasks will go here */
  });

  // Load the plugins that will perform necessary tasks.
  grunt.loadNpmTasks('grunt-mocha-test');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-jsbeautifier');

  /* Public grunt tasks - which you will call from command line */
  grunt.registerTask('default', []);
};

In the above script we load each plugin individually, however you could also load all installed grunt plugins using this handy one liner:

require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks);

Let’s start by configuring jshint, if you are not familiar with it, its a tool that helps to detect errors and potential problems in your JavaScript code. To use jshint you need to configure it so save the following into a file called .jshintrc

{
  "globals": {
    "module": true,
    "define": true,
    "requirejs": true,
    "require": true
  },
  "curly": true,
  "eqeqeq": true,
  "immed": true,
  "latedef": true,
  "newcap": true,
  "noarg": true,
  "sub": true,
  "undef": true,
  "unused": true,
  "boss": true,
  "eqnull": true,
  "node": true,
  "nonew": true,
  "trailing": true,
  "bitwise": true,
  "indent": 4
}

and then add the following to your gruntfile:

  grunt.initConfig({
    /* All grunt tasks will go here */

    jshint: {
        options: {
            jshintrc: '.jshintrc'
        },
        gruntfile: {
            src: 'Gruntfile.js'
        },
        lib: {
            src: ['lib/*.js']
        }
    }
  });

  // default task
  grunt.registerTask('default', ['jshint']);

The jshint task is configured to look for options in the .jshintrc file. The additional properties allow us to name different files/file patterns we want to to run jshint over. We also add jshint to our default task. This means we can execute the task from the command line by just typing grunt. Which will output:

$ grunt
Running "jshint:gruntfile" (jshint) task
>> 1 file lint free.

Running "jshint:lib" (jshint) task
>> 1 file lint free.

If any of the files contain any code quality issues the output would look like this:

$ grunt
Running "jshint:gruntfile" (jshint) task
>> 1 file lint free.

Running "jshint:lib" (jshint) task
Linting index.js ...ERROR
[L6:C8] W098: 'foo' is defined but never used.
var foo = "baz";

Warning: Task "jshint:lib" failed. Use --force to continue.

Aborted due to warnings.

That’s great but all we’ve really done is created a grunt task we need to manually call on the command line. Let’s really automate this by using watch to run jshint whenever we save any changes to the files in our project, so add the following to your gruntfile:

  grunt.initConfig({
    /* All grunt tasks will go here */

    jshint: {
        options: {
            jshintrc: '.jshintrc'
        },
        gruntfile: {
            src: 'Gruntfile.js'
        },
        lib: {
            src: ['lib/*.js']
        }
    },
    watch: {
        gruntfile: {
            files: '<%= jshint.gruntfile.src %>',
            tasks: ['jshint:gruntfile']
        },
        lib: {
            files: '<%= jshint.lib.src %>',
            tasks: ['jshint:lib']
        }
    }
  });

  // default task
  grunt.registerTask('default', ['jshint']);

Now we can leave grunt watch running on the command line, whenever any of the resources being watched is changed the configured tasks will be automatically executed :)

Let’s build further upon this. We can configure jsbeautify in much the same way we configured jshint. Start by adding your configuration options to .jsbeautifyrc:

{
  "indent_size": 4,
  "indent_char": " ",
  "indent_level": 0,
  "indent_with_tabs": false,
  "preserve_newlines": true,
  "max_preserve_newlines": 2,
  "jslint_happy": true,
  "brace_style": "collapse",
  "keep_array_indentation": false,
  "keep_function_indentation": false,
  "space_before_conditional": true,
  "break_chained_methods": false,
  "eval_code": false,
  "unescape_strings": false,
  "wrap_line_length": 0
}

and the following to your gruntfile:

  grunt.initConfig({

    /* All grunt tasks will go here */
    jshint: {
        options: {
            jshintrc: '.jshintrc'
        },
        gruntfile: {
            src: 'Gruntfile.js'
        },
        lib: {
            src: ['lib/*.js']
        }
    },
    watch: {
        gruntfile: {
            files: '<%= jshint.gruntfile.src %>',
            tasks: ['jshint:gruntfile']
        },
        lib: {
            files: '<%= jshint.lib.src %>',
            tasks: ['jshint:lib', 'jsbeautifier:check']
        }
    },
    jsbeautifier: {
        beautify: {
            src: ['Gruntfile.js', 'lib/*.js'],
            options: {
                config: '.jsbeautifyrc'
            }
        },
        check: {
            src: ['Gruntfile.js', 'lib/*.js'],
            options: {
                mode: 'VERIFY_ONLY',
                config: '.jsbeautifyrc'
            }
        }
    }    
  });

  // default task
  grunt.registerTask('default', ['jshint', 'jsbeautifier:check']);
  grunt.registerTask('beautify', ['jsbeautifier:beautify']);

By default jsbeautifier will modify files in place which is what our jsbeautifier:beautify task does, but here we’ve also added a task to check and report if any files need beautifying. Our watcher now runs jsbeautifier:check when our source files are changed. Our default grunt task now runs jshint, and jsbeautifier:check:

$ grunt
Running "jshint:gruntfile" (jshint) task
>> 1 file lint free.

Running "jshint:lib" (jshint) task
>> 1 file lint free.

Running "jsbeautifier:check" (jsbeautifier) task
Warning: The following files are not beautified:
index.js
 Use --force to continue.

Aborted due to warnings.

And to actually beautify the files :

$ grunt beautify
Running "jsbeautifier:modify" (jsbeautifier) task
Beautified 2 files, changed 1 files...OK

Done, without errors.

Finally, let’s add the ability to run unit tests. In this example our unit tests are written using Mocha. Add the following to your gruntfile:

  grunt.initConfig({

    /* All grunt tasks will go here */
    mochaTest: {
        test: {
            options: {
                reporter: "spec",
                timeout: 10000
            },
            src: ["test/**/*.js"]
        }
    },
    jshint: {
        options: {
            jshintrc: '.jshintrc'
        },
        gruntfile: {
            src: 'Gruntfile.js'
        },
        lib: {
            src: ['lib/*.js']
        }
    },
    watch: {
        gruntfile: {
            files: '<%= jshint.gruntfile.src %>',
            tasks: ['jshint:gruntfile']
        },
        lib: {
            files: '<%= jshint.lib.src %>',
            tasks: ['jshint:lib', 'jsbeautifier:check', 'mochaTest']
        }
    },
    jsbeautifier: {
        beautify: {
            src: ['Gruntfile.js', 'lib/*.js'],
            options: {
                config: '.jsbeautifyrc'
            }
        },
        check: {
            src: ['Gruntfile.js', 'lib/*.js'],
            options: {
                mode: 'VERIFY_ONLY',
                config: '.jsbeautifyrc'
            }
        }
    }    
  });

  // default task
  grunt.registerTask('default', ['jshint', 'jsbeautifier:check', 'mochaTest']);
  grunt.registerTask('beautify', ['jsbeautifier:beautify']);

The mochaTest task is configured to run all the tests in our test folder and, as with the other examples, we’ve also added it to the watcher so that tests are run whenever we change any of our source files. When executed it will output:

$ grunt
Running "jshint:gruntfile" (jshint) task
>> 1 file lint free.

Running "jshint:lib" (jshint) task
>> 1 file lint free.

Running "jsbeautifier:modify" (jsbeautifier) task
Beautified 2 files, changed 1 files...OK

Running "mochaTest:test" (mochaTest) task

  Persona Client Test Suite
    - Constructor tests
      ✓ should throw error if config.persona_host is not supplied
      ✓ should throw error if config.persona_port is not supplied
      ✓ should throw error if config.persona_scheme is not supplied
      ✓ should throw error if config.persona_oauth_route is not supplied
      ✓ should throw error if config.redis_host is not supplied
      ✓ should throw error if config.redis_port is not supplied
      ✓ should throw error if config.redis_db is not supplied
      ✓ should NOT throw any error if all config params are defined

...

So to summarise we’ve just used Grunt to automatically check our code for quality issues, run our unit tests and beautify our source code every time we save a change to a file. If you want to see a fully worked example here’s a simple client library we’ve created that uses some of the tasks we’ve described here.

Final thoughts

We hope you’ve found this introduction to Grunt.js useful, and hopefully we’ve succeeded in demonstrating how simple it is to automate parts of your development workflow using Grunt. There are many plugins for Grunt, and it should be easy to see how you can extend this example with many more capabilities.

There are many tools that can be used to automate your development workflows, in fact it’s a space that is evolving constantly. There’s a danger that people often get hung up on finding the perfect tool, so it worth remembering if you are doing any automation then you’re already doing something right! :)

Comments on HN