Article summary
I recently started doing a lot more standalone JavaScript front end development, often using CoffeeScript, less, jade, or other modern web stack components. Compiling assets like these is pretty much a solved problem when they are being delivered from a server side web stack like Rails, but I found myself in need of a JavaScript-based solution for statically compiling, serving, and testing my code.
I first looked into simple solutions involving node Cakefiles and static site generators like wintersmith, but I found these solutions lacked the power and flexibility I needed. At this point, through an obscure comment in a Google Talk thread about wintersmith, I heard my first reference to Grunt.
Grunt
The front page of Grunt immediately looked promising, offering support for CoffeeScript, handlebars, jade, JSHint, and less — a significant portion of the tech our team was using. The plugins page revealed a list several hundred items long. Since every technology needed for my project at the time had at least one plugin on the list, I decided to dive into Grunt.
Fortunately for me, Grunt tasks are very consistent and easy to configure, and Grunt itself has excellent documentation. Configuring Grunt in fact only requires two files: a package.json
file specifying which Grunt plugins you are using, and a Gruntfile.js
or Gruntfile.coffee
file to configure each task.
A Simple Example
Let’s look at a simple grunt configuration to compile CoffeeScript files into JavaScript. We can use the grunt-contrib-coffee
plugin to do this (the contrib in the name indicates that that particular plugin is maintained by the Grunt team.). First off, we need to put the appropriate dependencies into our package.json file.
package.json
{
"name": "my-project-name",
"version": "0.1.0",
"dependencies": {
"grunt": "latest",
"grunt-contrib-coffee": "latest"
}
}
Here we declare the only two dependencies we need: the latest versions of Grunt, and the contrib-coffee plugin.
Gruntfile.coffee
Now we can configure the coffee task in our Gruntfile.coffee. I prefer to use CoffeeScript for my Gruntfile because it is supported by Grunt out of the box and it makes the configuration much more concise:
module.exports = (grunt) ->
grunt.initConfig
coffee:
all:
options:
bare: true
files: [
src: ["./app/src/**/*.coffee"]
dest: "./public/js/app.js"
]
grunt.loadNpmTasks "grunt-contrib-coffee"
There’s a lot of information in the above configuration, so lets take an in-depth look at it. The first line simply defines an entry point for the Grunt object to be inserted into our configuration via nodejs’s module system. The second line calls Grunt’s initconfig method, which takes a Grunt configuration object as an argument.
The next section is our CoffeeScript task configuration. We start by defining a subtask all
. While we don’t strictly have to do this because we only have a single subtask right now, it will make it easier to add other subtasks later. Within this configuration, we have two sections, options
and files
. As you would probably expect, options
is where you set predefined configuration options for the task, and files
is where you define the files that the task will operate on.
In our config, we are setting the bare
option to true, which tells the CoffeeScript compiler to not wrap its output in a function.
The files
section specifies an array with one item, an object with a src
and dest
parameter. As you can see in our src
parameter, Grunt supports file glob syntax out of the box, allowing you to dynamically define which files you want to target. Since our dest is only one file, the coffee task also implicitly knows that it needs to concatenate all of the source files it finds into a single output file.
The last line simple loads the contrib coffee task from our our installed node_modules.
That’s all that is needed to configure a simple, automated build process for your CoffeeScript Source. Running grunt coffee:all
from the command line will execute our defined task.
A More Comprehensive Example
Unfortunately, doing simple CoffeeScript compilation is rarely the only thing you need to do to build a complex client side project. So let’s look at a more complete configuration for an AngularJS project I’m working on in my spare time. First, the Gruntfile.
Gruntfile.coffee
module.exports = (grunt) ->
grunt.initConfig
appDir: "./app"
srcDir: "<%= appDir %>/src"
contentDir: "<%= appDir %>/content"
templateDir: "<%= appDir %>/templates"
vendorDir: "<%= appDir %>/vendor"
cssDir: "<%= appDir %>/stylesheets"
testDir: "./test"
buildDir: "./tmp/build"
outputDir: "./public"
Here we start our Grunt config the same way we did before. Immediately after that, we create some aliases for various directories we will be using. These aliases, as you can see, can be referenced in the Grunt configuration using underscore style template placeholders: <%= %>
.
# Javascript compilation tasks
# Essentially we want everything in the vendor,
# templates, and src directories to end up in
# one big JS file in that order.
coffee:
options:
bare: true
all:
files: [
expand: true,
src: ["<%= srcDir %>/**/*.coffee"]
dest: "<%= buildDir %>"
ext: ".js"
]
changed:
files: [
expand: true
src: "<%= grunt.regarde.changed %>"
dest: "<%= buildDir %>"
ext: ".js"
]
jade:
options:
client: true
compileDebug: false
namespace: "APPLICATION_TEMPLATES"
processName: (path) ->
path
.replace(/^.*\btemplates\//, "")
.replace(/\..*$/, "")
all:
files: [
expand: true
src: ["<%= templateDir %>/**/*.jade"]
dest: "<%= buildDir %>"
ext: ".js"
]
changed:
files: [
expand: true
src: "<%= grunt.regarde.changed %>"
dest: "<%= buildDir %>"
ext: ".js"
]
concat:
js:
src: [ # Order matters, at some point we should use requireJS or something to fix that
"<%= vendorDir %>/**/*.js",
"<%= buildDir %>/<%= templateDir %>/**/*.js",
"<%= buildDir %>/<%= srcDir %>/*.js",
"<%= buildDir %>/<%= srcDir %>/modules/**/*.js",
"<%= buildDir %>/<%= srcDir %>/directives/**/*.js",
"<%= buildDir %>/<%= srcDir %>/services/**/*.js",
"<%= buildDir %>/<%= srcDir %>/filters/**/*.js",
"<%= buildDir %>/<%= srcDir %>/controllers/**/*.js",
]
dest: "<%= outputDir %>/js/app.js"
Now we’ve defined our javascript build tasks. These compile our CoffeeScript and jade sources into javascript files in a temporary build directory. Then our concat
task concatenates our 3rd party vendor javascript, as well as our newly created javascript sources, into one large javascript file. Take particular note of the changed
configuration blocks in the CoffeeScript and jade tasks; they will be explained shortly.
# LESS compilation
less:
options:
paths: "<%= cssDir %>"
all:
files: [
src: "<%= cssDir %>/app.less"
dest: "<%= outputDir %>/css/app.css"
]
# Copy static files
copy:
html:
files: [
expand: true
cwd: "<%= contentDir %>/html"
src: ["**/*.html"]
dest: "<%= outputDir %>"
]
images:
files: [
expand: true
cwd: "<%= contentDir %>/images"
src: ["**/*"]
dest: "<%= outputDir %>/images"
]
Here we define some tasks to compile our less stylesheets and copy our static assets like images into our output directory.
# Set up file monitors to rebuild project on changes
regarde:
coffee:
files: "<%= srcDir %>/**/*.coffee"
tasks: ["coffee:changed", "concat:js"]
templates:
files: "<%= srcDir %>/**/*.jade"
tasks: ["jade:changed", "concat:js"]
vendor:
files: "<%= vendorDir %>/**/*.js"
tasks: ["concat:js"]
less:
files: "<%= cssDir %>/**/*.less"
tasks: ["spawn_less"]
html:
files: "<%= contentDir %>/html/**/*.html"
tasks: ["copy:html"]
images:
files: "<%= contentDir %>/images/**/*"
tasks: ["copy:images"]
The regarde
task defined here watches for changes to our source files and executes the specified task(s) whenever a watched file is changed. This saves us from manually rebuilding every time we make a change. Notice that for CoffeeScript and jade files, it calls the changed
subtask, which only recompiles the files that were actually changed. This drastically cuts down our compilation times when there are a lot of source files, and only one was changed.
# Set up a static file server
connect:
server:
options:
hostname: "0.0.0.0"
port: 9292
base: "<%= outputDir %>"
keepalive: true
# Clean up artifacts
clean:
build: "<%= buildDir %>"
output: "<%= outputDir %>"
# Execute server script
exec:
server:
cmd: "./server.js"
The above are miscellaneous tasks:
connect
serves our output directory on our local machine so that we can open our newly generated site in a browserclean
simply erases the build and output directories when invokedexec
task lets us execute arbitrary shell programs (in this case, a custom server script that I’ll show in a moment)
# Karma conf
karma:
unit:
configFile: "<%= testDir %>/unit_conf.js"
e2e:
configFile: "<%= testDir %>/e2e_conf.js"
# The contrib-less task has an outstanding bug that will kill a running grunt
# process (like a watch process) when it fails.
grunt.registerTask 'spawn_less', 'Run Less in a subprocess', () ->
done = this.async()
grunt.util.spawn grunt: true, args: ['less'], (err) ->
if err
grunt.log.writeln(">> Error compiling LESS file!")
done()
grunt.registerTask "js", ["coffee:all", "jade:all", "concat:js"]
grunt.registerTask "build", ["clean", "js", "less", "copy"]
grunt.registerTask "server", ["exec:server"]
grunt.registerTask "unit", ["build", "karma:unit"]
grunt.registerTask "e2e", ["build", "karma:e2e"]
grunt.registerTask "ci", ["build", "karma"]
grunt.loadNpmTasks "grunt-contrib-coffee"
grunt.loadNpmTasks "grunt-contrib-jade"
grunt.loadNpmTasks "grunt-contrib-concat"
grunt.loadNpmTasks "grunt-contrib-less"
grunt.loadNpmTasks "grunt-contrib-copy"
grunt.loadNpmTasks "grunt-regarde"
grunt.loadNpmTasks "grunt-contrib-connect"
grunt.loadNpmTasks "grunt-contrib-clean"
grunt.loadNpmTasks "grunt-exec"
grunt.loadNpmTasks "grunt-karma"
The last part of the Gruntfile defines a task to load and run our tests via the karma test runner, registers some aliases for groups of tasks, and loads our tasks from our node modules.
The associated package.json file looks about like you would expect:
package.json
// package.json
{
"name": "rcr-frontend",
"version": "0.1.0",
"dependencies": {
"grunt": "latest",
"grunt-contrib-coffee": "latest",
"grunt-contrib-jade": "latest",
"grunt-contrib-less": "latest",
"grunt-contrib-copy": "latest",
"grunt-contrib-connect": "latest",
"grunt-contrib-concat": "latest",
"grunt-regarde": "latest",
"grunt-notify": "latest",
"grunt-contrib-clean": "latest",
"grunt-karma": "latest",
"grunt-exec": "latest"
}
}
And as promised, the server.js
file referenced in our exec
task. This script allows us to run both our webserver and file watcher service in the same terminal by just running the command grunt server
.
server.js
#!/usr/bin/env node
var spawn = require("child_process").spawn,
watcher = spawn("grunt", ["regarde", "--force"]),
server = spawn("grunt", ["build", "connect:server"]);
watcher.stdout.on("data", function(data) {
var importantOutput = data.toString().split("\r?\n").filter(function(str) {
return />>|Done|Warning|Running/.test(str);
});
process.stdout.write(importantOutput.join("\n"));
// process.stdout.write(data);
});
server.stdout.on("data", function(data) {
process.stdout.write(data);
});
watcher.on("exit", function(code, signal) {
server.kill();
process.exit();
});
server.on("exit", function(code, signal) {
watcher.kill();
process.exit();
});
process.on("exit", function() {
watcher.kill();
server.kill();
});
There you have it, a relatively straightforward build configuration for a complicated static JavaScript site, made easy with Grunt.
I’d recommend not using “latest” in your package.json dependencies, and setting specific version numbers. Depending on how they handle their repos, this might be the bleeding edge version, which can introduce bugs — Especially annoying when your local modules stay “cached” but your CI-based server re-installs all NPM modules on every push. Have been bitten by this before with grunt-contrib-coffee.
Agreed, I usually use a hardcoded minor version with a wildcard for the patch in my projects, e.g. 0.5.x.
When I was writing this, I didn’t want the package versions I listed In my examples to get out of date. Didn’t occur to mr that I would be encouraging bad practices in package.json setup though.
Never heard of grunt-regarde. Thanks for article, I will try it soon.
Good as introduction to Grunt. Thanks for sharing. But, is it correct that your example workflow doesn’t differentiate between development and production environments?
A real project would limely need a prod configuration at some point, but that was a bit outside the scope of this post. The post was more targeted as an intro to grunt’s capabilities, rather than a lesson on general configuration managent.
From the grunt-regarde GitHub page:
“[deprecated in favor of grunt-contrib-watch, click link below] Observe files for changes and run tasks — Read more https://github.com/gruntjs/grunt-contrib-watch“