This article is a Hall of Shamer™ for irrelevance. Webpack & NPM Scripts have made Grunt useless to me.
In this article I'm going to show you how to leverage Grunt.js to completely refactor your workflow. Follow in the footsteps of some of the most prolific open source projects in the world and leave the grunt work to Grunt.js.
Grunt.js is a fantastic task-based command line tool written in JavaScript on top of the wonderful Node.js platform. You can leverage Grunt.js to script away all of your grunt work. Tools and procedures that you historically ran and configured yourself, you can now abstract behind a convention based command line interface with a consistent means of configuration. You can write your most complicated tasks once and leverage them in all of your projects using project specific configuration.
What are these tasks I keep referencing? Well, really thats up to you, but a few of the most common ones are concatenating files, linting and testing your code, and minification. Grunt.js doesn't limit you to JavaScript specific tasks either, because Grunt.js is built on top of Node.js you can leverage all the power of Node in your tasks. Even if your tool isn't implemented in JavaScript we can defer tasks to child processes, using their command line interface or even a web service. For example, the grunt-contrib-compass task, which allows you to compile your SCSS/SASS stylesheets using the excellent Compass framework (a ruby program), is implemented using the Compass provided command line interface. The point is that just about any tool can be abstracted behind a Grunt task.
Grunt.js provides a consistent interface for configuring and using any task.
Grunt.js allows you to run your tasks when monitored files change or manually using the command line. It also allows you to aggregate tasks using aliases, this is a very powerful feature which allows you to abstract the details of a more generic task. For example, one might abstract linting, testing, concatenating, and minification behind a task named "build".
Grunt.js has a strong and rapidly growing community. The barrier to entry for using and customizing Grunt is so minimal that there is already a whole bunch great tasks available for use today. Take a look at the bountiful plugin landscape.
Grunt.js provides all the power of Node.js for your tasks. It also provides some very fundamental and powerful abstractions for common needs, like working with the file system or logging to the standard out (stdout).
Lots of excellent projects are leveraging Grunt.js. To name a few:
The project structure we are going to use in this example is straight forward. We have a folder called "src", which has our application's source code and a folder called "test" that has our application's test. This is a Mocha driven test suite.
First things first, lets get Grunt installed and integrated into the project so we can start adding useful tasks.
Lets install the Grunt CLI globally so we can access the "grunt" command. To install Grunt run the following command. I assume you already have Node.js installed. The job of the grunt command is to load and run the version of Grunt you have installed locally to your project, irrespective of its version. If you have some projects using Grunt 0.4 and others using Grunt 0.3, the grunt-cli will select and run the proper grunt installation.
npm install grunt-cli -g
You can check and make sure Grunt is installed correctly by asking the program for it's version.
grunt --version
Time to integrate Grunt with our project. First lets create a package.json file so we can quickly track and install our applications dependencies. What dependencies? Well, we will need to add Grunt.js as one. Soon we will add our third party Grunt tasks here too!
Having a package.json file is not required to use Grunt, however it makes managing our third party tasks a lot easier! You will see what I mean in a little bit. For now, we can create a package.json at the root of our project with the following contents.
{
"name": "Example",
"version": "0.0.1",
"private": true,
"devDependencies": {
"grunt": "latest"
}
}
If you already have a package.json file and you want to add Grunt to your project. You can do that by running the following command:
npm install grunt-cli --save-dev
Terrific now it is time to create our Gruntfile! A Gruntfile is a JavaScript file that Grunt leverages to understand your projects tasks and configuration. When you run "grunt" from the command line, Grunt will recurse upwards till it finds your Gruntfile. This functionality allows you to run Grunt from any sub directory of your project.
This is the basis of a Grunt file, it's a wrapper function that takes in "grunt" as an argument. This allows us to register tasks and configuration with grunt (and leverage Grunt's APIs) before Grunt actually runs any tasks. Think of this as an entry point of sorts for Grunt.
module.exports = function (grunt) {
// set up grunt
};
Well done, you now have Grunt installed. Go ahead, and run "grunt" from your command line at the root of your project. You will see a warning that it can't find a default task. Very well then, lets go about adding some tasks!
$ grunt
Warning: Task "default" not found. Use --force to continue.
Aborted due to warnings.
Wouldn't it be great if we could run our JavaScript code through JSHint and get all sorts of great feedback on making our code more consistent and less buggy? Fortunately, Grunt has a few common tasks built right in, code linting with JSHint is one of them. Lets modify our Gruntfile to get this working!
First install the JSHint task by running this command:
npm install grunt-contrib-jshint --save-dev
/*global module:false*/
module.exports = function (grunt) {
grunt.initConfig({
jshint: {
src: [
"Gruntfile.js",
"src/app/**/*.js",
"src/config.js",
"tests/app/**/*.js",
],
options: {
curly: true,
eqeqeq: true,
immed: true,
latedef: true,
newcap: true,
noarg: true,
sub: true,
undef: true,
boss: true,
eqnull: true,
browser: true,
globals: {
require: true,
define: true,
requirejs: true,
describe: true,
expect: true,
it: true,
},
},
},
});
// Load JSHint task
grunt.loadNpmTasks("grunt-contrib-jshint");
// Default task.
grunt.registerTask("default", "jshint");
};
Here we install and configure the "jshint" task telling it which files to lint using minimatch style globbing. We also configure JSHint to our liking, adding globals that are ok and making sure it only complains when we violate our decided coding standards. We then create an alias called "default", and we tell it to run the "jshint" task.
Go ahead and run grunt again…
$ grunt
Running "jshint:src" (jshint) task
Lint free.
Done, without errors.
We can also run the jshint task directly!
$ grunt jshint
Running "jshint:src" (jshint) task
Lint free.
Done, without errors.
Wouldn't it be great if we could lint our code automatically every time one of our linted files changes? Thats easy enough thanks to the grunt "watch" task. Let's install and add some "watch" configuration to your grunt.initConfig call.
First we install it:
npm install grunt-contrib-watch --save-dev
Then we configure and load it:
grunt.initConfig({
// …
watch: {
files: "<%= jshint.src %>",
tasks: ["jshint"],
},
// …
});
grunt.loadNpmTasks("grunt-contrib-watch");
This tells the grunt "watch" task (a built in task), to run the "lint" task every time one of the configuration specified lint files changes! The sharp reader will notice the use of lodash templates to reference the "lint" configuration from "watch".
While code linting is a great tool to try an manage code quality it hardly provides the confidence we need to ship our application to production. Lets set up Grunt to run our Mocha specs in PhantomJS, that should give us a bit more confidence before shipping.
Before we set out to write our own Grunt task for Mocha, lets check NPM and make sure nobody else has implemented one already.
$ npm search gruntplugin mocha
NAME DESCRIPTION
grunt-mocha Grunt task for running Mocha specs
grunt-mocha-test A grunt task for running server side mocha tests
grunt-simple-mocha A simple wrapper for running tests with Mocha.
istanbul-grunt-mocha Grunt task for running Mocha specs, writing istanbul code
What do you know!? Looks like someone has already created a Grunt task to run Mocha specs! Lets go ahead and leverage it!
$ npm install grunt-mocha -D
This command installs grunt-mocha and saves it as a development dependency in our package.json. This is useful when someone else clones our repository; instead of tracing down our dependencies manually, they can simply run "npm install" and NPM will download the correct version of grunt-mocha for them!
Now that grunt-mocha is installed we need to load it into our project using the grunt.loadNpmTasks method, which allows us to load in grunt tasks from NPM installed dependencies.
// …
modules.exports = function(grunt) {
grunt.initConfig(…);
grunt.loadNpmTasks('grunt-mocha');
};
Now that Grunt is loading in grunt-mocha, lets configure it to run our tests.
grunt.initConfig({
// …
mocha: {
all: ["tests/index.html"],
},
// …
});
The tests/index.html file is a basic Mocha test runner, much like the one you see if you run "mocha init" from the command line. Now we can execute our mocha tests in PhantomJS from Grunt.
$ grunt mocha
Running "mocha:all" (mocha) task
Testing index.html.OK
>> 1 assertions passed (0.04s)
Pretty cool, eh? With, 3 lines of configuration and a method call we are running our tests in PhantomJS. Lets add the mocha task to our watch command so each time a linted file changes we execute our tests as well as lint our code.
grunt.initConfig({
// …
watch: {
files: <%= jshint.src %>,
tasks: ['jshint', 'mocha']
},
// …
});
Now when we execute "grunt watch" and change a file we get immediate feedback on our tests and code quality.
$ grunt watch
Running "watch" task
Waiting...OK
>> File "Gruntfile.js" renamed.
Running "jshint:src" (jshint) task
Lint free.
Running "mocha:all" (mocha) task
Testing index.html.OK
>> 1 assertions passed (0.04s)
Pretty sweet, huh!?
Obviously Grunt.js isn't limited to code quality tasks. To demonstrate how generic a Grunt.js task can be lets write a custom task to compliment us every time we run grunt.
Lets start out by registering a task called "compliment".
module.exports = function (grunt) {
// …
grunt.registerTask("compliment", function () {
grunt.log.writeln("You are so awesome!");
});
// …
};
With a call to register task, and simple call back function we have a custom Grunt task. We can run it from the command line directly just like we ran lint or mocha.
$ grunt compliment
Running "compliment" task
You are so awesome!
Done, without errors.
Well, thank you Grunt. I think you are awesome too.
Wouldn't it be great if users could customize their compliments using their grunt configuration and have a random compliment each time? Simple enough…
grunt.initConfig({
//...
compliment: [
"You are so awesome!",
"You remind me of Brad Pitt, only you have a better body.",
"You are a funny, funny kid.",
],
});
grunt.registerTask("compliment", "Treat yo' self!", function () {
var defaults = ["No one cares to customize me."];
// Notice we use the grunt object to retrieve configuration.
var compliments = grunt.config("compliment") || defaults;
var index = Math.floor(Math.random() * compliments.length);
grunt.log.writeln(compliments[index]);
});
Observant readers will note that I added a description argument, this is used when consumers run grunt -h…
$ grunt -h
…
compliment Treat yo' self!
…
We pull the compliment array out of the configuration using the grunt.config function, we then select a random compliment and echo it to the user. Lets add this to our default just before lint and test.
// …
grunt.registerTask("default", ["compliment", "jshint", "mocha"]);
Now when we run "grunt" we get a fresh compliment. My day is already starting to look up.
$ grunt
Running "compliment" task
You are a funny, funny kid.
Running "jshint:src" (jshint) task
Lint free.
Running "mocha:all" (mocha) task
Testing index.html.OK
>> 1 assertions passed (0.04s)
Done, without errors.
If you are interested in writing Grunt tasks others can use, see grunt-compliment where I refactored this "compliment" task and published it NPM.
Grunt is a powerful tool and I've only scratched the surface in this article. Try to leverage it in your next project and if it doesn't completely refactor your workflow email me and tell me why. Leave the grunt work to Grunt.js so your mind and time can be liberated for the hard problems.