Writing an Awesome Build Script with Grunt

Share this article

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

Key Takeaways

  • Grunt Setup: Begin by installing Node.js, NPM, and the Grunt CLI. Set up your project as an NPM package and specify dependencies in `package.json` to ensure all necessary Grunt plugins are installed.
  • Task Configuration: Utilize `grunt.initConfig` to define tasks such as copying files, cleaning directories, and compiling Stylus, CoffeeScript, and Jade files. Each task can have multiple configurations using subtasks.
  • Automation and Efficiency: Integrate plugins like Autoprefixer for CSS, Uglify for JavaScript minification, and CSSMin for CSS compression to optimize output files, enhancing the performance of the final product.
  • Development Workflow: Implement the `watch` task to monitor changes in the source files and automatically rebuild the project, and use `grunt-contrib-connect` for running a local development server, accessible from any device on the network.
  • Build Customization: Create custom tasks using `grunt.registerTask` to sequence operations like cleaning up directories, compiling assets, and copying files, allowing for a modular and maintainable build script that can be easily extended or modified.

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!

Frequently Asked Questions (FAQs) about Writing Awesome Build Scripts with Grunt

What is Grunt and why is it important in writing build scripts?

Grunt is a JavaScript task runner that automates repetitive tasks like minification, compilation, unit testing, and linting. It’s built on Node.js and uses a command-line interface to run custom tasks defined in a file known as the Gruntfile. Grunt is crucial in writing build scripts because it simplifies the process of managing and automating tasks, thus increasing productivity and reducing the chances of errors.

How do I install Grunt on my system?

To install Grunt, you first need to have Node.js and npm (Node Package Manager) installed on your system. Once you have these, you can install Grunt by running the command npm install -g grunt-cli in your terminal. This installs the Grunt command line interface (CLI) globally on your system.

What is a Gruntfile and how do I create one?

A Gruntfile is a JavaScript or CoffeeScript file that sits in the root directory of your project. It’s where you define and configure tasks that Grunt will run. To create a Gruntfile, create a new file in your project root directory and name it Gruntfile.js. Inside this file, you’ll define your tasks and configurations.

How do I define tasks in a Gruntfile?

Tasks in a Gruntfile are defined within the grunt.initConfig() method. Each task is a property of this method and can have multiple targets. Each target can have its own set of configuration options. For example, a simple task to minify JavaScript files might look like this:

grunt.initConfig({
uglify: {
my_target: {
files: {
'dest/output.min.js': ['src/input1.js', 'src/input2.js']
}
}
}
});

How do I run Grunt tasks?

To run Grunt tasks, you use the grunt command followed by the name of the task. For example, to run the uglify task defined in the previous question, you would use the command grunt uglify in your terminal.

Can I run multiple Grunt tasks at once?

Yes, you can run multiple Grunt tasks at once by passing multiple task names to the grunt command. For example, grunt task1 task2 task3. You can also define a default task that runs multiple tasks by using the grunt.registerTask() method.

How do I use plugins in Grunt?

Grunt has a wide range of plugins that can be used to perform various tasks. To use a plugin, you first need to install it using npm, for example npm install grunt-contrib-uglify --save-dev. Then, you load it in your Gruntfile using the grunt.loadNpmTasks() method.

How do I handle errors in Grunt?

Grunt has a built-in error handling mechanism. If a task encounters an error, Grunt will immediately stop executing and display the error message in the terminal. You can also use the --force option to force Grunt to continue executing tasks even if one fails.

Can I customize the Grunt CLI options?

Yes, you can customize the Grunt CLI options by using flags. For example, the --verbose flag will display detailed logging information, while the --no-color flag will disable colored output.

How do I keep my Gruntfile organized?

As your project grows, your Gruntfile can become quite large and difficult to manage. To keep it organized, you can split your tasks into separate files and folders using the load-grunt-config plugin. This allows you to maintain a clean and organized Gruntfile, regardless of the size of your project.

Landon SchroppLandon Schropp
View Author

Landon is a freelance developer specializing in web and mobile applications. He's passionate about building simple things people love to use.

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week