Theme development with Grunt: LiveReload, lint, uglify, oh my!

Grunt calls itself "The JavaScript Task Runner." Huh? What's a JavaScript task runner?

The JavaScript part comes from its dependency on using Node.js. The task runner is another way of saying automation. So what we have is an automation tool built on top of Node.js.

What can it do, and why do I need it?

As a Drupal themer/developer, I'm all about being lazy. Lazy in that I don't like doing the same thing over and over again. It compiles & watches for SASS changes. It LiveReloads. It lints, minifies & combines JavaScript and/or CSS files. It copies library assets. It can minify & combine images. Plus tons more. Pretty much any repetitive task can accomplished with Grunt.

Install Node.js/npm & Grunt

First things first. You gotta install Grunt. It is installed from npm (Node.js package manager). Node.js install instructions.

Once Node.js & npm are installed, in a terminal, run (might need to use sudo):

$ npm install -g grunt-cli

Grunt's files

A Grunt project consists of two files: package.json & Gruntfile.js. package.json is used by npm to install the Grunt plugins. The Gruntfile.js (or Gruntfile.coffee if you prefer) is the config file where we setup our tasks. Both of these files should be placed at the root of your theme.

package.json

Start with a simple package.json file, and you can customize it to your specific project:

{ 
	"name": "my-project-name", 
	"version": "0.1.0", 
	"devDependencies": { 
	} 
}

Once this initial stub is in place, you need to add plugins. The easiest way is to run the following:

$ npm install [plugin] --save-dev 

where [plugin] is the name of the plugin you want installed. The --save-dev argument tells npm to not only install the plugin locally, but also adds it to the devDependencies section of the pavkage.json file. At the bare minimum, you'll need to install grunt itself as a plugin:

$ npm install grunt --save-dev 

The Grunt website has a rather extensive set of plugins you can use. Here's an example of a package.json file I used recently:

{ 
	"name": "my-awesome-theme", 
	"version": "1.0.0", 
	"devDependencies": { 
		"grunt": "~0.4.1", 
		"grunt-contrib-compass": "~0.5.0", 
		"grunt-contrib-jshint": "~0.6.2", 
		"grunt-contrib-uglify": "~0.2.2", 
		"grunt-contrib-watch": "~0.5.1", 
		"grunt-contrib-copy": "~0.4.1" 
	}
} 

Gruntfile.js

Like I said above, the Gruntfile.js is the config file where you setup all your tasks. Here is a Gruntfile.js stub that you can use as a starter:

'use strict'; 

module.exports = function (grunt) { 
	grunt.initConfig({ 
		pkg: grunt.file.readJSON('package.json'), 
	}); 
}; 

Next, you'll add and configure tasks. For this example, I'm using JSHint which validates (lints) JavaScript files.

Here's a Gruntfile.js file with JSHint configured:

'use strict'; 

module.exports = function (grunt) { 
	
	grunt.initConfig({ 
		pkg: grunt.file.readJSON('package.json'), 
		
		jshint: { 
			options: { 
				jshintrc: '.jshintrc' 
			}, 
			all: [ 
				'js/src/*.js', 
				'!js/src/*.min.js' 
			] 
		} 
	}); 
	
	grunt.loadNpmTasks('grunt-contrib-jshint'); 
	
	grunt.registerTask('default', [
		'jshint'
	]); 
};

There are three things that were added to the file.

First is the jshint block of code. Within this block is an "options" section that allows you to specify "global" options that will be used anytime the jshint task is invoked. In this case, the jshintrc: '.jshintrc' allows you to specify where the jshint config file is located.

Also in this code block is a "dev" section and a "build" section. The section identifier is simply a way to reference which set of files (or options) to use.

As you'll see below, you can use these set (or target) identifiers like so:

$ grunt jshint:dev 

or

$ grunt jshint:build 

The next part that was added is the loadNpmTasks() function. This loads the plugin into Grunt. You'll need to specify each plugin that you use in the Gruntfile.

Finally, the registerTask() function is used to create chainable tasks. This allows you to create a task that does multiple things at once. For example, with one command, you can lint your JavaScript files, combine them into a single file, then minimize the single file, all the while putting the rendered file in a location completely different from your source files.

Here's a much longer and extensive Gruntfile:

'use strict';

module.exports = function (grunt) {

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

    // Watch for changes and trigger compass, jshint, uglify and livereload
    watch: {
      compass: {
        files: ['sass/{,**/}*.scss'],
        tasks: ['compass:dev']
      },
      js: {
        files: '<%= jshint.all %>',
        tasks: ['jshint', 'uglify:dev']
      },
      livereload: {
        options: {
          livereload: true
        },
        files: [
          'css/style.css',
          'js/*.js',
          'images/{,**/}*.{png,jpg,jpeg,gif,webp,svg}'
        ]
      }
    },

    // Compass and scss
    compass: {
      options: {
        bundleExec: true,
        httpPath: '/sites/all/themes/my-theme',
        cssDir: 'css',
        sassDir: 'sass',
        imagesDir: 'images',
        javascriptsDir: 'js',
        fontsDir: 'css/fonts',
        assetCacheBuster: 'none',
        require: [
          'sass-globbing'
        ]
      },
      dev: {
        options: {
          environment: 'development',
          outputStyle: 'expanded',
          relativeAssets: true,
          raw: 'line_numbers = :true\n'
        }
      },
      dist: {
        options: {
          environment: 'production',
          outputStyle: 'compact',
          force: true
        }
      }
    },

    // Javascript linting with jshint
    jshint: {
      options: {
        jshintrc: '.jshintrc'
      },
      all: [
        'js/src/*.js',
        '!js/src/*.min.js'
      ]
    },

    // Concat & minify
    uglify: {
      dev: {
        options: {
          mangle: false,
          compress: false,
          preserveComments: 'all',
          beautify: true
        },
        files: {
          'js/vendor.js': [
            'components/eventEmitter/EventEmitter.js',
            'components/eventie/eventie.js',
            'components/imagesloaded/imagesloaded.js',
            'components/formalize/assets/js/jquery.formalize.js'
          ],
          'js/script.js': [
            'js/src/*.js',
            '!js/src/*.min.js'
          ]
        }
      },
      dist: {
        options: {
          mangle: true,
          compress: true
        },
        files: {
          'js/vendor.js': [
            'components/eventEmitter/EventEmitter.js',
            'components/eventie/eventie.js',
            'components/imagesloaded/imagesloaded.js',
            'components/formalize/assets/js/jquery.formalize.js'
          ],
          'js/script.js': [
            'js/src/*.js',
            '!js/src/*.min.js'
          ]
        }
      }
    },

    // Copy files
    copy: {
      main: {
        files: [
          {
            expand: true,
            cwd: 'components/formalize/assets/css/',
            src: '_formalize.scss',
            dest: 'sass/partials/vendor/',
            filter: 'isFile',
            flatten: true
          },
          {
            expand: true,
            cwd: 'components/formalize/assets/images/',
            src: '**',
            dest: 'images/',
            filter: 'isFile',
            flatten: true
          }
        ]
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-compass');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-copy');

  grunt.registerTask('build', [
    'copy',
    'jshint',
    'uglify:dist',
    'compass:dist'
  ]);

  grunt.registerTask('default', [
    'copy',
    'jshint',
    'uglify:dev',
    'compass:dev',
    'watch'
  ]);

};

There are two tasks that I've created. The "default" task is what I leave running all the time. Since it is the default, you can start it by simply running:

$ grunt 

No arguments are required if you create a default task. First, it copies files from third party libraries that I've specified. Next, it checks my JavaScript files for any issues. Then is uses the "dev" section of uglify to combine JavaScript files into a single file. Next it uses the compass plugin to compile all the .scss files. Finally, it starts the watch plugin, which doesn't terminate (until you manually stop it with a Ctrl+C). Any time the "watch" plugin detects a change in one of my .scss files, it runs the compass plugin to recompile .scss partials into .css. It also JSHints whenever a change is saved. And finally (and certainly not least) it LiveReloads the browser.

Whew! That was a lot of things to do. But Grunt handled it marvelously. Pretty cool, eh?

I've also setup a "build" task that can be used when development is completed and you want your files optimized. Run it by also specifying the task like so:

$ grunt build 

More info

For more information, please see the getting started page as well as these additional resources.

I've also setup a starter repo over at GitHub that you can use when creating new themes so you can get started quicker.

Have fun!

Topics