Advanced Grunt tooling

http://www.jonathanjeter.com/images/gruntjs.png
Grunt has made web development more enjoyable. By automating repetitive tasks, it has allowed web developers to focus on building features rather than copying, compiling, and configuring.

In this post, I will share some advanced tooling with Grunt which will help you use it to its full potential.

Bash functions

When using grunt frequently, it is nice to have shortcuts for installing plugins. These two bash functions, when included in your ~/.bash_profile, will allow you to quickly install grunt plugins:

# Install a grunt plugin and save to devDependencies
function gi() {
  npm install --save-dev grunt-"$@"
}

# Install a grunt-contrib plugin and save to devDependencies
function gci() {
  npm install --save-dev grunt-contrib-"$@"
}

The first bash function can be used as follows to install grunt-nodemon:

gi nodemon

The second function is an additional shortcut to install contrib plugins and can be used as follows to install grunt-contrib-watch:

gci watch

Loading all grunt tasks automatically

Every time you want a new grunt plugin, you typically have to add a grunt.loadNpmTasks call to your Gruntfile. This is a hassle and results in unecessary maintenance when you want to add and remove grunt plugins.

Thankfully there is a node module, matchdep, which reads your package.json file and extracts all of the dependencies that match a specified minimatch string. To automate task loading, first install matchdep with the following command:

npm install --save-dev matchdep

Then add this line to your Gruntfile which will run grunt.loadNpmTasks on each grunt plugin found in your package.json:

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

Now you will never have to manually add and remove grunt.loadNpmTasks :).

Aliasing tasks

In grunt, you can alias groups of tasks and make a more semantic interface for you and your development team.

For example, you could write a custom test grunt task like the following which would lint your code and run all of your CasperJS and Jasmine tests:

grunt.registerTask('test', ['jshint', 'ghost:all', 'jasmine:all']);

You could also write a development build task to compile your Sass and Handlebars templates and copy your scripts into a dev folder:

grunt.registerTask('build:dev', ['compass:dev', 'handlebars:dev', 'copy:scripts']);

It is helpful to have the most common group of tasks aliased as the default, so that developers can simply enter grunt into the command line. The following default task would capture the most common use case by building your project and then watching for changes:

grunt.registerTask('default', ['build:dev', 'watch']);

Verbose mode & debugging

For a long time in my early grunting I didn't know you could see more task output by adding the -v flag to a command. Some tasks opt for extended logging in verbose mode, and in that case, setting this flag can expose more information about a task if it's not functioning as expected.

If you need to look further into debugging a task by seeing a stack trace, you can also set the --stack flag.

Debugging during grunt development

One of the challenging aspects of build process development is knowing if your configuration works as expected from a clean state. A useful plugin for debugging this situation is grunt-contrib-clean.

Using the clean task, you can clear out the destination folders of your build tasks and execute them from the perspective of a developer who will pull down your changes with an empty destination folder.

Invest in a good watch

grunt-contrib-watch is an essential plugin to achieve an efficient development workflow. When used properly, you can have all of your development tasks triggered via watch.

Here is an example which illustrates what you can achieve with a properly configured watch task:

watch: {
  scripts: {
    files: ['*.js', 'src/**.js'],
    tasks: ['jshint:all', 'copy:scripts'],
    options: {
      livereload: true
    }
  },
  compass: {
    files: ['src/styles/**'],
    tasks: ['compass:dev'],
    options: {
      livereload: true
    }
  },
  handlebars: {
    files: ['src/templates/**'],
    tasks: ['handlebars:dev'],
    options: {
      livereload: true
    }
  },
  jasmine: {
    files: ['test/spec/**'],
    tasks: ['jshint:all', 'jasmine:all']
  }
}

Using the above config, when a file is modified, the tasks related to the file are run. Looking at the scripts target, when a source script file is saved, the script files are linted, copied to the dev folder to be served, and the livereload event is triggered to refresh your browser. When a jasmine test spec is saved, the code is linted and the tests are run, but the browser is not livereloaded as no source files have changed.

Using variables in your configuration

Sometimes in grunt it is valuable to use variables in your configuration. In situations like the build:dev command above, all of the included tasks can point to a directory variable which can easily be modified if your folder structure changes. Using variables also allows you to dynamically generate configuration based on task arguments.

In the first example below, there is an example of using a variable for a directory name and the following example uses dynamically generated configuration. Grunt allows you to pass these variables via underscore templates inside of your configuration, so you could have a global variable, globalConfig, which would be available to each task using the following config:

module.exports = function (grunt) {
  var globalConfig = {
    src: 'src',
    dest: 'dev'
  };

  grunt.initConfig({
    globalConfig: globalConfig,
    compass: {
      options: {
        sassDir: '<%= globalConfig.src  %>/styles',
        cssDir: '<%= globalConfig.dest %>'
      },
      dev: {}
    },
    handlebars: {
      compile: {
        files: [{
          src: ['<%= globalConfig.src %>/templates/*.hbs'],
          dest: '<%= globalConfig.dest %>/scripts/compiledTemplates/appTemplates.js'
        }]
      }
    },
    copy: {
      scripts: {
        files: [{
          expand: true,
          cwd: '<%= globalConfig.src %>/scripts',
          src: ['**'],
          dest: '<%= globalConfig.dest %>/scripts'
        }]
      }
    }
  });

  grunt.registerTask('build:dev', ['compass:dev', 'handlebars:compile', 'copy:scripts']);
};

Running individual test specs

When writing a significant amount of tests, it is nice to be able to run a single test in isolation without running the whole suite. To achieve this, you can use a custom task called spec which takes a task and filename as arguments and then runs that task's spec target pointing at that file. Here is the grunt config to run individual test specs via spec:

module.exports = function (grunt) {
  grunt.initConfig({
    globalConfig: globalConfig,
    jasmine: {
      all: {
        src: 'src/**.js',
        options: {
          specs: ['test/spec/*Spec.js']
        }
      },
      spec: {
        src: 'src/**.js',
        options: {
          specs: ['test/spec/<%= globalConfig.file %>Spec.js']
        }
      }
    }
  });

  grunt.registerTask('spec', 'Runs a task on a specified file', function (taskName, fileName) {
    globalConfig.file = fileName;
    grunt.task.run(taskName + ':spec');
  });
}

To run a spec located at test/spec/utilSpec.js using the config above, you would enter the following command:

grunt spec:jasmine:util

Plugins to check out

grunt-pages

grunt-pages builds html pages from markdown and layout templates. When used with Cabin, you can scaffold out a Gruntfile to generate a static site. This blog is using the default Cabin theme, so if you dig the look, you should check out Cabin!

grunt-nodemon

grunt-nodemon is extremely useful if you are developing a node.js server. It configures nodemon to monitor your node server for changes and restart when changes are detected. Nodemon's advantage over similar tools is that it stops attempting to restart your server when an error is encountered and only tries again when another file change is detected.

grunt-concurrent

grunt-concurrent was originally designed to make your tasks run faster, but it can also be used to run blocking tasks concurrently like watch and nodemon.

Conclusion

Some of these concepts are present in the yeoman-generated Gruntfile. I recommend checking it out to see some of these concepts in action.

cabin logo