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