JavaScript Automation, Part II - Configuring Grunt

Building complex JavaScript front-ends is complicated. Grunt can help you improve your code/debug lifecycle by automating a lot of repetitive tasks that otherwise take up a lot of your time. If you are not familiar with Grunt yet, read Part I - Setting up Grunt. Once you have Grunt installed in your project, proceed to the next step - Grunt configuration.

 

Choosing Grunt Plugins

Npm has a lot of Grunt plugins in its repository of plugins. Just search for "grunt" in the npmjs.org site to get an idea of what is available. For our purposes, though, there are a few Grunt plugins that will help us with our code/debug lifecycle:

  • grunt-contrib-concat

    Concatenates files from source. 
     
  • grunt-contrib-imagemin

    Produces bandwidth-optimized images from original source images
     
  • grunt-contrib-sass

    Produces SASS files from CSS source.
     
  • grunt-contrib-uglify

    Minifies and obfuscates JavaScript source files, and produces source maps for debugging.
     
  • grunt-contrib-watch

    Watches source code tree for changes and runs tasks when files change.
     
  • grunt-jslint

    Lints JavaScript code.
     
  • grunt-newer

    Operates only on files that have changed, speeding up the overall process by skipping files that have not changed.
     
  • load-grunt-config

    Configuration system that allows you to cut up your Gruntfile into multiple files for easier maintenance.
     
  • load-grunt-tasks

    Configuration system for loading Grunt tasks.
     

Imagemin and Sass are optional plugins dealing with images and SASS conversion respectively. You don't necessarily need them for JavaScript development, but you might find them useful as part of your overall front-end development lifecycle. If you decide to use Imagemin, you will need some additional plugins:

  • imagemin-gifsicle
  • imagemin-optipng
  • imagemin-pngquant

These plugins enable optimization of specific image formats. 

 

You install each of these modules using the same syntax used to install Grunt. From the root of your project type:

 

$ npm install <module> --save-dev

 

Configuring Your Project

At this point, we have installed the required software to configure the project for automated JavaScript ( and SASS/image optimization if you choose ) builds. Next, we need to configure Grunt to do the tasks we need done. As a reminder, our primary goal for automation is to eliminate manual steps required to edit a JavaScript source file and debug it in our application. With that in mind, we need the following steps included in the process:

  1. concatenating
  2. minifying/obfuscating
  3. generating source maps for debugging

Furthermore, we only want our tasks to operate on source code that has changed so we don't incur unecessary delays. The difference between running these tasks on just a single set of changed source files and on our entire application might be several seconds. That doesn't sound like a big deal until you have to wait several seconds over and over again because of minor code changes like punctuation corrections, variable name changes and the like.

With our configuration, our goal is that the taks have run within the time between when we click Save on a JavaScript source file and the time we click Refresh on the browser, so we never have to do anything manually and we never have to wait for our changes to be rolled into test code.

 

Gruntfile.js

In its simplest form, this file loads our tasks and tells Grunt what to do based on which commands we give it on the command line. We are setting up and extended configuration, so first we are going to create a grunt folder inside the root of our project to hold our config files. Next we'll create a Gruntfile in the root that defines how our plugins get loaded and run:

Gruntfile.js

//module pattern

module.exports = function(grunt) {

var path = require('path');

// use load-grunt-config to load config files
require('load-grunt-config')(grunt, {

       // path is ./grunt from root of project to our config files
    configPath: path.join(process.cwd(), 'grunt'), 
    init: true, 
    data: { 
                  // node config file
         pkg: require('./package.json')
    },

    loadGruntTasks: { 
        // define the tasks we want to load, note use of wildcards 
        pattern: ['grunt-contrib-*', 'grunt-jslint', 'grunt-newer', 'imagemin-*'],
             // our Grunt tasks are defined in devDependencies in package.json
             scope: 'devDependencies'
        },
        postProcess: function(config) {} 
    });

    // our base task that will be run by default if we type "grunt" in the root of the project
    grunt.registerTask("default", ["newer:jslint", "newer:concat", "newer:uglify", "newer:sass"]);
};

 

Although our Gruntfile is relatively short at this point, there are a lot of things going on:

  1. We are using load-grunt-config to load separate config files in the grunt folder. 
  2. We are loading the Grunt tasks as defined in the pattern argument to loadGruntTasks(), scoped to the devDependencies section of package.json.
  3. We can use wildcard operators to load multiple Grunt tasks by pattern, which shortens our config file and increases readability
  4. We are registering a default set of tasks to run when we type "grunt" at the command line.
  5. In the default tasks, the use of "newer:*" is an instruction to use the grunt-newer plugin to only operate against source files that have changed since the last execution of the task, saving us time.

The real magic, as we will see later, occurs in the config files for the individual tasks, which we will house in the grunt folder.

Before we look at our other config files, though, let's take a peek at the devDependencies section of package.json:

  "devDependencies": {

    "grunt": "^0.4.5",

    "grunt-contrib-concat": "^0.5.0",

    "grunt-contrib-imagemin": "^0.9.0",

    "grunt-contrib-sass": "^0.8.1",

    "grunt-contrib-uglify": "^0.7.0",

    "grunt-contrib-watch": "^0.6.1",

    "grunt-jslint": "^1.1.12",

    "grunt-newer": "^1.1.0",

    "imagemin-gifsicle": "^2.0.0",

    "imagemin-optipng": "^2.0.1",

    "imagemin-pngquant": "^2.0.0",

    "load-grunt-config": "*",

    "load-grunt-tasks": "^3.1.0"

  },

If you installed your modules using npm install, your devDependencies section of package.json should look something like the above. Npm is managing our dependencies by tracking the versions of each module we need in order to run our tasks. 

In Part III of the series, we'll look at setting up our config files for each task and putting it all together into a working development environment.