Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

Web Development Automation - gruntfile Using CoffeeScript

This is the third post in a series on automation in web development. The most common task in web development is running a series of tasks on your JavaScript and CSS source files to optimize them for a high performing website. In general, our optimized JS and CSS files would be small in size, and small in number.

To do this, I first started using Apache Ant with the YUI compressor to create minified versions of my JavaScript and CSS source files. We’ve come a long way since then. We are now using CoffeeScript and LESS source files that compile to JavaScript and CSS. This doesn’t quite fit into the legacy workflow of using Apache Ant. So, Grunt to rescue. To get started, I had to install and configure Grunt for my project. At then end of the tutorial, we we able to use our package.json file to install the required plugin dependencies for my Gruntfile.

This post will finish up the [automation series(/2014/04/18/web-development-automation/) by creating our Gruntfile and running the grunt watch command to continually run our automation tasks after any changes are saved to our source files. We can then seamlessly code our CoffeeScript and LESS files, save them, and instantly test in the browser.

Gruntfile Wrapper

The Gruntfile is pretty simple. Here’s the basics.

The following snippet is the first part of our Gruntfile.

gruntFunction = (grunt) ->

  gruntConfig =
    pkg:
      grunt.file.readJSON 'package.json'

  grunt.initConfig gruntConfig
  null

module.exports = gruntFunction

We have defined a function named gruntFunction(), which is our wrapper function as it is defined as the module.exports function on the last line of the above snippet. Within our gruntFunction, we are setting up the configuration options for Grunt. In this example, I am simply telling Grunt where to find the package file.

The next step is to call the loadNpmTasks() method to load the plugins that we installed.

grunt.loadNpmTasks 'grunt-contrib-coffee'
grunt.loadNpmTasks 'grunt-coffeelint'
grunt.loadNpmTasks 'grunt-concat-css'
grunt.loadNpmTasks 'grunt-contrib-cssmin'
grunt.loadNpmTasks 'grunt-contrib-concat'
grunt.loadNpmTasks 'grunt-contrib-uglify'
grunt.loadNpmTasks 'grunt-contrib-less'
grunt.loadNpmTasks 'grunt-contrib-watch'

OK, now we are ready to start adding our tasks.

Create Application CSS from LESS Source

gruntConfig =
  pkg:
    grunt.file.readJSON 'package.json'

  less:
    app:
      files:
        "www/webroot/css/app.css": "src/less/styles.less"

  grunt.initConfig gruntConfig
  grunt.loadNpmTasks 'grunt-contrib-less'
  null

module.exports = gruntFunction

In this example, I am using the less plugin to create the app.css file from the styles.less source file.

Concat Library CSS

gruntFunction = (grunt) ->

gruntConfig =
  pkg:
    grunt.file.readJSON 'package.json'

  concat_css:
    libs:
      src: ["build/css/libs/*.css"]
      dest: "www/webroot/css/libs.css"

  grunt.initConfig gruntConfig
  grunt.loadNpmTasks 'grunt-concat-css'
  null

module.exports = gruntFunction

In this example, I am combing (concatenating) all of the CSS library files into a single libs.css file.

Minify Library and Application CSS

gruntFunction = (grunt) ->

gruntConfig =
  pkg:
    grunt.file.readJSON 'package.json'

  cssmin:
    app:
      files:
        "www/webroot/css/app.min.css": ["www/webroot/css/app.css"]
    libs:
      files:
        "www/webroot/css/libs.min.css": ["www/webroot/css/libs.css"]

  grunt.initConfig gruntConfig
  grunt.loadNpmTasks 'grunt-contrib-cssmin'
  null

module.exports = gruntFunction

In this example, I am minifying the app.css and libs.css file into app.min.css and libs.min.css.

Create Application JS from CoffeeScript Source

gruntFunction = (grunt) ->

gruntConfig =
  pkg:
    grunt.file.readJSON 'package.json'

  coffee:
    app:
      options:
        join: true
        sourceMap: true,
      files:
        "build/js/app.js": ["src/coffee/*.coffee"]

  grunt.initConfig gruntConfig
  grunt.loadNpmTasks 'grunt-contrib-coffee'
  null

module.exports = gruntFunction

In this example, I am compiling my CoffeeScript source files into a single app.js JavaScript file.

Concat Library JS

gruntFunction = (grunt) ->

gruntConfig =
  pkg:
    grunt.file.readJSON 'package.json'

  concat:
    options:
      separator: ';'
    libs:
      src: [
        "build/js/libs/moment-with-langs.min.js",
        "build/js/libs/jquery.timepicker.min.js",
        "build/js/libs/md5.js"
      ]
      dest: "build/js/libs/libs.js"

  grunt.initConfig gruntConfig
  grunt.loadNpmTasks 'grunt-contrib-concat'
  null

module.exports = gruntFunction

In this example, I am combining all of the JavaScript library files into a single libs.js file.

Uglify Application JS

gruntFunction = (grunt) ->

gruntConfig =
  pkg:
    grunt.file.readJSON 'package.json'

  uglify:
    options:
      banner: "/img/banners/2014/""/**
        * <%= pkg.name %>
        * Copyright (c) <%= grunt.template.today("yyyy") %>
        * <%= pkg.author.name %> <<%= pkg.author.email %>>
        */"""
      mangle: true
      compress: true
      drop_console: true
    dev:
      options:
        beautify: true
        compress: false
        mangle: false
        drop_console: false
        preserveComments: 'all'

      files:
        "www/webroot/js/libs.js": "build/js/libs/libs.js"
        "www/webroot/js/app.js": "build/js/app.js"
    dist:
      files:
        "www/webroot/js/libs.min.js": "build/js/libs/libs.js"
        "www/webroot/js/app.min.js": "build/js/app.js"

  grunt.initConfig gruntConfig
  grunt.loadNpmTasks 'grunt-contrib-uglify'
  null

module.exports = gruntFunction

In this example, I am running UglifyJs to minify the JavaScript files for both the libraries (libs.js) and my application (app.js).

Setup Watch Tasks

gruntFunction = (grunt) ->

gruntConfig =
  pkg:
    grunt.file.readJSON 'package.json'

  watch:
    less:
      files: ["src/less/*.less"]
      tasks: ["less", "cssmin"]
    coffee:
      files: ["src/coffee/*.coffee"]
      tasks: ["coffee", "uglify"]

  grunt.initConfig gruntConfig
  grunt.loadNpmTasks 'grunt-contrib-watch'
  null

module.exports = gruntFunction

In this example, I am setting up a watch task. This enables me to run the grunt watch command. It will monitor all of the source files (LESS and CoffeeScript) in my source directories for any changes. When a change has been saved, I run the tasks necessary to compile and minify the application files as necessary. To fire this up, I simply run the command:

$ grunt watch

Register Default Task

gruntFunction = (grunt) ->

gruntConfig =
  pkg:
    grunt.file.readJSON 'package.json'

  grunt.registerTask 'default', ['less', 'concat_css', 'cssmin', 'coffee', 'concat', 'uglify']

  grunt.initConfig gruntConfig
  null

module.exports = gruntFunction

In this example, I am setting up the default tasks to run grunt is executed. Note that I have specified the order in the array. This is important, as most of the tasks rely on the completion of previous tasks.

Complete Gruntfile.coffee and Package.json Files

Here is the complete Gruntfile. You can also download the full source file if you prefer.

After downloading the file, just remove the .txt file extensions and place the file in your project root folder.

gruntFunction = (grunt) ->

  gruntConfig =
    pkg:
      grunt.file.readJSON 'package.json'

    less:
      app:
        files:
          "www/webroot/css/app.css": "src/less/styles.less"

    concat_css:
      libs:
        src: ["build/css/libs/*.css"]
        dest: "www/webroot/css/libs.css"

    cssmin:
      app:
        files:
          "www/webroot/css/app.min.css": ["www/webroot/css/app.css"]
      libs:
        files:
          "www/webroot/css/libs.min.css": ["www/webroot/css/libs.css"]

    coffee:
      app:
        options:
          join: true
          sourceMap: true,
        files:
          "build/js/app.js": ["src/coffee/*.coffee"]

    coffeelint:
      app:
        src: "src/coffee/*.coffee"
      no_tabs:
        level: "ignore"
      indentation:
        level: "warn"
      no_trailing_whitespace:
        level: "error"
      no_trailing_semicolons:
        level: "error"
      no_plusplus:
        level: "warn"
      no_implicit_parens:
        level: "ignore"
      max_line_length:
        level: "ignore"

    concat:
      options:
        separator: ';'
      libs:
        src: [
          "build/js/libs/moment-with-langs.min.js",
          "build/js/libs/jquery.timepicker.min.js",
          "build/js/libs/md5.js"
        ]
        dest: "build/js/libs/libs.js"

    uglify:
      options:
        banner: "/img/banners/2014/""/**
          * <%= pkg.name %>
          * Copyright (c) <%= grunt.template.today("yyyy") %>
          * <%= pkg.author.name %> <<%= pkg.author.email %>>
          */"""
        mangle: true
        compress: true
        drop_console: true
      dev:
        options:
          beautify: true
          compress: false
          mangle: false
          drop_console: false
          preserveComments: 'all'

        files:
          "www/webroot/js/libs.js": "build/js/libs/libs.js"
          "www/webroot/js/app.js": "build/js/app.js"
      dist:
        files:
          "www/webroot/js/libs.min.js": "build/js/libs/libs.js"
          "www/webroot/js/app.min.js": "build/js/app.js"

    watch:
      less:
        files: ["src/less/*.less"]
        tasks: ["less", "cssmin"]
      coffee:
        files: ["src/coffee/*.coffee"]
        tasks: ["coffee", "uglify"]

  grunt.initConfig gruntConfig

  grunt.loadNpmTasks 'grunt-contrib-coffee'
  grunt.loadNpmTasks 'grunt-coffeelint'
  grunt.loadNpmTasks 'grunt-concat-css'
  grunt.loadNpmTasks 'grunt-contrib-cssmin'
  grunt.loadNpmTasks 'grunt-contrib-concat'
  grunt.loadNpmTasks 'grunt-contrib-uglify'
  grunt.loadNpmTasks 'grunt-contrib-less'
  grunt.loadNpmTasks 'grunt-contrib-watch'

  grunt.registerTask 'default', ['less', 'concat_css', 'cssmin', 'coffee', 'concat', 'uglify']
  grunt.registerTask 'lint', 'coffeelint'

  null

module.exports = gruntFunction