Setting up a boilerplate Gruntfile.js

This is an introductory to the Grunt JavaScript task runner command line tool. If you’ve been following my previous posts, you’ll be privy to my JavaScript adventures and my stance on automated web application build tools. I started experimenting with grunt to familiarize myself with its process and its power. As per usual I went to the source to gain knowledge on all things grunt, which resulted in yearning for more detailed examples of Grunt files being used in modern day applications. Most of the information on the interwebs covers very minimal aspects of building a Gruntfile. I hope I can provide a little bit more insight or at least conclude this post with a basic boilerplate Gruntfile to start your projects with. Since, I have chosen Angular.js as my preferred JavaScript MVC framework, I will use an Angular.js seed project to help demonstrates grunts abilities. To re-iterate, Grunt is a command line tool; therefore, I’ll assume you know your way around a terminal window. Lets get started!

You must first install Node.js, it’s required!

Installing Node.js gives you access to the node package manager (npm). You will use npm to install node programs, like  grunt-cli, the command-line tool that is used to run grunt tasks. After installing Node.js, fire up the command-line(Terminal) to confirm the version number as a sanity check. Type:

node -v

this should print out the version of node running on your system.

Screen Shot 2013-05-21 at 11.11.22 AM

CONFIRMED! Next, let’s install grunt-cli using the command-line(Terminal), type:

sudo npm install -g grunt-cli

Grunt-cli is installed globally on your machine. You can’t run a grunt task until you have installed grunt and have created a Grunt file. If you try to run a grunt command via command-line (Terminal) now, it will result in an error. The grunt command looks for a locally installed version of grunt, usually in a project directory. A typical project includes adding two files: package.json and Gruntfile.js to the root of the project directory. There are a few ways you can create each file, and I will mention them briefly, but for learning purposes, be prepared to create each file manually. Lets start off by addressing the package.json file.

The package.json file defines the project meta data, it lists project dependencies, as well as the project name, version, description, source repository, license type, node version, project’s authors and contributors, directories, scripts and more. A package.json file is not required to use grunt. You can install grunt and its various plugins using sudo npm install ‘module’ –save-dev, however a package.json file, makes managing our grunt plugin dependencies a lot easier! In fact, we will include grunt as a development dependency in the package.json .

As mentioned, there are a few methods to create a package.json file for your project, lets discuss them.

The first method is to navigate to the project directory via command-line(Terminal) and install grunt using npm : sudo npm install grunt –save-dev. After the install, you can use grunt-init task to automatically generate a project-specific package.json file.

The second method is to navigate to the project directory via command-line(Terminal) and use the npm init command which walks through the process of generating a common package.json file.

The third method and my preferred method for learning grunt is to create a package.json file manually and save it to the root of the project directory.

If you haven’t created a sample project directory yet, this would be a good time to do so. I named my project directory “sample_app” and I placed it in /Users/manuelgonzalez/sample_app. The name of your project directory will be a requirement for the “name” field in the package.json. There are a few rules to follow when naming your project directory. Never use non-url-safe characters, never use “js” or “node” in the name and never start the name with a dot or an underscore.

Fire up your favorite text editor and type the following :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"name": "sample_app",
"version": "0.0.0",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-copy": "~0.4.0",
"grunt-contrib-concat": "~0.1.3",
"grunt-contrib-clean": "~0.4.0",
"grunt-contrib-uglify": "~0.2.0",
"grunt-contrib-jshint": "~0.3.0",
"grunt-contrib-cssmin": "~0.5.0",
"grunt-contrib-connect": "~0.2.0",
"grunt-contrib-htmlmin": "~0.1.1",
"grunt-contrib-imagemin": "~0.1.2",
"grunt-contrib-livereload": "~0.1.2",
"grunt-contrib-watch": "~0.4.3",
"grunt-usemin": "~0.1.10",
"grunt-rev": "~0.1.0",
"grunt-open": "~0.2.0",
"matchdep": "~0.1.1",
"grunt-google-cdn": "~0.1.1",
"grunt-ngmin": "~0.0.2"

},
"engines": {
"node": ">=0.10"
}

}

and save the file as package.json in your project directory.

Let’s review the meta data in the package.json file.

The “name” field.
The name field is a required field, and the package will not install without it. The name and version together form an identifier that is assumed to be completely unique. Don’t put “js” or “node” in the name, you can specify the engine using the “engines” field. The name ends up being part of a URL, an argument on the command line, and a folder name. Any name with non-url-safe characters will be rejected. Also, it can’t start with a dot or an underscore and keep it short and descriptive.

The “version” field is equally important and is required. As stated above, the name and version together form an identifier that is assumed to be completely unique, if you don’t include the field, the package will not work. You can not use ‘v’ to start the version number. It’s a numeric item separated from the main three-number version by a hyphen, it will be interpreted as a “build” number, and will increase the version. If you do not use a number separated by a hyphen, then it’s treated as a pre-release tag, and is less than the version without a tag.
Changes to the package should come along with changes to the version.

The “devDependencies” is the section where we include the development dependencies for Grunt and its modules(plugins). I’ve added all the basic modules that I feel are helpful to a generic development project including some that are specific to Angular. I have included a quick blurb as to what each task the plugin provides:

‘grunt’: ‘~0.4.1’ – The version of Grunt that you want to use.
‘grunt-contrib-copy’: ‘~0.4.0’ – Copy files and folders.
‘grunt-contrib-concat’: ‘~0.1.3’ – Concatenate files.
‘grunt-contrib-clean’: ‘~0.4.0’ – Clean files and folders.
‘grunt-contrib-uglify’: ‘~0.2.0’ – Minify files with UglifyJS.
‘grunt-contrib-jshint’: ‘~0.3.0’ – Validate files with JSHint.
‘grunt-contrib-cssmin’: ‘~0.5.0’ – Compress CSS files.
‘grunt-contrib-connect’: ‘~0.2.0’ – Start a connect web server.
‘grunt-contrib-htmlmin’: ‘~0.1.1’ – Minify HTML.
‘grunt-contrib-imagemin’: ‘~0.1.2’ – Minify PNG and JPEG images.
‘grunt-contrib-livereload’: ‘~0.1.2’ – Reload assets live in the browser.
‘grunt-contrib-watch’: ‘~0.4.3’ – Run predefined tasks whenever watched file patterns are added, changed or deleted.
‘grunt-usemin’: ‘~0.1.10’ – Grunt task replaces references to non-optimized scripts or style sheets into a set of HTML files (or any templates/views).
‘grunt-rev’: ‘~0.1.0’ – Static file asset revisioning through content hashing
‘grunt-open’: ‘~0.2.0’ – Open urls and files from a grunt task.
‘matchdep’: ‘~0.1.1’ – Use minimatch to filter npm module dependencies by name.
‘grunt-google-cdn’: ‘~0.1.1’ – Grunt task for replacing refs to resources on the Google CDN.
‘grunt-ngmin’: ‘~0.0.2’ – Grunt task for minifying AngularJS projects.

The “engines” field is where you specify the version of node.js that you are targeting.

Lets run npm to download and install the dev dependencies. Open up the command-line(Terminal) and cd to your project directory :

cd /your/project/directory

install the dependencies:

sudo npm install

If you get an error, it’s most likely a json parsing error. Review the package.json and make sure you have the correct syntax and quotes. Save and try to install again.

Once the task has completed, you will find a newly created “node_modules” folder in your project directory containing the installed dependencies defined in the package.json file.

Screen Shot 2013-05-23 at 2.18.03 AM

This is the perfect time to add a JavaScript seed project to your project folder. As I stated previously, to demonstrate the power of grunt I will be using a basic Angular.js project from GitHub. You could use any JavaScript framework that your heart desires, just make sure you follow along and know when to adjust parameters accordingly. Here’s what my project folder looks like after adding the Angular.js seed project.

Screen Shot 2013-05-23 at 2.19.30 AM

I will not be covering the Angular.js seed project in depth, if you are not familiar with Angular.js and you would like to check it out, I advise you visit the website for more info. Lets get crackin’ on the Grunt file!

The Gruntfile is a JavaScript file that defines your projects tasks and configuration. When you run grunt from the command-line(Terminal), Grunt will re-curse upwards till it finds your Gruntfile. This functionality allows you to run Grunt from any sub directory of your project. When grunt is found, grunt-cli loads the local installation of the grunt library, applies the configuration from your Gruntfile, and executes any tasks you’ve defined.

In the root of your project directory, create a new file and save it as Gruntfile.js.

Screen Shot 2013-05-23 at 2.19.55 AM

Lets review the component parts of a Gruntfile before we starting writing code :

(1) The wrapper function.
example:

1
2
3
4
5
6
7
8
9
'use strict';
module.exports = function(grunt) {

// Project configuration.
grunt.initConfig({

});

};

The example above is a basic implementation of the “wrapper” function and is required for your Gruntfile to work. This particular example is in Strict Mode, due to the line of code ‘use strict’;. This is a feature in ECMAScript 5 that allows you to place a program, or a function, in a “strict” operating context. This strict context prevents certain actions from being taken and throws more exceptions, in other words it prevents you from writing bad JavaScript. The function module.exports function is where all the grunt configurations and tasks code will be specified for your build .

(2) Task Configuration.
example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'use strict';
module.exports = function(grunt) {

// Project configuration.
grunt.initConfig({
//task - grunt-contrib-copy
copy: {
main: {
files: [
{expand: true, src: ['path/*'], dest: 'dest/', filter: 'isFile'}, // includes files in path
{expand: true, src: ['path/**'], dest: 'dest/'} // includes files in path and its subdirs
]
}
}

});

};

The grunt.initConfig initializes a task configuration object for the current project. Most Grunt tasks rely on task configuration data defined in the object passed to the grunt.initConfig method. You may store any arbitrary data inside of the configuration object, as long as it doesn’t conflict with properties your tasks require, it will be otherwise ignored. The example above displays a task configuration for the grunt-contrib-copy plugin.

(3) Loading grunt plugins and tasks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
'use strict';
module.exports = function(grunt)

// Project configuration.
grunt.initConfig({
//task - grunt-contrib-copy
copy: {
main: {
files: [
{expand: true, src: ['path/*'], dest: 'dest/', filter: 'isFile'}, // includes files in path
{expand: true, src: ['path/**'], dest: 'dest/'} // includes files in path and its subdirs
]
}
}

});
//load plugin
grunt.loadNpmTasks('grunt-contrib-copy');

};

Continuing from the last example, I have added a new line of code grunt.loadNpmTasks(‘grunt-contrib-copy’);. The loadNpmTasks(pluginName) command loads a specified grunt plugin, in this case grunt-contrib-copy. please note, all plugins must be installed locally via npm, and must be relative to the Gruntfile.

(4) Custom Tasks.
example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
'use strict';
module.exports = function(grunt) {

// Project configuration.
grunt.initConfig({
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= project.dist %>/*',
'!<%= project.dist %>/.git*'
]
}]
},
server: '.tmp'
},
connect: {
options: {
port: 9000,
// Change this to '0.0.0.0' to access the server from outside.
hostname: 'localhost'
},
karma: {
unit: {
configFile: 'karma.conf.js',
singleRun: true
}
}

});
//load plugins
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-karma');
//custom test task
grunt.registerTask('test', [
'clean:server',
'connect:test',
'karma'
]);
//define the default task
grunt.registerTask('default', ['test']);

};

The example above displays a simple grunt file which covers all the basic component parts of a Gruntfile. It defines task configurations, loads the necessary grunt plugins, defines a custom task with arguments and declares a default task. The custom task ‘test’ can have any number of tasks (with or without arguments)in the array. The declared ‘default’ task runs the ‘test’ task automatically, if grunt is executed without any tasks specified.

That concludes our review of the component parts of a Grunt file. Lets move onto building our own Gruntfile with tasks that may be meaningful to your projects. Lets start from the top and work our way down, open your favorite text editor of choice, save your file as Gruntfile.js in your project directory and lets start coding.

As mentioned above lets make this grunt.js file “strict”, so type:

‘use strict’;

Your next line of text deals with livereload, which is an awesome piece of software that monitors changes in files you want it to watch. By requiring the livereloadSnippet we have added coolness to our grunt file, so type:

var lrSnippet = require(‘grunt-contrib-livereload/lib/utils’).livereloadSnippet;

The next line of code also deals with livereload, mainly middle-ware, don’t ask me how it works because their documentation is lacking the info, so just type:

var mountFolder = function (connect, dir) {
return connect.static(require(‘path’).resolve(dir));
};

next lets type up the grunt wrapper method :

module.exports = function (grunt) {
};

Now the first thing we want to do is type the following in the grunt wrapper method :

require(‘matchdep’).filterDev(‘grunt-*’).forEach(grunt.loadNpmTasks);

The new line of code will load all the grunt tasks that have been installed in your node_modules folder. It also eliminates the necessity to load each grunt plug-in line by line. Next lets add a var that will help with configuration, and make life a bit easier on us, next, type:

var projectConfig = {
app: ‘app’,
dist: ‘dist’
};

As you would assume, app is the name of the folder where most of your scripts, images, styles, live, and dist is the name of the folder your grunt file will place all minified assets and scripts into for the production build. Next lets add the initConfig method and decide what we should add for tasks.


grunt.initConfig({
});

Now we are ready to start adding tasks that would be useful towards a generic project. Since every one of my projects are different, I really don’t have a silver bullet for all projects, but I usually use the basics tasks like minification, concatenation, copy, clean and jshinting to help in my development. Instead of having you add tasks one at a time, here’s my boiler plate Gruntfile.js including the custom ‘build’ task set as the default.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
'use strict';
var lrSnippet = require('grunt-contrib-livereload/lib/utils').livereloadSnippet;
var mountFolder = function (connect, dir) {
    return connect.static(require('path').resolve(dir));
};

module.exports = function (grunt) {

    // load all grunt tasks
    require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);

    // configurable paths
    var projectConfig = {
        app: 'app',
        dist: 'dist'
    };
    // Project configuration.
    grunt.initConfig({
        project: projectConfig,
        watch: {
            livereload: {
                files: [
                    '<%= project.app %>/{,*/}*.html',
                    '{.tmp,<%= project.app %>}/css/{,*/}*.css',
                    '{.tmp,<%= project.app %>}/js/{,*/}*.js',
                    '<%= project.app %>/img/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
                ],
                tasks: ['livereload', 'compass']

            }
        },
        connect: {
            options: {
                port: 9000,
                // Change this to '0.0.0.0' to access the server from outside.
                hostname: 'localhost'
            },
            livereload: {
                options: {
                    middleware: function (connect) {
                        return [
                            lrSnippet,
                            mountFolder(connect, '.tmp'),
                            mountFolder(connect, projectConfig.app)
                        ];
                    }
                }
            },
            test: {
                options: {
                    middleware: function (connect) {
                        return [
                            mountFolder(connect, '.tmp'),
                            mountFolder(connect, 'test')
                        ];
                    }
                }
            }
        },
        open: {
            server: {
                url: 'http://localhost:<%= connect.options.port %>'
            }
        },
        clean: {
            dist: {
                files: [{
                    dot: true,
                    src: [
                        '.tmp',
                        '<%= project.dist %>/*',
                        '!<%= project.dist %>/.git*'
                    ]
                }]
            },
            server: '.tmp'
        },
        jshint: {
            all: [
                'Gruntfile.js',
                '<%= project.app %>/js/{,*/}*.js'
            ],
            options: {
                jshintrc: '.jshintrc'
            }
        },
        concat: {
            dist: {
                files: {
                    '<%= project.dist %>/js/scripts.js': [
                        '.tmp/js/{,*/}*.js',
                        '<%= project.app %>/js/{,*/}*.js',
                        '<%= project.app %>/partials/**/*.js'
                    ]
                }
            }
        },
        useminPrepare: {
            html: '<%= project.app %>/**/*.html',
            options: {
                dest: '<%= project.dist %>'
            }
        },
        usemin: {
            html: ['<%= project.dist %>/**/*.html'],
            css: ['<%= project.dist %>/**/*.css'],
            options: {
                basedir: '<%= project.dist %>',
                dirs: ['<%= project.dist %>']
            }
        },
        imagemin: {
            dist: {
                files: [
                    {
                        expand: true,
                        cwd: '<%= project.app %>/img',
                        src: '{,*/}*.{png,jpg,jpeg}',
                        dest: '<%= project.dist %>/img'
                    }
                ]
            }
        },
        cssmin: {
            dist: {
                options: {
                    report: 'min'
                },
                files: {
                    '<%= project.dist %>/css/app.css': [
                        '<%= project.app %>/css/app.css'
                    ]
                }
            }
        },
        htmlmin: {
            dist: {
                options: {
                },
                files: [{
                    expand: true,
                    cwd: '<%= project.app %>',
                    src: [
                        '*.html',
                        'partials/**/*.html'
                    ],
                    dest: '<%= project.dist %>'
                }]
            }
        },
        cdnify: {
            dist: {
                html: ['<%= project.dist %>/*.html']
            }
        },
        ngmin: {

            controllers: {
                src: ['<%= project.dist %>/js/scripts.js'],
                dest: '<%= project.dist %>/scripts/scripts.js'
            }
        },
        uglify: {
            options: {
                report: 'min'
            },
            dist: {
                files: {
                    '<%= project.dist %>/scripts/scripts.js': ['<%= project.dist %>/scripts/scripts.js']
                }
            }
        },
        rev: {
            dist: {
                files: {
                    src: [
                        '<%= project.dist %>/scripts{,*/}*.js',
                        '<%= project.dist %>/styles/**/*.css',
                        '<%= project.dist %>/img/**/*.{png,jpg,jpeg,gif,webp,svg}',
                        '<%= project.dist %>/css/*'
                    ]
                }
            }
        },
        copy: {
            dist: {
                files: [{
                    expand: true,
                    dot: true,
                    cwd: '<%= project.app %>',
                    dest: '<%= project.dist %>',
                    src: [
                        '*.{ico,txt,png}',
                        '.htaccess',
                        'lib/**/*',
                        'img/{,*/}*.{gif,webp}',
                        'partials/**/*',
                        'css/*'
                    ]
                }]
            }
        }
    });

    grunt.registerTask('server', [
        'clean:server',
        'livereload-start',
        'connect:livereload',
        'open',
        'watch'
    ]);
    grunt.registerTask('build', [
        'clean:dist',
        'jshint',
        'useminPrepare',
        'imagemin',
        'cssmin',
        'htmlmin',
        'concat',
        'copy',
        'cdnify',
        'ngmin',
        'uglify',
        'rev',
        'usemin'

    ]);


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

};

The paths in the tasks posted in the code above, point to files in the Angular seed project, make sure to revise the paths to adhere to your project. Also, take note that I have not included a test task or a documentation task. I did not include them because they are based off of personal preference. To test the Gruntfile, open up the command-line(Terminal) and cd to your project directory :

cd /your/project/directory

and run the default grunt task default by typing

grunt

You should see the output of each task ran by Grunt and finally end up with the “Done, without errors.” text. There you have it a simple boiler plate Grunt file to get you up and running. As I mentioned, I normally would add other tasks like unit testing, documentation, compass, compress to name a few, but thats all based off the projects needs or use cases.
Enjoy.
DOWNLOAD – Gruntfile.js (625)

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: