There are a number of tools from the web development community that are designed to make your life a whole lot easier. One such type of tool are automation tools or task runners. The three biggest players I have come across are Guard, Gulp, and Grunt.js. We will be diving into Grunt.js for the remainder of this article.

Grunt.js Logo all rights reserved to http://gruntjs.com/

Logo property of http://gruntjs.com/

Build Tools

While I can't speak on behalf of Gulp, I have used both Guard and Grunt.js in my projects. In one of my earlier posts (Leveling up CSS with Sass) I mentioned the usage of Guard for compiling Sass files. For this purpose it is really useful but I have found that Grunt.js is just a little easier for me. I still use Guard for Rails based projects though as it integrates well.

Now before I begin to explain how to setup Grunt.js lets conceptualize what we wish to accomplish from the final configuration. We want a Grunt.js build system that has automatically loaded plugins, an easy way of maintaining tasks, and external plugin configurations. Over the next couple sections I will detail how we can get such a grunt setup working. Additionally, for developers new to Grunt this article will serve as a crash course into the world of task runners.

Sections

Setup Node.js

Before we can begin using grunt you need to first make sure you have node installed so we can use npm. NPM is like Ruby Gems, Composer, or Bower, in that it manages specific packages for your project.

To install node, download the proper version based on your OS from here. Once installed run the following initialize command in the project directory from your terminal. npm init

This will walk you through creating a package.json file that contains all the necessary project information. Just use the defaults for the purposes of this article. Once completed you will be ready to move onto the next step.

Basic Grunt.js Setup

Just like step one we need to install grunt, which is made trivial now that have npm installed. Run the following command to install grunt as a devDependency (--save-dev). npm install grunt --save-dev. If your system balks then try running the command as sudo. The format of npm install [grunt-plugin] --save-dev is generally the easiest way to install plugins. Additionally, a devDependency is just a way of letting npm know that the plugin or module is only required for the development environment.

Install a few more plugins on your own for Grunt.js's usage. grunt-contrib-watch, grunt-notify, grunt-contrib-compass. Watch looks for file directory changes, notify displays growl like notifications to your OS, and compass compiles sass and adds compass features.

Note:
You will need to run the above plugins in the standard npm install [plugin-name] --save-dev format

Next we will need to create a basic Gruntfile.js. Every Grunt.js project requires this file as well as the above package.json file. Here is a sample to copy/paste into your project. Name the file Gruntfile.js and place into your base directory

module.exports = function(grunt) {

  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json')
  });

  // Loading of plugins
  grunt.loadNpmTasks('grunt-contrib-compass');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-notify');

  // Default task(s).
  grunt.registerTask('default');

};

Configure the Gruntfile.js

There are three basic parts to each Gruntfile: loading plugins, task definitions, and configurations. Loading just places plugins into the grunt. Tasks are basically a chain of actions that can be called. And configurations set up external data and internal plugins.

Loading plugins

The loading of plugins is really quite simple. After install a plugin you just use the built in function loadNpmTasks to load the plugin into your grunt file for usage. If you are thinking to yourself, "There has got to be an easier way to load plugins", you would be correct. I will discuss an improved plugin loading method in Step 4. Here is a sample of code from above showing this section.

// Loading of plugins
grunt.loadNpmTasks('grunt-contrib-compass');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-notify');

Configurations

Configurations are the soldiers of Grunt.js. They do exactly as they are told and nothing else.

The main configuration is created through the usage of the initConfig configuration object. This allows you to pass in external values and plugin configurations. For instance we placed pkg: grunt.file.readJSON('package.json') in the above code. This reads our package.json file created from npm init and places it into the config variable pkg.

I've provided a simple configuration for notify, watch, and compass below. Take a close look at the notify configuration and you will notice the string Hello there <%= pkg.name %>. This outputs, "Hello there test" in my case, we setup for pkg when we read in the package.json file. Pretty cool right?

    grunt.initConfig({
      pkg: grunt.file.readJSON('package.json'),

      // Watch plugin config
      watch: {
        compass: {
            files: '**/*.scss',
            tasks: ['compass:compile']
        }
      },

      // Notify plugin config
      notify: {
        welcome: {
            options: {
                title: "Hello there <%= pkg.name %>", // Note we are outputting the package.json name variable here
                message: "My name is Grunt"
            }
        },
        another: {
            options: {
                title: "Just another notify config",
                message: "I will be important later on"
            }
        }
      },

      // Compass plugin config
      compass: {
        compile: {
            options: {
                sassDir: 'sass',
                cssDir: 'css'
            }
        }
      }
    });

Configurations are setup by listing the task name, which is generally the plugin name like notify or watch, followed by the target and then the options. In the case of the grunt-notify plugin, notify is the task name (not to be confused with tasks coming up later), welcome is the target, and options are specific to the plugin and grunt. The task and target can actually be named whatever you want as long as the they are called correctly in the task definition.

We can actually bring up terminal and run grunt notify:welcome and it will start grunt looking for a task name of notify with a target of welcome. Assuming grunt-notify is setup correctly (outside scope of this article) you should get something like this.

Grunt notify display

Congratulations, you have just run your very first grunt task, albeit somewhat lengthly and not really automated yet. Don't worry we will fix that soon.

Cool Trick:
Try running just grunt notify without the target. It will run both notify:welcome and notify:another. Essentially running any target under the notify task.

Tasks

The task is essentially an order the commander (you) gives. The order outlines which platoon (target configurations) should go to the battlefield to run their attack plans.

In other words, a task is at its most basic a shortcut to run a list of configurations. Tasks are loaded by using the function grunt.registerTask('shortcutName', ['runFirst', 'runSecond']);. It includes the task name or as I like to call it the shortcut, followed by a list of tasks to run in a specific order.

So how would we setup a task that would run the welcome target from notify and afterwards run the compile target for compass? It would probably look something like this.

    grunt.registerTask('custom', ['notify:welcome', 'watch:compass']);

This code now lets us just run grunt custom and now it will fire off the list of tasks being notify:welcome first followed by watch:compass second. This is a simple example of chaining tasks together.

Additionally, the registerTask function has a lot of power in it. It can run a callback method or anonymous function like so:

grunt.registerTask('custom', ['notify:welcome', 'watch:compass'], function () {
    grunt.log.write('Just a simple debug to console command');
});

Allowing you to create some extra functionality where a task would be unnecessary. It is certainly helpful in debugging more advanced setups. At this point your Gruntfile should look like this.

module.exports = function(grunt) {

  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),

    // Watch plugin config
    watch: {
      compass: {
          files: '**/*.scss',
          tasks: ['compass:compile']
      }
    },

    // Notify plugin config
    notify: {
      welcome: {
          options: {
              title: "Hello there <%= pkg.name %>", // Note we are outputting the package.json name variable here
              message: "My name is Grunt"
          }
      },
      another: {
          options: {
              title: "Just another notify config",
              message: "I will be important later on"
          }
      }
    },

    // Compass plugin config
    compass: {
      compile: {
          options: {
              sassDir: 'sass',
              cssDir: 'css'
          }
      }
    }
  });

  // Loading of plugins
  grunt.loadNpmTasks('grunt-contrib-compass');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-notify');

  // Default task(s).
  grunt.registerTask('default');

  // Our custom task with callback
  grunt.registerTask('custom', ['notify:welcome', 'watch:compass'], function () {
      grunt.log.write('Just a simple debug to console command');
  });

};

There is nothing wrong with this setup. It is actually totally functionally and usable at this point. However, say we were to add 3 or 4 more tasks and 3 or 4 more plugins. You can imagine that this file can get quite big in a hurry. This leads us to the next step which is to abstract some the logic to make it cleaner to manage multiple plugins and multiple tasks.

Optimize Gruntfile.js with load-grunt-config

There are a number of popular plugins out there to help organize Gruntfile's. The one I always kept running into was load-grunt-tasks.

Load grunt tasks automatically loads all of your plugins. So instead of typing grunt.loadNpmTasks('...'); half a hundred times you would just use a single line of code require('load-grunt-tasks')(grunt);. While this is really useful it unfortunately still leaves the configuration settings in the Gruntfile which is typically the bulk of the content.

Enter load-grunt-config

Load grunt config takes the awesome loading cabilities of load-grunt-tasks and adds the ability to abstract configurations and task defintions to external files. This makes managing your Grunt setup a lot easier. Run the following to install it npm i --save-dev load-grunt-tasks.

We now can use a better folder structure for our grunt plugin configurations. Load-grunt-config enables the usage of a folder called grunt/ where you can now place files where the file name is actually the taskname.

For example lets abstract the notify logic into its own file called notify.js inside the grunt/ folder. It should look like this.

module.exports = function (grunt) {

    // Variables, Custom tasks, Etc....

    // Return the configurations
    return {
        welcome: {
          options: {
              title: "Hello there <%= pkg.name %>", // Note we are outputting the package.json name variable here
              message: "My name is Grunt"
          }
        },
        another: {
          options: {
              title: "Just another notify config",
              message: "I will be important later on"
          }
        }
    };
};

As you can see we have now removed the configuration logic from the main Gruntfile into a plugin specific file. Now it is much easier to maintain plugin options and settings without having to search for them.

Next we can abstract task definitions into a special file within the grunt folder called aliases.yaml. This file allows you to easily define task lists.

Quick Tip:
The aliases file also support js, json, coffee as well as the above yaml.

Lets take a look at what our aliases file should look like after abstracting out our tasks. It should look pretty simple in comparison.

default:
    - 'notify:welcome'

custom:
    - 'notify:welcome'
    - 'watch:compass'

Conclusion

So lets see what we have accomplished so far. We have setup node as well as grunt. There were a number of grunt plugins installed as devDependencies. All grunt plugins are automatically loaded via load-grunt-config. And finally we have abstracted our plugin configurations and task definitions into separate files.

We now have a highly efficient grunt setup configured. Adding new tasks and/or plugins should be a breeze from here on forward. I highly suggest looking more into load-grunt-config as there are a number of features to making your life easier in the world of task runners.

Did I miss something? Have a favorite grunt plugin? I would love to hear your feedback and comments.

« Previous Post
Advanced Techniques - Part 2
Next Post »
Getting up and Running with Minitest, Guard, Sass, and Bootstrap for Ruby on Rails Development

Join the conversation

comments powered by Disqus