Build a Rich JavaScript Front End with Grunt

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

gruntjsThe 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 browser
  • clean simply erases the build and output directories when invoked
  • exec 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.
 

Conversation
  • David Kaneda says:

    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.

    • Al Scott Al Scott says:

      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.

  • srigi says:

    Never heard of grunt-regarde. Thanks for article, I will try it soon.

  • Sergey says:

    Good as introduction to Grunt. Thanks for sharing. But, is it correct that your example workflow doesn’t differentiate between development and production environments?

    • Al Scott Al Scott says:

      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.

  • Jaime Pillora says:

    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

  • Comments are closed.