Grunt is an aptly named JavaScript taskrunner that - once set up - does a dirty great chunk of the grunt (heh) work for you.
Thereβs an ever growing list of plugins out there thatβre ready to automate the repetitive tasks like compiling, testing, setup, building, linting, etc, that we have to do every day. Lets automate that shit!
Grunt has become a fundamental part of my workflow. Iβm going to show you how I have it set up and, after installation, weβll get it to:
If youβve been here before, feel free to skip to the tl;dr for the complete gruntfile.
Letβs imagine our app currently has a directory structure like this:
$ tree
βββ app
β βββ ..
βββ public
β βββ css
β β βββ main.css
β βββ img
β β βββ logo.png
β β βββ icon.png
β β βββ photo.jpg
β β βββ sprite.png
β βββ js
β βββ vendor
β β βββ modernizr.js
β β βββ plugin_one.js
β β βββ plugin_two.js
β βββ bar.js
β βββ foo.js
β βββ main.js
βββ gruntfile.js
βββ package.json
Grunt is a command line app. Weβll need to install it globally so we can access it from anywhere on the system.
$ npm install -g grunt-cli
Prerequisites: Node.js and itβs package manager; npm.
This will put $ grunt
in your system path.
If we dropped the -g
(global) flag, the package would be installed locally
within this project only, and not be available system wide.
The package.json
file is where information about your application is stored, such as
itβs name, version, authors, homepage, etc. More importantly (to us at least), is
that it remembers any dependencies of your application, so anyone else who works
on your project can install the very same dependencies and be up and running in a jiffy.
If youβve used composer before, this is the node equivalent to your composer.json
file.
Hereβs a basic package.json
file.
{
"name": "grunt-demo",
"version": "0.1.0",
"devDependencies": {
"grunt": "~0.4.1",
}
}
note: You can $ npm init
to generate a package.json
file.
npm
has a couple of commands youβll use a lot: install
and update
.
Install will read the dependencies listed in package.json
and install them.
$ npm install
Update will (obviously) update your dependencies to newer versions (within
the βversion boundsβ set within package.json
).
$ npm update
To install a new package, we run install again with a couple of options:
$ npm install PACKAGE_NAME --save-dev
The --save-dev
flag will update your package.json
file with the new dependency.
Grunt reads the gruntfile.js
file. This is the file where you define and configure
your tasks. Below is a template for the gruntfile.js
file.
Take note of where the hashed numbers (e.g. #1
) are, weβll reference these points later.
module.exports = function (grunt) {
grunt.initConfig({
// Load in data from package.json.
// Now pkg.name == 'grunt-demo'
pkg : grunt.file.readJSON('package.json'),
// #1 Define a task
TASK : {
ROUTINE : {
//..
}
}
});
// #2 Load the package
grunt.loadNpmTasks('PACKAGE');
// #3 Register a task
grunt.registerTask('REGISTERED', [
'TASK' // Or, more verbosely, 'TASK:ROUTINE'
]);
};
In this one file, we can tell grunt what packages to load, configure and define our tasks/routines. Itβs brilliantly simple.
Before we go any further, letβs just clarify what a package, task, routine and registered is:
Package : Within the context of Grunt, a package is really a plugin. Anyone can write a package that does anything they reqiure, whether it be compiling sass, minifying images, etc. The great thing about Grunt is that the community have already written a whole bunch of great packages already!
Task
: A task is a loaded, configured and βexecutableβ plugin. For example, the
grunt-sass
package will allow you to compile and minify SASS files with the task
$ grunt sass
.
Routine
: Tasks can be configured in multiple ways to achieve different results. For example,
we could configure sass to compile expanded
throught development with $ grunt sass:development
,
and compressed
on release with $ grunt sass:release
Registered
: A registered task is a set of tasks that are run with a single command. For example, you
may want to run the sass
, coffeescript
and imagemin
tasks one after the
other, automatically in a routine called βreleaseβ by with $ grunt release
.
Run registered tasks (REGISTERED
is default
by default).
$ grunt [REGISTERED]
Run a task
$ grunt TASK[:ROUTINE]
All great css started life as Sass.
Sass is a css preprocessor that offers great features like variables, nesting, imports, mixins, inheritence and much much more.
So, with that in mind letβs create a few Sass
files that will compile and minify into main.min.css
.
$ tree
.
βββ public
β βββ css
β β βββ main.min.css
| .
β βββ sass
β βββ _bar.scss
β βββ _foo.scss
β βββ main.
.
grunt-sass
Install the grunt-sass
package locally, and tell npm to add it to your package.json
file as a development dependency.
$ npm install grunt-sass --save-dev
How a package is configured is dependent on that paticular package. However,
they do follow a very similar pattern. Here we set up sass to accept one source .scss
file, compile and compress it (with compass) into one minified .css
file.
// #1 Define a task
sass : {
main : {
options : {
// style : ['nested', 'expanded', 'compact', 'compressed']
style : 'compressed',
compass : true
},
files : {
// destination : source
'public/css/main.min.css' : 'public/sass/main.scss'
}
}
}
// #2 Load the package
grunt.loadNpmTasks('grunt-sass');
Register the default sass task to default, so we can simply run $ grunt
.
// #3 Register a task
grunt.registerTask('default', ['sass']);
$ grunt [sass]
By default, Grunt will execute your tasks and finish. This quickly becomes annoying once you start developing anything: make changes, save changes, run grunt, reload browser, make changes, sa⦠Ugh.
Watch will automate this! It monitors the filesystem for changes and will trigger the appropriate tasks again for you.
Add in LiveReload and itβll even reload your browser for you. What a hoot.
grunt-contrib-watch
$ npm install grunt-contrib-watch --save-dev
// #1 Define a task
watch : {
options : {
livereload : true
}
sass : {
// **/*.scss will match all of these:
// ./main.scss
// ./nested/file.scss
// ./very/deeply/nested/file.scss
files : 'public/sass/**/*.scss',
tasks : ['sass']
}
}
// #2 Load the package
grunt.loadNpmTasks('grunt-contib-watch');
PROTIP: Add watch to the end of your default task. Start working with a simple $ grunt
.
// #3 Register a task
grunt.registerTask('default', ['sass', 'watch]);
$ grunt [watch]
Prerequisites: The LiveReload browser extension (for that magical reload).
CoffeeScript is a great little language that compiles into JavaScript. Itβs actually makes writing JavaScript enjoyable again!
Weβll be using RequireJs later to build and optimize our JavaScript,
so weβll set up CoffeeScript to compile each .coffee
file into the equivalent .js
file, 1-for-1.
$ tree
.
βββ public
β βββ coffee
β β βββ bar.coffee
β β βββ foo.coffee
β β βββ main.coffee
| .
β βββ js
β β βββ bar.js
β β βββ foo.js
β β βββ main.js
. . .
grunt-contrib-coffee
$ npm install grunt-contrib-coffee --save-dev
// #1 Define a task
coffee : {
compile : {
expand : true,
cwd : 'public/coffee',
src : '**/*.coffee',
dest : 'public/js',
ext : '.js'
}
},
// #2 Load the package
grunt.loadNpmTasks('grunt-contib-coffee');
We can now run $ grunt coffee
to compile our CoffeeScript, but since we have sass
set up to compile too, letβs register them as build
.
// #3 Register a task
grunt.registerTask('build', ['sass', 'coffee']);
grunt.registerTask('default', ['build', 'watch]);
$ grunt [coffee]
Websites are fast becomming Web apps, codebases grow fat with horrible JavaScript
sphagetti, HTTP requests are in double tripple figures!
Whoβs to blame? JavaScript. No really, whoβs to blame? me.
RequireJs (and AMD in general) will help you structure your code much, much better, and make building a breeze.
$ tree
.
βββ public
| .
β βββ js
β β βββ vendor
β β β βββ ..
β β β βββ plugin_one.js
β β β βββ plugin_two.js
β β βββ bar.js
β β βββ foo.js
β β βββ main.js
β β βββ main.min.js
. .
β βββ templates
β β βββ index.html
β β βββ header.html
β β βββ partial.html
. .
grunt-contrib-requirejs
$ npm install grunt-contrib-requirejs --save-dev
CONFESSION: From what I can tell, we pretty much have to duplicate the settings
from the require.config({ .. })
in our main.js
file here. That sucks, let me know
if you know of a better way. thanks xx
// #1 Define a task
requirejs : {
release : {
options : {
baseUrl : 'public/js',
name : 'main',
out : 'public/js/main.min.js',
stubModules : ['text'],
paths : {
text : 'vendor/text',
templates : '../templates',
jquery : 'vendor/jquery/jquery'
//..
},
shim: {
jquery: {
exports: 'jQuery'
}
//..
}
}
}
}
// #2 Load the package
grunt.loadNpmTasks('grunt-contib-requirejs');
See why we created the build task earlier? Letβs use it again in out release task.
// #3 Register a task
grunt.registerTask('build', ['sass', 'coffee']);
grunt.registerTask('default', ['build', 'watch]);
grunt.registerTask('release', ['build', 'requirejs']);
$ grunt release
Modernizr gives us a tonne of feature detection goodness. But itβs doubtful weβll use all of it. So letβs trim the fat β¦ automatically.
grunt-modernizr
is super sweet. Itβll take a look at your files to determine what
Modernizr features youβre actually using, pluck them from a development copy
you have laying around (*cough* use bower *cough*) and spit
you our a lean, minified, custom build. Hot.
$ tree
.
βββ public
β βββ css
β β βββ main.min.css
| .
β βββ js
β β βββ vendor
β β β βββ modernizr.js
β β β βββ ..
β β βββ ..
β β βββ modernizr.min.js // output file
. .
grunt-modernizr
$ npm install grunt-modernizr --save-dev
// #1 Define a task
modernizr : {
devFile : "public/js/vendor/modernizr.js",
outputFile : "public/js/modernizr.min.js",
extra : {
// Include w/e you like
shiv : true,
printshiv : false,
load : true,
mq : true,
cssclasses : true
},
files : [
"public/css/main.min.css",
"public/js/main.min.js"
// include anything that uses modernizr,
// remember you can use **/*.ext
]
},
// #2 Load the package
grunt.loadNpmTasks('grunt-modernizr');
The great thing about this is, we can work in development with the feature-rich development copy of Modernizr, and simply create our custom build on release.
// #3 Register a task
grunt.registerTask('build', ['sass', 'coffee']);
grunt.registerTask('default', ['build', 'watch]);
grunt.registerTask('release', ['build', 'requirejs', 'modernizr']);
$ grunt release
I hate optimizing images manualy. So I donβt.
$ tree
.
βββ public
β βββ img
β β βββ sprites
β β β βββ .. // compass will output to ../sprite.png
β β βββ logo.png
β β βββ photo.jpg
β β βββ sprite.png
. .
grunt-contrib-imagemin
$ npm install grunt-contrib-imagemin --save-dev
CAUTION: I live life on the edge. My optimized images replace their chubby predecessors. If, for whatever reason it goes wrong the originals are gone forever. Change the dest(ination) directories to something else if youβre a girl.
*[forever]: A back-up a dayβ¦
// #1 Define a task
imagemin : {
png : {
options: {
optimizationLevel: 7
},
files : [
{
expand : true,
cwd : 'public/img',
src : ['**/*.png'],
dest : 'public/img',
ext : '.png'
}
]
},
jpg : {
options : {
progressive: true
},
files : [
{
expand : true,
cwd : 'public/img',
src : ['**/*.jpg'],
dest : 'public/img',
ext : '.jpg'
}
]
}
},
// #2 Load the package
grunt.loadNpmTasks('grunt-contrib-imagemin');
Again, we only care about this on release; add it to the list.
// #3 Register a task
grunt.registerTask('build', ['sass', 'coffee']);
grunt.registerTask('default', ['build', 'watch]);
grunt.registerTask('release', ['build', 'requirejs', 'modernizr', 'imagemin']);
$ grunt release
gruntfile.js
module.exports = function (grunt) {
grunt.initConfig({
pkg : grunt.file.readJSON('package.json'),
sass : {
main : {
options : {
style : 'compressed',
compass : true
},
files : {
'public/css/main.min.css' : 'public/sass/main.scss'
}
}
},
coffee : {
compile : {
expand : true,
cwd : 'public/coffee',
src : '**/*.coffee',
dest : 'public/js',
ext : '.js'
}
},
watch : {
options : {
livereload : true
}
sass : {
files : 'public/sass/**/*.scss',
tasks : ['sass']
}
},
requirejs : {
release : {
options : {
baseUrl : 'public/js',
name : 'main',
out : 'public/js/main.min.js',
stubModules : ['text'],
paths : {
text : 'vendor/text',
templates : '../templates',
},
shim: { }
}
}
},
modernizr : {
devFile : "public/js/vendor/modernizr.js",
outputFile : "public/js/modernizr.min.js",
extra : {
shiv : true,
printshiv : false,
load : true,
mq : true,
cssclasses : true
},
files : [
"public/css/main.min.css",
"public/js/main.min.js"
]
},
imagemin : {
png : {
options: {
optimizationLevel: 7
},
files : [
{
expand : true,
cwd : 'public/img',
src : ['**/*.png'],
dest : 'public/img',
ext : '.png'
}
]
},
jpg : {
options : {
progressive: true
},
files : [
{
expand : true,
cwd : 'public/img',
src : ['**/*.jpg'],
dest : 'public/img',
ext : '.jpg'
}
]
}
}
});
grunt.loadNpmTasks('grunt-sass');
grunt.loadNpmTasks('grunt-contib-coffee');
grunt.loadNpmTasks('grunt-contib-watch');
grunt.loadNpmTasks('grunt-contib-requirejs');
grunt.registerTask('build', ['sass', 'coffee']);
grunt.registerTask('default', ['build', 'watch]);
grunt.registerTask('release', ['build', 'requirejs', 'modernizr', 'imagemin']);
};
package.json
{
"name": "grunt-demo",
"version": "0.1.0",
"devDependencies": {
"node-sass" : "~0.7.0",
"grunt": "~0.4.1",
"grunt-sass": "~0.8.0",
"grunt-contrib-watch" : "~0.5.3",
"grunt-contrib-coffee" : "~0.8.0",
"grunt-contrib-requirejs" : "~0.4.1",
"grunt-modernizr" : "~0.4.1",
"grunt-contrib-imagemin" : "~0.1.4",
}
}
While developing:
$ grunt
To build for release:
$ grunt release