Organizing Your Grunt Tasks

Avatar of Jason Witt
Jason Witt on

The idea of breaking up your code into smaller bite sized chunks creates an environment that is easy to work in and maintain. That’s often thought of as module design, and is a standard for web development these days. I’m going to show you a way you can use module design to better organize your Grunt tasks.

I’m going to assume that you already know the basics of using Grunt. If you don’t, here’s an article by Chris to get you going: Grunt for People Who Think Things Like Grunt are Weird and Hard.

If you use Grunt, you’re probably used to seeing your Gruntfile looking something like this.

module.exports = function(grunt) {
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    sass: {
      dist: {
        options: {
          style: 'expanded',
          sourcemap: 'none'
        },
        files: {
          'style.css': 'sass/global.scss',      
          'css/dev.style.css': 'sass/global.scss',
          'css/ie9.style.css': 'sass/ie9.scss',
        }
      }
    },
    postcss: {
      options: {
        processors: [
          require('autoprefixer')(),
          require('rucksack-css')({ fallbacks: true })
        ]
      },
      dist: {
        src: 'style.css',
        dest: 'style.css'
      },
      dev: {
        src: 'css/dev.style.css',
        dest: 'css/dev.style.css'
      },
    },
    cssmin: {
      target: {
        files: {
          'style.css': 'style.css'
        }
      }
    },
    concat: {
      dist: {
        src: [
          'js/lib/no-conflict.js',
          'js/lib/skip-navigation.js',
        ],
        dest: 'js/scripts.js'
      },
    },
    jshint: {
      files: [
        'js/scripts.js',
        'js/ie.js',
      ],
      options: {
        scripturl: true,
        globals: {
          jQuery: true
        }
      }
    },
    uglify: {
      options: {
        mangle: false,
        compress: true,
        quoteStyle: 3
      },
      dist: {
        files: {
          'js/head.min.js': 'js/head.js',
          'js/scripts.min.js': 'js/scripts.js',
          'js/ie.min.js'     : 'js/ie.js',
        }
      }
    },
    watch: {
      scripts: {
        files: ['js/**/*.js'],
        tasks: ['concat', 'uglify'],
        options: {
          spawn: false
        }
      },
      css: {
        files: ['sass/**/*.scss'],
        tasks: ['sass', 'postcss', 'cssmin']
      }
    },
  });
  grunt.loadNpmTasks('grunt-postcss');
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-cssmin');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-jsvalidate');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-sass');
  grunt.registerTask('default', ['watch']);
};

This is pretty normal looking Gruntfile. Actually this is a pretty small one. I’ve seen Gruntfiles that have been three time this size. Looking at those large Gruntfiles gives me a headache. I’ve had module design drilled into me so much that seeing a Gruntfile that large throws me off my game. Why can’t my Gruntfile look more like this?

module.exports = function(grunt) {
  var tasks = {scope: ['devDependencies', 'dependencies' ]};
  var options = {config: { src: "grunt/*.js" }};
  var configs = require('load-grunt-configs')(grunt, options);
  require('load-grunt-tasks')(grunt, tasks);
  grunt.initConfig(configs);
  grunt.registerTask('default', ['watch']);
};

It can! I’m going to show you how.

To accomplish this, we’re going to install two Grunt packages.

  1. load-grunt-tasks
  2. load-grunt-configs

Go ahead, and install these packages as you would any other Grunt package.

$ npm install --save-dev load-grunt-tasks
$ npm install --save-dev load-grunt-configs

Now your `package.json` file should include the two packages like this.

{
  "name": "your-project",
  "version": "1.0.0",
  "description": "",
  "main": "Gruntfile.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "grunt-contrib-cssmin": "^1.0.1",
    "load-grunt-configs": "^1.0.0",
    "load-grunt-tasks": "^3.5.0"
  }
}

For the sake of this article, I also included grunt-contrib-cssmin. Your `package.json` file should have any Grunt packages you’ll need for your project.

Now, let’s start with a fresh `Gruntfile.js`. Create a new `Gruntfile.js` file and add the following.

module.exports = function(grunt) {

}

The first thing we’ll do is set up load-grunt-tasks. What load-grunt-tasks does is it creates all of your grunt.loadNpmTasks() for you. This removes the need to having to write out each grunt.loadNpmTasks() by hand.

Let’s create a variable tasks that will define the options for the load-grunt-tasks package. All we’re going to do is define the scope and tell it to create a grunt.loadNpmTasks() for all the devDependencies and dependencies packages defined in the `package.json` file.

module.exports = function(grunt) {
  var tasks = {scope: ['devDependencies', 'dependencies']};
}

We’ll need to require() it and add the tasks variable to it as the second parameter. Let’s also add two more variables options and configs for load-grunt-configs.

module.exports = function(grunt) {
  var tasks = {scope: ['devDependencies', 'dependencies' ]};
  var options = {config: { src: "grunt/*.js" }};
  var configs = require('load-grunt-configs')(grunt, options);
  require('load-grunt-tasks')(grunt, tasks);
}

In the options variable we telling load-grunt-configs where to look for the files that will contain the grunt task options. The configs variable is simply requiring load-grunt-configs and adding the options as the second variable.

Finally, we’ll add the configs variable to the grunt.initConfig() function and register a basic task that will run grunt-contrib-cssmin.

module.exports = function(grunt) {
  var tasks = {scope: ['devDependencies', 'dependencies' ]};
  var options = {config: { src: "grunt/*.js" }};
  var configs = require('load-grunt-configs')(grunt, options);
  require('load-grunt-tasks')(grunt, tasks);
  grunt.initConfig(configs);
  grunt.registerTask('default', ['cssmin']);
}

That’s all we’ll be putting into the Gruntfile. The only other things you may want to add would be additional tasks you want to register. Like a watch task or a build task.

In the options variable we defined the src as grunt/*.js. This tells load-grunt-configs to look in a directory named grunt and include all JavaScript files within it.

Let’s create a directory named grunt and add the task options for cssmin by creating a file named `cssmin.js`. Your project’s directory structure should look similar to this now.

Let’s add the cssmin options to the `cssmin.js` file. The first thing you want to add is the Grunt module.exports variable just like in the Gruntfile, so that Grunt knows this file is to be loaded when you run your Grunt task. Then add the options for cssmin:

module.exports = {
  target: {
    files: {
      'style.css': 'styles.css'
    }
  }
};

If you noticed I didn’t add the 'cssmin: {}' wrapper like you would in a basic Gruntfile. This is because load-grunt-configs uses the file’s name to recognize which task is being run. For example if you’re using grunt-contrib-uglify the file name would be `uglify.js`. If you’re using grunt-postcss. That file name will be `postcss.js`.

This is how we’re adding the module design concept to our grunt tasks. Each task will have its own file containing the task’s options. This makes to easy to add new tasks, and for multiple developers to makes changes without the worries of accidentally messing up another task in a enormous Gruntfile.

Now when you run your grunt task. Grunt will look in the `grunt` folder find all the task files and run those tasks using the options you defined within each file.

This was a basic example of this technique. The load-grunt-tasks and load-grunt-configs packages have more options available that can give you even more control of your Grunt tasks.

I hope this helps you get your Gruntfile under control and add more flexibility to your projects using Grunt.