Task Automation With Grunt

This month I started a fresh project and found myself in need of a task runner to handle the build process. While I've heard good things about Gulp, I have experience with Grunt and prefer the cleaner config style syntax, so on this occassion, I opted to stick with what I know. Here's a quick walk-through.

Installing Grunt

Assuming you have Node.js installed on your machine, the first step is to check that you have the latest version of npm installed. npm is the package manager that we'll use to install Grunt.

// sudo is required for Mac OSX
// -g flag means that we want to install npm globally on our machine
$ sudo npm update -g npm

Now we can install the Grunt command line interface.

$ sudo npm install -g grunt-cli

Build Requirements

Before we go any further, let's define exactly what it is that we want from our build task.

Production Build

  • Take src assets and output them to public assets dir.
  • Compile multiple Sass files to a single concatenated, minified CSS file.
  • Compile Browserify modules to a single concatenated and minified javascript file.

Development Build

  • All of the above without concatenation and minification as we need the ability to debug CSS and JS issues.
  • Watch for changes and automatically trigger a new build to speed up the development workflow.

Tasks

I decided to register three seperate tasks:

// 1. build assets for dev without watching for changes
$ grunt dev-build

// 2. Build assets for dev and watch for changes
$ grunt dev-watch

// 3. Build assets for prod
// This task is the one you would add to your CI server scripts
$ grunt prod-build

Listing Plugin Dependencies

Manifest File

We need to create a package.json file in the root directory of our project. This is effectively a mainest file. It contains project meta data and a full list of packages that our project needs to install.

I might expand this post to look at the plugins seperately but for now I'm just going to provide the complete file.

// ### should be replaced with your specific projects meta data
{
  "name": "###",
  "description": "###",
  "version": "0.1.0",
  "homepage": "https://github.com/###",
  "repository": {
    "type": "git",
    "url": "git@github.com:###/###.git"
  },

  // these are the npm plugins that will be installed
  "devDependencies": {
    "grunt": "0.4.2",
    "grunt-browserify": "^3.8.0",
    "grunt-contrib-clean": "0.6.0",
    "grunt-contrib-concat": "0.5.1",
    "grunt-contrib-copy": "~0.5.0",
    "grunt-contrib-sass": "0.9.2",
    "grunt-contrib-uglify": "0.9.1",
    "grunt-contrib-watch": "0.6.1"
  }
}

It's worth noting, that installing each package from the command line will automatically populate the devDependencies object in your package.json file. For example:

$ sudo npm install grunt-contrib-watch --save-dev

Would result in:

"devDependencies": {  
    "grunt": "0.4.2",
    "grunt-contrib-watch": "0.6.1" // this line added
}

Once all our project dependencies are listed, we can install them all from the command line.

$ npm install

Configuring Our Gruntfile

In the root directory you will also need a file named Gruntfile. This is the file that contains all the task configuration.

Export Function

The entire contents of the file needs to be exported as a function.

module.exports = function(grunt) {

    // config goes here

};

Load NPM Packages

No we can specify which npm packages we are using.

grunt.loadNpmTasks('grunt-contrib-watch');  
grunt.loadNpmTasks('grunt-contrib-sass');  
grunt.loadNpmTasks('grunt-contrib-concat');  
grunt.loadNpmTasks('grunt-contrib-uglify');  
grunt.loadNpmTasks('grunt-contrib-clean');  
grunt.loadNpmTasks('grunt-contrib-copy');  
grunt.loadNpmTasks('grunt-browserify');  

Configure Plugin Parameters

Each plugin has a set of defined parameters that will except certain values, this always requires a quick look at the docs.

grunt.initConfig({  
    clean: {
        options: {
            force: true
        },
        vars: {
            src: ['../public/*']
        }
    },
    copy: {
        main: {
            files: [
                {
                    expand: true, 
                    src: ['images/*'], 
                    dest: '../public/assets/'
                }
            ]
        }
    },
    watch: {
        files: ['scss/*.scss', 'js/*.js', 'js/**/*.js'],
        tasks: [ 'clean', 'sass:concat', 'browserify:concat', 'copy' ],
        options: {
            spawn: false,
            livereload: true
        }
    },
    sass: {
        concat: {
            options: {
                style: 'expanded'
            },
            files: {
                '../public/assets/css/main.css': 'scss/main.scss'
            }
        },
        minify: {
            options: {
                style: 'compressed'
            },
            files: {
                '../public/assets/css/main.css': 'scss/main.scss'
            }
        }
    },
    uglify: {
        options: {
            compress: true,
            mangle: true,
            separator: ';',
        },
        target: {
            src: '../public/assets/js/main.js',
            dest: '../public/assets/js/main.js'
        }
    },
    browserify: {
        options: {
            debug: true
        },
        concat: {
            src: ['js/main.js'],
            dest: '../public/assets/js/main.js'
        }
    }
});

Register Tasks

Finally, we assign a task name that we will use to run our grunt tasks from the command line. The Grunt task will run the tasks specified in the initConfig in order.

grunt.registerTask('dev-build', ['clean', 'sass:concat', 'browserify:concat', 'copy']);

grunt.registerTask('dev-watch', ['clean', 'sass:concat', 'browserify:concat', 'copy', 'watch']);

grunt.registerTask('prod-build', ['clean', 'sass:minify', 'browserify:concat', 'uglify', 'copy']);  

If we wanted to run dev-watch from the command line, we would simply run:

$ grunt dev-watch

Asssuming, there are no errors (in which case Grunt would indicate what they were) all your tasks will be run. In this particular case, that means that every time you changed a Sass file or Browserify module in the location specified within the various configs of your Gruntfile, the public dir would be emptied, assets compiled & concatenated from the src dir (but not minified) and finally copied to the public dir before re-initiating the 'watch' for changes.

You Skipped Explaining The Plugin Configurations

The docs are your friend here, there's not a single one of those configs that I can remember without a google search for their docs.