JavaScript's Modern Tools – Grunt – Ant for the Browser?

by
Tags: , , ,
Category:

Javascript developers have been hiding under a browser rock for more than a decade now. Originally a language running only in browsers, it now has moved into its own, with engines like v8 and platforms like NodeJS, Meteor and VertX running on the server, and many MVC platforms running client-side on HTML5 web applications and mobile applications, it’s a true client/server language.

As these platforms have flourished, so too have tools around them – we already discussed bower and npm, two tools that help you download and install dependencies. But what about tools to build your application, run tests, distribute minified versions and run quality checks? Enter Grunt.

Grunt – it’s tooling for Javascript

Grunt is a task runner. It has several built-in tasks, and can be extended to encompass whatever tools you desire. Generally the tools are installed via npm as Javascript and Grunt APIs.

Typical grunt tasks include linting, building (assembling) your runtime application, running test suites, executing your web application, minifying and concatenating your scripts and CSS files, running SCSS jobs, and the like.

Grunt syntax is Javascript, as with our other tools. It has a very specific DSL, which we’ll review in our samples below. It uses npm for its dependencies, and expects a package.json file with these minimum requirements:

{
   "name": "yourProjectName",
   "version": "0.1.1", // that's whatever version you want
   "devDependencies" : {
      "grunt" : "~0.4.2"
   }
}

Installing Grunt

If this is your first time running Grunt, it will prompt you that the grunt-cli package is not installed. Install this globally on your machine:

npm install -g grunt-cli

Once you’ve created your package.json file in the root of your project, you can install Grunt with npm install, which as we saw in our prior post, installs all dependencies in the package file:

npm install

The structure of a Grunt build file

Your grunt build file is created with the name Gruntfile.js (Gruntfile.coffee is also allowed for you coffeescript afficianados). Let’s look at a sample:

module.exports = function(grunt) {
  grunt.task.registerTask('mytask', function() {
    grunt.log.writeln('this is my first task');
  });
  grunt.registerTask('default', ['mytask']);
};

This simple Gruntfile creates a task, calls it mytask, and uses Grunt’s log API to write a message to the console. It is then registered as part of the default tasks to run when grunt fires up without any additional arguments. Let’s run it:

$ grunt
Running "mytask" task
this is my first task
Done, without errors.

Grunt is all about plugins

Grunt is pretty much a low-level runner in Javascript without the plugins. In other words, you’ll want to install plugins to get anything significant done. Good news? There are TONS of them. Let’s add one of the most typical ones to do a good old-fashioned linting of our project:

npm install grunt-contrib-jshint --save-dev

For those who read my last post--save-dev adds the package to the package.json file in the devDependencies section. Now we can register it in Grunt and add it to our default tasks. We’ll also add a config section to the Gruntfile to customize where we are searching for our Javscript files:

module.exports = function(grunt) {
  grunt.initConfig({
    jshint: {
      all: ['src/**/*.js']
    }
  });
  grunt.task.registerTask('mytask', function() {
    grunt.log.writeln('this is my first task');
  });
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.registerTask('default',
    ['mytask', 'jshint']);
};

You’ll note we had to do several things to activate JSHint:

  • Used npm install grunt-contrib-jshint --save-dev to install the plugin in our package.json file
  • Load the grunt-contrib-jshint plugin with loadNpmTasks
  • Add the jshint task to our default task list
  • Configure the targets for hinting. We have selected anything ending in .js in the src directory, recursively

Now we can run Grunt again:

$ grunt
grunt-demos [release-1.5●●●] % grunt
Running "mytask" task
this is my first task
Running "jshint:all" (jshint) task
Linting src/bad-for-lint.js ...ERROR
[L4:C12] W025: Missing name in function declaration.
   function(a, b) {
Warning: Task "jshint:all" failed. Use --force to continue.
Aborted due to warnings.

To just run jshint itself, we can execute it as a task directly:

$ grunt jshint
Running "jshint:all" (jshint) task
Linting src/bad-for-lint.js ...ERROR
[L4:C12] W025: Missing name in function declaration.
   function(a, b) {
Warning: Task "jshint:all" failed. Use --force to continue.
Aborted due to warnings.

In either case, our project failed due to jshint failing. You may not wish to include jshint in your build You can modify this by changing the configuration per the plugin documentation.

Testing with Karma

Now we’ll sneak in another great npm-based tool, Karma – a test runner that can automate Jasmine or Mocha test suites. We’ll write a simple Javascript file and test it with Jasmine. Let’s install it:

npm install karma --save-dev

We’ll use the grunt-karma task runner too:

npm install grunt-karma --save-dev

(The reason the word contrib isn’t in the project name is that it’s not a package supported by the Grunt team).

Our sample JavaScript file to test (src/calculator.js):

var calculator = {
   add: function(a, b) {
     return a + b;
   },
   subtract: function(a, b) {
     return a - b;
   }
}

Our test in Jasmine format (test/calculator-spec.js):

describe("The calculator class", function() {
  it("should add two numbers", function() {
    expect(calculator.add(10, 12)).toBe(22);
  });
  it("should subtract two numbers", function() {
    expect(calculator.subtract(10, 12)).toBe(-2);
  });
});

We need to add our Karma config file too (karma.conf.js):

module.exports = function(config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine'],
    files: [
      'src/**/*.js',
      'test/**/*.js'
    ],
    exclude: [],
    port: 8080,
    logLevel: config.LOG_INFO,
    autoWatch: false,
    browsers: ['Chrome'],
    singleRun: true
  });
};

Finally, we configure Grunt:

module.exports = function(grunt) {
  grunt.initConfig({
    jshint: {
      all: ['src/**/*.js'],
      options: {
        reporter: require('jshint-stylish')
      }
    },
    karma: {
     unit: {
      configFile: 'karma.conf.js',
      }
    }
  });
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-karma');
  grunt.registerTask('default', ['jshint', 'karma:unit']);
};

Notice we killed our sample task, and made our default build require that we’ve passed successful linting, AND we ran our tests:

$ grunt
Running "jshint:all" (jshint) task
✔ No problems
Running "karma:unit" (karma) task
INFO [karma]: Karma v0.10.9 server started at http://localhost:8080/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 32.0.1700 (Mac OS X 10.9.1)]: Connected on socket b959LkBrB5GT_EiYrrC2
Chrome 32.0.1700 (Mac OS X 10.9.1):
Executed 1 of 2 SUCCESS (0 secs / 0.017 secsChrome 32.0.1700 (Mac OS X 10.9.1):
Executed 2 of 2 SUCCESS (0 secs / 0.018 secsChrome 32.0.1700 (Mac OS X 10.9.1):
Executed 2 of 2 SUCCESS (0.098 secs / 0.018 secs)
Done, without errors.

Nice, eh? Turns out we can run individual tasks as well so if we want to kick off karma alone, we can issue grunt karma:unit (or since we only have one karma configuration, we can just issue grunt karma).

Going further

You can use a ton of Grunt tasks to minify/uglify your CSS and Javascript code, concatenate multiple source files, and a lot more. Visit the plugins page on the Grunt website and browse around. More importantly, start downloading sample GitHub projects for Node and client-side applications, and you’ll find Grunt at their heart.

In our next blog post, we’ll pull all of these things together to build a sophisticated web application using the Yeoman project creator/manager tool, which generates Bower, Karma, Grunt, and npm configuration files.

For more information on Grunt, visit the website at gruntjs.com.