Perfecting your Grunt Workflow
Perfecting your Grunt Workflow
Deep within the brain of a developer, nestled between the gentle folds of the nucleus accumbens, lives a strong and primitive craving for automation. The concept of a function, f(x)=y
, and likely algebra itself induces this region of the brain to release a powerful, pleasurable burst of dopamine.
It is from this area of the brain that Grunt, the automation system extraordinaire, was born. Grunt saves you the effort of running repetitive tasks such as concatenation, minification, translation (LESS -> CSS), testing, deployment, code linting, and more. It’s a bit like a Makefile with a bunch of community-contributed bells and whistles written in JavaScript.
A well-configured Gruntfile can put you close to nirvana but getting there may involve a good deal of amygdala-coordinated anger and rage. This post skips the basics to offer advanced tips and tricks for perfecting your Grunt workflow developed while building a Japanese Mahjong AI and simulator. The Mahjong codebase is written in Node.js but there is no reason you couldn’t use Grunt to automate your language of choice.
The following sections will use my Gruntfile as an example with specific sections reproduced here for clarity.
Stylesheets
As a warmup here is code to manage all your stylesheets.
var less_src_files = ['public/css/src/**/*.less'];
// within grunt.initConfig
less: {
development: {
files: {
'public/css/dist/all.css': less_src_files
}
},
production: {
options: {
cleancss: true
},
files: {
'public/css/dist/all.min.css': less_src_files
}
}
}
The less
directive converts LESS files in the specified folders to CSS, creating a concatenated version (all.css
) and a minified version of the same (all.min.css
). The latter uses class-css for minification.
JavaScript
Next let’s deal with JavaScript concatenation and minification.
var js_src_files = ['public/js/global/**/*.js',
'shared/**/*.js',
'public/js/src/**/*.js'
];
// within grunt.initConfig
concat: {
options: {
separator: ';\n'
},
dist: {
src: js_src_files,
dest: 'public/js/dist/all.js'
}
},
uglify: {
options: {
banner: '\n'
},
build: {
src: js_src_files,
dest: 'public/js/dist/all.min.js'
}
}
The concat
directive gathers files within the specified directories producing all.js
as a result. Note that the order of the folders is maintained which can be useful if you need some global libraries (jQuery, socket.io, Underscore, etc.) to appear before other functions.
The uglify
section performs a similar task, concatenating the same JavaScript files and producing a minified all.min.js
. In development you can serve all.js
while saving all.min.js
for production.
The banner
option is useful if you would like to produce a short comment at the top of your JavaScript file.
meta: {
banner:
'/*!\n' +
' * mahjong.js <%= pkg.version %> (<%= grunt.template.today("yyyy-mm-dd, HH:MM") %>)\n' +
' * MIT licensed\n' +
' *\n' +
' * Copyright (C) 2013 Benjamin Gleitzman\n' +
' */'
}
You might have noticed that there is a bit of duplicated effort — surely uglify
must also concatenate files so why not run concat
followed by uglify
? The answer is concurrency, the next task on our list.
Concurrency
Some Grunt tasks can take a while to complete. The concurrent
directive orchestrates running tasks in parallel.
concurrent: {
compress: ['less', 'concat', 'uglify'],
start: {
tasks: ['mochaTest', 'nodemon', 'watch'],
options: {
logConcurrentOutput: true
}
}
},
Running grunt concurrent:compress
causes our three previously mentioned steps (less
, concat
, and uglify
) to be run in parallel. In practice running these steps concurrently, rather than running concat
followed by uglify
, appears to be faster.
Let’s shift focus to the last three tasks mentioned by concurrent
: testing, application monitoring, and task triggering.
Testing
This section is rather simple. Mocha tests are located and run from the test/
directory with the results are printed to standard output.
mochaTest: {
test: {
options: {
reporter: 'spec'
},
src: ['test/**/*.js']
}
},
Application Monitoring
When modifying files you would like your application to restart.
var port = grunt.option('port') || 3000;
// within grunt.initConfig
nodemon: {
dev: {
options: {
nodeArgs: ['--port', port]
}
}
},
Nodemon
watches files in the directory where Grunt was started and restarts the application server whenever the files change.
Task Triggering
The watch
directive triggers other tasks when certain files are changed. Thus, editing a LESS file will result in a new version of all.min.css
.
watch: {
files: ['public/**/*.*',
'shared/**/*.*',
'views/partials/**/*.*',
'!**/dist/**'
], // ignore dist folder
tasks: ['concurrent:compress']
}
When any of the files within public/
, shared/
, or views/partials/
are modified the concurrent:compress
task will be run (concurrently concatenating and minifying CSS and JavaScript). Note that the dist/
folder is ignored since we don’t want to end up in an infinite loop when minified files are created.
Hopefully this post gets you a little closer to Grunt nirvana.