Expanding Your Toolset #2: Grunt - The Javascript Task Runner
This is the second post in “Expanding Your Toolset” series (previous post was Expanding Your Toolset: Bower - Package Management For The Web). Last time we’ve looked at Bower - a package manager for the web. Bower is useful for quickly resolving dependencies for your project (e.g. if you need a specific version of jQuery, AngularJS, UnderscoreJS and etc.).
GruntJS is a task runner. It’s primary purpose is to automate the tedious stuff we as developers have to do. For example, when developing for the web it’s usually a good idea to minify and combine CSS and Javascript. Do you do this by hand? Or maybe you have a custom shell script to do that for you? Been there, done that. In any case using GruntJS will make it easier and quicker to accomplish such tasks. The community has written many plugins for it (e.g. JSHint, Sass, Less, CoffeeScript, RequireJS and etc.), thus it may be really beneficial for your current (or future) projects.
Installing Grunt
GruntJS is a Javascript library and it runs on NodeJS. It’s distributed via Npm, so to install it globally simply run:
npm install -g grunt-cli
As usual, on Linux you may have to prepend sudo
command if you’re getting EACCESS
errors.
Setting up Grunt
For a new project we will need to create two files: package.json
and Gruntfile.js
(or Gruntfile.coffee
if you use CoffeeScript).
For our example we will automate concatenation of a bunch of files.
First let’s set up our package.json
. grunt
is needed to be defined as a dependency and we’ll use grunt-contrib-concat
and grunt-contrib-watch
plugins for this example. We don’t provide the version so that Npm would install the latest ones.
{
"name": "testproj",
"version": "0.1.0",
"dependencies": {
"grunt": "",
"grunt-contrib-concat": "",
"grunt-contrib-watch": "",
}
}
Now run npm install
to install all dependencies for our little example. Next let’s set up our Gruntfile.js
.
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
concat: {
options: {
banner: "/* <%= pkg.name %> v<%= pkg.version %>*/\n"
},
dist: {
src: ['app/dev/aaa.js', 'app/dev/bbb.js', 'app/dev/ccc.js'],
dest: 'app/app.js'
},
dev: {
src: ['app/dev/*.js'],
dest: 'app/dev.js'
}
}
});
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.registerTask('default', ['concat']);
};
module.exports = function(grunt) { ...
is the wrapping function that will be executed once you run grunt
command. This is the basic format, that’s where all of your Grunt code should go to.
Configuration is passed into grunt.initConfig(configuration)
. Configuration should describe what tasks are available and configuration of those tasks. You can store any arbitrary data in this configuration object as well. In our example we load configuration information from package.json
and will use it to construct the banner. <% %>
are template tags (yes, Grunt supports templates).
concat
defines configurations for the concatenation task. We can add more tasks and will do so in later examples. We can specify an options
object to override the default configuration for that particular task. dist
and dev
are targets. Unless specified otherwise, if we run grunt concat
it will run concat
task on both targets. But if we run grunt concat:dist
, then concat
will only be run for dist
target. dist
object has to have a src
attribute which takes an array of file paths (e.g. in target dist
). Grunt supports globbing (e.g. target dev
), which means any javascript file in app/dev/
will be concatenated into app/dev.js
.
grunt.loadNpmTasks('grunt-contrib-concat');
enables you to use grunt-contrib-concat
task. Though it must be noted, that grunt-contrib-concat
must be specified as a dependency in your package.json
file and installed.
grunt.registerTask('default', ['concat']);
tells Grunt which tasks it should run if you don’t specify which task should be run. So if you’d run grunt
- it would run the task concat
. If default task is not specified and grunt
command is run - it will only return Warning: Task "default" not found. Use --force to continue.
and exit.
Next let’s create three files: app/dev/aaa.js
, app/dev/bbb.js
and app/dev/ccc.js
with their contents respectively:
(function() { console.log('File aaa.js'); })();
(function() { console.log('File bbb.js'); })();
(function() { console.log('File ccc.js'); })();
Now if we run grunt
we should see this:
% grunt
Running "concat:dist" (concat) task
File "app/app.js" created.
Running "concat:dev" (concat) task
File "app/dev.js" created.
Done, without errors.
And if we check app/app.js
:
/* testproj v0.1.0*/
(function() { console.log('File aaa.js'); })();
(function() { console.log('File bbb.js'); })();
(function() { console.log('File ccc.js'); })();
Wonderful, so it is indeed working. Sure, you may not want to use this exact method to combine your javascript (using grunt-contrib-uglify
is much better). But let’s take it one step further. Let’s use grunt-contrib-watch
to watch for changes in any of our app/dev/*.js
files and reconcatenate them as needed.
Watching for changes
We’ll need to do a few alterations. We’ve already added grunt-contrib-watch
dependency to package.json
. So let’s add grunt.loadNpmTasks('grunt-contrib-watch');
to the Gruntfile.js
.
Then we’ll set up the watching. Add this to the configuration object:
watch: {
scripts: {
files: ['app/dev/*.js'],
tasks: ['concat']
}
}
So we’re adding a new task watch
with target scripts
(the target name is arbitrary - you can pick one yourself if you wish). Then we define which files it should watch and which tasks should be executed if any of the watched files change. The end file should look like this:
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
concat: {
options: {
banner: "/* <%= pkg.name %> v<%= pkg.version %>*/\n"
},
dist: {
src: ['app/dev/aaa.js', 'app/dev/bbb.js', 'app/dev/ccc.js'],
dest: 'app/app.js'
},
dev: {
src: ['app/dev/*.js'],
dest: 'app/dev.js'
}
},
watch: {
scripts: {
files: ['app/dev/*.js'],
tasks: ['concat']
}
}
});
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.registerTask('default', ['concat']);
};
Now if we run grunt watch
and change app/dev/ccc.js
, then we should see this:
% grunt watch
Running "watch" task
Waiting...OK
>> File "app/dev/ccc.js" changed.
Running "concat:dist" (concat) task
File "app/app.js" created.
Running "concat:dev" (concat) task
File "app/dev.js" created.
Done, without errors.
Completed in 0.382s at Fri Mar 07 2014 22:58:49 GMT+0200 (EET) - Waiting...
As you can see it reruns concat
task on each change as expected.
You can really leverage watch
task in certain situations. For example it was used for grunt-contrib-livereload
(and apparently it’s now baked in directly into grunt-contrib-watch
!) to detect code changes and refresh your browser automatically. And since Grunt can play with other languages well, I’ve used it with grunt-shell to do some rapid prototyping with Python (nothing fancy, it detects any changes and reruns the application, yet it sped up the working process).
Usage ideas
The example above should give you the basic picture of how Grunt works. I’ve realized that there are many use cases for this tool and it probably be pointless to describe each one step by step, so I’ll just provide you with some ideas how Grunt could help your workflow and point to some plugins. You can read their wiki pages for integration details.
- Linting Javascript/CSS: grunt-contrib-jshint, grunt-contrib-csslint
- Compiling SASS/LESS/CoffeeScript: grunt-contrib-sass, grunt-contrib-less, grunt-contrib-coffee
- Compiling Handlebars/Jade templates: grunt-contrib-handlebars, grunt-contrib-jade
- Testing: grunt-mocha, grunt-contrib-qunit, grunt-mocha-test, grunt-contrib-nodeunit, grunt-contrib-jasmine
- Running shell commands: grunt-shell
- Watching for changes, reloading your browser when code changes: grunt-contrib-watch
- Minifying Javascript/CSS/HTML: grunt-contrib-uglify, grunt-contrib-cssmin, grunt-contrib-htmlmin
- Optimizing images: grunt-contrib-imageming
Last thoughts
I haven’t provided as many examples as I wanted to, but I hope that this one example gave you an idea of how this works and whether it would fit into your workflow. Personally, I really like Grunt so far and I’ve already started to integrate it into my projects. Currently it’s limited to minifying code and running shell commands, but I hope to expand the usage as I explore.
Grunt can help a lot if you allow it to do so. Community has already created 2443 plugins (at the time of writing), so I bet there’s a lot that can be automated simply by installing a plugin. And if there’s no plugin for your use case - you can simply create one and publish it yourself.
Subscribe via RSS