A Quick Guide to Grunt

Grunt is a task runner for Javascript development. Grunt automates the process to bundle, transpile, uglify, and compress the Javascript source code. This post would like to give a quick guide to Grunt.

Installation

Our first step is to create a package.json. We will need a package.json to save the version of Grunt package. Besides, we would like to load package information from package.json in the future. Thus, if you don't have an existing package.json, then create one with npm init:

$ mkdir hello_grunt
$ cd hello_grunt
$ npm init

Second, install Grunt and its command line tool with the following command. We should keep them as development dependencies, thus --save-dev is specified:

$ npm install --save-dev grunt grunt-cli

Third, add node_modules/.bin to PATH environment variable:

$ export PATH="$(pwd)/node_modules/.bin:${PATH}"

Now, we can test the Grunt command with:

$ grunt
A valid Gruntfile could not be found. Please see the getting started guide for
more information on how to configure grunt: http://gruntjs.com/getting-started
Fatal error: Unable to find Gruntfile.

Apparently, we got an error because we haven't written a Gruntfile.js yet. We will cover the basic usages in the next section.

Gruntfile Basics

Gruntfile.js is a Javascript module specifying the task to be run by Grunt. Let's start with our first example:

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
    });

    grunt.registerTask('default', []);
};

This Gruntfile.js registers a default task which does nothing. There are several important concepts in this example. First, Gruntfile.js is essentially a Node.js module. It exports a function accepting a grunt argument which will be bound to a Grunt API object. Second, it calls grunt.initConfig() and passes an object containing a pkg property. grunt.initConfig() initializes the configuration that will be accessed by the tasks. Third, it calls grunt.registerTask() to register a task named default.

After running the grunt command, we will see:

$ grunt
Done, without errors.

Apparently, Grunt did nothing because there was nothing to do. Let's write a real hello world task. The following code snippet registers another task named hello:

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
    });

    grunt.registerTask('hello', function () {
        grunt.log.writeln('hello world');
    });

    grunt.registerTask('default', []);
};

With this Gruntfile.js, we can run the hello task with:

$ grunt hello
Running "hello" task
hello world

Done, without errors.

To run the hello task without explicitly specifying it in the command line arguments, add hello task to the dependencies of the default task:

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
    });

    grunt.registerTask('hello', function () {
        grunt.log.writeln('hello world');
    });

    grunt.registerTask('default', ['hello']);  // Modified
};

After this modification, running grunt command (without arguments) should print the same output:

$ grunt
Running "hello" task
hello world

Done, without errors.

So far, we have learned to register a single task with registerTask(). However, under some circumstances, we would like to register multiple tasks with the same function code but different parameters. We can achieve this goal with grunt.registerMultiTask():

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        hello: {
            dist: 'world',
            dist_other: 'grunt',
        },
    });

    grunt.registerMultiTask('hello', function () {
        grunt.log.writeln('hello ' + this.data + ' from ' + this.target);
    });

    grunt.registerTask('default', ['hello']);
};

In the code snippet above, we replaced registerTask() with registerMultiTask(). In addition, an extra key-value pair hello is added to the configuration. The hello property holds another object which keeps the key-value pairs for each targets. When the task function is invoked, this.target will be bound to the key and this.data will be bound to the value.

For example, in the first invocation, this.target will be bound to dist and this.data will be bound to world. In the second invocation, this.target will be bound to dist_other and this.data will be bound to grunt. Here is the output:

$ grunt
Running "hello:dist" (hello) task
hello world from dist

Running "hello:dist_other" (hello) task
hello grunt from dist_other

Done, without errors.

We can run a specific task by combining the task name and the target name with a colon. For example, running grunt hello:dist will only invoke the hello task with the dist parameter:

$ grunt hello:dist
Running "hello:dist" (hello) task
hello world from dist

Done, without errors.

This convention applies to all places where Grunt expects a task name. For example, we can specify hello:dist as a dependency of the default task:

grunt.registerTask('default', ['hello:dist']);

We have learned the basic usage of Grunt. It's time for some real-world examples. In the upcoming sections, we will cover three different Grunt tasks: uglify, clean, and babel transpilation.

Uglify

In the past, web developers had to distribute Javascript source code to web site visitors. It is undesirable because most people don't want their competitors take their code easily. Besides, web developers would like to reduce the file size as well. Since Javascript engines don't care about the comments and most local variable names, we can reduce the file size by removing comments or renaming local variables.

UglifyJS is a Javascript minifier and compressor that can remove comments, rename variables, rewrite Javascript expressions or statements, etc. grunt-contrib-uglify integrates UglifyJS into Grunt, thus our first step is to install grunt-contrib-uglify with following command:

$ npm install --save-dev grunt-contrib-uglify

To demonstrate the functionality of UglifyJS, create an input file src/hello_grunt.js containing following Javascript code:

function hello(name) {
    console.log('hello ' + name + '!');
}

Then, change Gruntfile.js to:

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        uglify: {
            dist: {
                dest: 'dist/<%= pkg.name %>.min.js',
                src: 'src/<%= pkg.name %>.js',
            },
        },
    });

    grunt.loadNpmTasks('grunt-contrib-uglify');

    grunt.registerTask('default', ['uglify']);
};

Compared to the hello world example, there are two differences.

First, an extra uglify configuration is added and a dist target is declared. The uglify:dist target reads the input from src/hello_grunt.js and writes the output to dist/hello_grunt.min.js. (Note: <%= pkg.name %> will be replaced by the package name specified in package.json, i.e. hello_grunt in this example.)

Second, grunt.loadNpmTasks() loads grunt-contrib-uglify from the installed NPM package and registers the uglify tasks.

After running the grunt command:

$ grunt
Running "uglify:dist" (uglify) task
>> 1 file created.

Done, without errors.

An uglified Javascript file can be found at dist/hello_grunt.min.js. It contains:

function hello(a){console.log("hello "+a+"!")}

As shown by the code snippet, unnecessary whitespace characters or semicolons are removed and local variables are renamed.

Clean

During the build process, several temporary files or output files are generated. grunt-contrib-clean package allows us to clean up working directories with a single command grunt clean.

To install grunt-contrib-clean package, run the following command:

$ npm install --save-dev grunt-contrib-clean

Next, load grunt-contrib-clean with grunt.loadNpmTasks() and add a clean property to the configuration:

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        uglify: {
            dist: {
                dest: 'dist/<%= pkg.name %>.min.js',
                src: 'src/<%= pkg.name %>.js',
            },
        },

        clean: {
            dist: ['dist'],
        },
    });

    grunt.loadNpmTasks('grunt-contrib-clean');
    grunt.loadNpmTasks('grunt-contrib-uglify');

    grunt.registerTask('default', ['uglify']);
};

Multiple targets and configurations may be specified in the clean property. The data for each target keep an array of paths to be deleted. In this example, the clean:dist target wants to remove the dist directory (specified in the array).

To show how grunt-contrib-clean work, let's run the default task first:

$ grunt
Running "uglify:dist" (uglify) task
>> 1 file created.

Done, without errors.

$ find dist
dist
dist/hello_grunt.min.js

Then, run grunt clean:

$ grunt clean
Running "clean:dist" (clean) task
>> 1 path cleaned.

Done, without errors.

$ find dist
find: ‘dist’: No such file or directory

As expected, the dist directory was removed after running grunt clean command.

Babel Transpilation

Babel is a transpiler (source-to-source compiler) which converts the latest or experimental Javascript features into widely-adopted Javascript. Babel has many functionalities, but the most notable one is its capability to convert ES2015 (ES6) source code into ES5 source code. The upcoming example demonstrates the interaction between Babel, UglifyJS, and Grunt.

First, to transpile ES2015 source code, we have to install grunt-babel and babel-preset-es2015:

$ npm install --save-dev grunt-babel babel-preset-es2015

Second, add a template literal to src/hello_grunt.js:

function hello(name) {
    console.log(`hello ${name}!`);  // Modified
}

Third, register a babel task and add babel to the dependencies of the default task:

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        babel: {
            options: {
                presets: ['es2015'],
            },

            dist: {
                dest: 'dist/<%= pkg.name %>.es5.js',  // Notice
                src: 'src/<%= pkg.name %>.js',
            },
        },

        uglify: {
            dist: {
                dest: 'dist/<%= pkg.name %>.min.js',
                src: 'dist/<%= pkg.name %>.es5.js',  // Modified
            },
        },

        clean: {
            dist: ['dist'],
        },
    });

    grunt.loadNpmTasks('grunt-babel');
    grunt.loadNpmTasks('grunt-contrib-clean');
    grunt.loadNpmTasks('grunt-contrib-uglify');

    grunt.registerTask('default', ['babel', 'uglify']);  // Modified
};

Four places in the code snippet above must be noticed. First, there is an options property in the configuration for babel. The options property asks Babel to apply all ES2015-related transformations. Second, the output file for the babel:dist target is dist/hello_grunt.es5.js. Third, the output file for babel:dist will be the input file for uglify:dist. Fourth, babel is added to the front of the dependencies array of the default task. Since Grunt runs the tasks in the user-specified order, placing babel before uglify is necessary.

Finally, run the grunt command:

$ grunt
Running "babel:dist" (babel) task

Running "uglify:dist" (uglify) task
>> 1 file created.

Done, without errors.

Babel will generate the following output file dist/hello_grunt.es5.js:

"use strict";

function hello(name) {
    console.log("hello " + name + "!");
}

And UglifyJS will convert it into dist/hello_grunt.min.js:

"use strict";function hello(a){console.log("hello "+a+"!")}

These output files complete our example.

Conclusion

In this post, we covered several aspects of Grunt. We started from the basic usages and then walked through three common tasks such as uglify, clean, and babel transpilation. I hope you enjoy this post. Let's start to automate the Javascript build process with Grunt!

Note

NPM packages and versions referred in this post:

babel-preset-es2015@6.3.13 grunt@0.4.5 grunt-babel@6.0.0 grunt-cli@0.1.13 grunt-contrib-clean@0.7.0 grunt-contrib-uglify@0.11.0