Writing an Awesome Build Script with Grunt

Landon Schropp

Grunt is a fantastic build system for web development, but it can be tricky to set up. In this guide, you’ll learn to configure Grunt to build a modern web project. When you’re done, your Gruntfile will be able to:

  • Copy files from a source directory to a build directory
  • Remove build files
  • Compile Stylus files and add vendor prefixes to them
  • Compile CoffeeScript
  • Minify CSS and JavaScript
  • Compile Jade
  • Automatically build source files when they’re modified
  • Run a development server

Getting started

If you haven’t already done so, install Node.js and NPM. You also need to install the Grunt command line interface by running npm install -g grunt-cli. This allows you to run the grunt command from anywhere on your system.

Create a package.json with the content below.

json 
{
  "name": "grunt_tutorial",
  "description": "An example of how to set up Grunt for web development.",
  "author": "Landon Schropp (http://landonschropp.com)",
  "dependencies": {
    "grunt": "0.x.x",
    "grunt-autoprefixer": "0.2.x",
    "grunt-contrib-clean": "0.5.x",
    "grunt-contrib-coffee": "0.7.x",
    "grunt-contrib-connect": "0.4.x",
    "grunt-contrib-copy": "0.4.x",
    "grunt-contrib-cssmin": "0.6.x",
    "grunt-contrib-jade": "0.8.x",
    "grunt-contrib-jshint": "0.6.x",
    "grunt-contrib-stylus": "0.8.x",
    "grunt-contrib-uglify": "0.2.x",
    "grunt-contrib-watch": "0.5.x"
  },
  "engine": "node >= 0.10"
}

This file defines your project as an NPM package and declares your project’s dependencies. Each dependency has a version number. For example, grunt-contrib-copy: "0.4.x" tells NPM to install the latest 0.4 version of the grunt-contrib-copy package. Run npm install in your console to install the dependencies.

Copy

A good build script always keeps the source code separate from the build files. This separation allows you to destroy the build without affecting your source and prevents you from accidentally editing the build.

To get started, you’ll make Grunt copy the files from a source directory to a build directory. Create a Gruntfile.js file and paste the following into it:

javascript
module.exports = function(grunt) {

  // configure the tasks
  grunt.initConfig({

    copy: {
      build: {
        cwd: 'source',
        src: [ '**' ],
        dest: 'build',
        expand: true
      },
    },

  });

  // load the tasks
  grunt.loadNpmTasks('grunt-contrib-copy');

  // define the tasks
};

Let’s break this down. In Node, when you require a module, the modules.exports function is called and the result is returned. By setting modules.exports` in the Gruntfile, you’re telling Node to return a function that defines the Grunt configuration. grunt.initConfig is a method which takes one argument: an object whose properties configure individual Grunt tasks.

Inside the Grunt configuration, you’ve added the configuration for a copy task. This task has one subtask, called build. In Grunt, some tasks, called multi-tasks, can have several subtasks which can be called separately. For copy, you don’t need this feature, but it’s still required to have at least one subtask.

Inside the build subtask is Grunt’s files array format. This format is one of the ways Grunt allows you to provide the source files to a task. cwd points to a directory the source files are relative to, and src specifies the source files. '**' is a globbing pattern that tells Grunt to match any file. dest is where Grunt will output the result of the task. You’ve set it to "build" to tell grunt to copy the content to the build directory. If there is a source/index.html file, this configuration will output build/index.html. Finally, you set the expand paramter to true to enable all of these options.

grunt.loadNpmTasks("grunt-contrib-copy"); tells Grunt to load the tasks from the grunt-contrib-copy package. This gives us a copy command, which you can run by typing grunt copy into your console.

Clean

Now that you have a build directory, it’s time to write a task that wipes it clean. After the copy configuration, add the following:

 javascript
clean: {
  build: {
    src: [ 'build' ]
  },
},

Just like copy, you have a clean target with the task’s configuration. The src of the clean configuration is set to "build" to remove the build directory.

After grunt.loadNpmTasks("grunt-contrib-copy");, load the clean task, which will allow you to run grunt clean from the console.

 javascript
grunt.loadNpmTasks('grunt-contrib-clean');

Build

Wouldn’t it be great if you had a build task that would remove your old build before copying over the new source files? Let’s add one!

 javascript
// define the tasks
grunt.registerTask(
  'build', 
  'Compiles all of the assets and copies the files to the build directory.', 
  [ 'clean', 'copy' ]
);

The registerTask method creates a new task. The first argument, "build", defines the name of the task. The second is a description of the task. The last is an array of tasks that will be run. The build task runs the clean task followed by the copy task.

Stylus

Stylus is a nifty language that compiles to CSS. It enhances CSS in several ways, including adding variables, nesting and functions.

 javascript
stylus: {
  build: {
    options: {
      linenos: true,
      compress: false
    },
    files: [{
      expand: true,
      cwd: 'source',
      src: [ '**/*.styl' ],
      dest: 'build',
      ext: '.css'
    }]
  }
},

This is slightly different than the other task configurations. There’s still a build subtask, but it now has two properties: options and files. options specifies how we want the task to behave. We’ve added two options: compress determines if the CSS output should be compressed and linenos adds the line numbers of the selectors in the source Stylus files.

files takes the same file array mapping format as before. This will run the task on all the files in the source directory that end with .styl. ext changes the extension of the output files to .css.

Now that the stylus task outputs the CSS files to the build directory, there’s no reason to have copy the Stylus files to the build directory anymore. Let’s modify the copy configuration to prevent that.

 javascript
copy: {
  build: {
    cwd: 'source',
    src: [ '**', '!**/*.styl' ],
    dest: 'build',
    expand: true
  },
},

The ! at the beginning of the path prevents grunt from including files that match the pattern. Don’t forget to add "stylus" to the build task.

 javascript
grunt.registerTask(
  'build', 
  'Compiles all of the assets and copies the files to the build directory.', 
  [ 'clean', 'copy', 'stylus' ]
);

Autoprefixer

Autoprefixer is a plugin that adds vendor prefixes to CSS3 properties after the Stylus files are compiled to CSS. It’s a great replacement for libraries like Nib and Compass.

Go ahead and add the autoprefixer configuration.

 javascript
autoprefixer: {
  build: {
    expand: true,
    cwd: 'build',
    src: [ '**/*.css' ],
    dest: 'build'
  }
},

Noticing a pattern? This configuration is very similar to the other tasks. One notable difference is cwd and dest are both set to "build". This makes autoprefixer output the files to the same folder that it reads them from, which replaces the original files.

As before, you also need to load the Autoprefixer task.

 javascript
grunt.loadNpmTasks('grunt-autoprefixer');

Rather than shove all of the CSS tasks into build, create a new task for stylesheets and add that task to build.

 javascript
// define the tasks
grunt.registerTask(
  'stylesheets', 
  'Compiles the stylesheets.', 
  [ 'stylus', 'autoprefixer' ]
);

grunt.registerTask(
  'build', 
  'Compiles all of the assets and copies the files to the build directory.', 
  [ 'clean', 'copy', 'stylesheets' ]
);

CSS Minification

Passing a bunch of bulky CSS files to the client can really slow down a website’s loading time. Luckily, the grunt-contrib-cssmin package minifies CSS files and combines them into a single file. Once again, start with the configuration.

 javascript
cssmin: {
  build: {
    files: {
      'build/application.css': [ 'build/**/*.css' ]
    }
  }
},

Instead of using the files array format, this configuration uses Grunt’s files object format, which maps several files to a single destination. All of the CSS files in the build directory will be minified and output to build/application.css.

Load the package and add the CSS minification to the stylesheets task.

 javascript
grunt.loadNpmTasks('grunt-contrib-cssmin');
 javascript
grunt.registerTask(
  'stylesheets', 
  'Compiles the stylesheets.', 
  [ 'stylus', 'autoprefixer', 'cssmin' ]
);

CoffeeScript

CoffeeScript is a fantastic language that compiles to JavaScript. It has clean, beautiful syntax, includes classes and hides a lot of JavaScript’s uglier aspects.

Adding CoffeeScript to the project is easy! First, add the configuration.

 javascript
coffee: {
  build: {
    expand: true,
    cwd: 'source',
    src: [ '**/*.coffee' ],
    dest: 'build',
    ext: '.js'
  }
},

This pulls in the source CoffeeScript files, changes their extensions to .js and outputs them to the build directory. Next, load the grunt-contrib-coffee package.

 javascript
grunt.loadNpmTasks('grunt-contrib-coffee');

Add a scripts task and add that to the build task.

 javascript
grunt.registerTask(
  'scripts', 
  'Compiles the JavaScript files.', 
  [ 'coffee' ]
);

grunt.registerTask(
  'build', 
  'Compiles all of the assets and copies the files to the build directory.', 
  [ 'clean', 'copy', 'stylesheets', 'scripts' ]
);

Once again, you’ll need to add an exception to copy so the CoffeeScript files are not copied into the build directory.

 javascript
copy: {
  build: {
    cwd: 'source',
    src: [ '**', '!**/*.styl', '!**/*.coffee' ],
    dest: 'build',
    expand: true
  },
},

Uglify

Like cssmin, UglifyJS minifies JavaScript files and combines them into a single file. Here’s the configuration:

 javascript
uglify: {
  build: {
    options: {
      mangle: false
    },
    files: {
      'build/application.js': [ 'build/**/*.js' ]
    }
  }
},

By default, UglifyJS will replace the names of variables and functions in your scripts with shorter names. This is handy if your project’s code is self-contained, but if it’s shared with another project it can cause problems. Setting mangle to false turns off this behavior.

Like the cssmin task, this task also uses the files object format.

Load the package and add "uglify" to the scripts task.

 javascript
grunt.loadNpmTasks('grunt-contrib-uglify');
 javascript
grunt.registerTask(
  'scripts', 
  'Compiles the JavaScript files.', 
  [ 'coffee', 'uglify' ]
);

Cleaning up

When you run grunt build, in addition to build/application.css and build/application.js, all the other CSS and JavaScript files are hanging around in the build directory. Since you don’t need them, add subtasks to remove them to the clean configuration.

 javascript
clean: {
  build: {
    src: [ 'build' ]
  },
  stylesheets: {
    src: [ 'build/**/*.css', '!build/application.css' ]
  },
  scripts: {
    src: [ 'build/**/*.js', '!build/application.js' ]
  },
},

When running a task, if you don’t specify a subtask, Grunt will run them all. If you run grunt clean from the console, it will run clean:build, clean:stylesheets and clean:scripts. This isn’t a problem because if the clean task can’t remove a file, it just ignores it.

Notice how build/application.css and build/application.js are excluded from the stylesheets and scripts subtasks. You don’t want to delete those false after all of your hard work!

Update the tasks to use the appropriate subtasks.

 javascript
// define the tasks
grunt.registerTask(
  'stylesheets', 
  'Compiles the stylesheets.', 
  [ 'stylus', 'autoprefixer', 'cssmin', 'clean:stylesheets' ]
);

grunt.registerTask(
  'scripts', 
  'Compiles the JavaScript files.', 
  [ 'coffee', 'uglify', 'clean:scripts' ]
);

grunt.registerTask(
  'build', 
  'Compiles all of the assets and copies the files to the build directory.', 
  [ 'clean:build', 'copy', 'stylesheets', 'scripts' ]
);

Jade

Jade is a templating language that makes writing HTML fun. Add Jade to your project by using the grunt-contrib-jade package.

 javascript
jade: {
  compile: {
    options: {
      data: {}
    },
    files: [{
      expand: true,
      cwd: 'source',
      src: [ '**/*.jade' ],
      dest: 'build',
      ext: '.html'
    }]
  }
},

Like the stylus and coffee tasks, jade is configured using the files array format. Notice the data object inside options? This object is passed to each template when the Jade files are compiled. It’s handy for things such as creating separate development and production builds or generating dynamic content.

As before, you need to add an exception to the copy task to prevent Jade files from being copied.

 javascript
copy: {
  build: {
    cwd: 'source',
    src: [ '**', '!**/*.styl', '!**/*.coffee', '!**/*.jade' ],
    dest: 'build',
    expand: true
  },
},

Don’t forget to load grunt-contrib-jade and add it to `build`.

 javascript
grunt.loadNpmTasks('grunt-contrib-jade');
 javascript
grunt.registerTask(
  'build', 
  'Compiles all of the assets and copies the files to the build directory.', 
  [ 'clean:build', 'copy', 'stylesheets', 'scripts', 'jade' ]
);

Watch

Your Gruntfile is really starting to shine, but wouldn’t it be nice if you didn’t have to run grunt build every time you made a change? With grunt-contrib-watch, you don’t need to! Let’s configure a task that will watch your source code for changes and automatically build them.

 javascript
watch: {
  stylesheets: {
    files: 'source/**/*.styl',
    tasks: [ 'stylesheets' ]
  },
  scripts: {
    files: 'source/**/*.coffee',
    tasks: [ 'scripts' ]
  },
  jade: {
    files: 'source/**/*.jade',
    tasks: [ 'jade' ]
  },
  copy: {
    files: [ 'source/**', '!source/**/*.styl', '!source/**/*.coffee', '!source/**/*.jade' ],
    tasks: [ 'copy' ]
  }
},

The stylesheets, scripts and jade subtasks watch the Stylus, CoffeeScript and Jade files for changes and runs their respective tasks. The copy task watches all of the remaining files in the application and copies them to the build directory.

Again, you’ll need to load the grunt task.

 javascipt
grunt.loadNpmTasks('grunt-contrib-watch');

Development server

No web development environment is complete without a development server. The grunt-contrib-connect package is a full-featured static file server that’s perfect for your project.

 javascript
connect: {
  server: {
    options: {
      port: 4000,
      base: 'build',
      hostname: '*'
    }
  }
}

You’ve configured the server to host the build directory on port 4000. By default, Connect will only host the site on localhost, which restricts you from accessing the server outside of your computer. Setting hostname to "*" allows the server to be accessed from anywhere.

As before, you’ll also need to load the NPM task.

 javascript
grunt.loadNpmTasks('grunt-contrib-connect');

If you try to run grunt connect from the command line, the server runs and then stops right away. This is because by default the grunt connect task does not run indefinitely. You’ll learn how to fix this in the next section.

Default

Wouldn’t it be great if you had a task that combined all of the other tasks together in one? A default task is perfect for this.

 javascript
grunt.registerTask(
  'default', 
  'Watches the project for changes, automatically builds them and runs a server.', 
  [ 'build', 'connect', 'watch' ]
);

The default task runs `build` to create an initial build. Then it starts the Connect server. Finally, it runs watch to watch the files for changes and build them. Since watch runs until it’s killed, the Connect server will run indefinitely. Run grunt in your console and navigate to http://localhost:4000 to see your project!

Conclusion

We’ve covered a lot in this tutorial, there’s so much more Grunt can do. For a complete list of all of the plugins available to Grunt, check out the Grunt plugins site. Happy Grunting!

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

  • Anonymous

    would be nice to have a completed file to look at.

  • Anonymous

    This tut is missing a grunt.loadNpmTasks(‘grunt-contrib-stylus’);

  • Anonymous

    It’d also be cool to see livereload and middleware added like Yeoman has.

  • george

    Yeah complete file would have helped a lot.

  • Anonymous

    For anyone who wants it, I’ve thrown up the complete Gruntfile on Gist: https://gist.github.com/LandonSchropp/6816538

  • George

    Thanks for the gist. I think adding livereload would be cool too. I can never get that to work properly :(

  • Anonymous

    I would’ve loved to include LiveReload (and Jasmine!) in the article, but it started to get pretty long. I’ve been thinking about writing a part 2…

  • markdesign

    Can’t wait to see part two!

  • http://memeLab.com.au/ Tim Osborn

    Best grunt intro yet!