My Gruntfile.js: An example Gruntfile and My Workflow

I’m building a site on the MEAN stack; codename Zoltar. To build and run my tasks, I use the wonderful Grunt. I’d like to explore the Gruntfile.js and explain what does what and why I chose to do it that way. Hopefully this example will assist people trying to solve the same (common) problems.

Let’s look at the entire Gruntfile.js, then we can dig into each section.

[code lang="js"]
'use strict';

module.exports = function (grunt) {
grunt.initConfig({
pkg: require('./package.json'),
ngmin: {
zoltar: {
cwd: 'public/javascripts/zoltar',
expand: true,
src: ['**/*.js'],
dest: 'public/javascripts/dist/generated'
}
},
uglify: {
options: {
report: 'min',
sourceMap: 'public/javascripts/dist/<%= pkg.name %>-<%= pkg.version %>.map.js',
sourceMapRoot: '/',
sourceMapPrefix: 1,
sourceMappingURL: '/javascripts/dist/<%= pkg.name %>-<%= pkg.version %>.map.js'
},
dist: {
files: {
'public/javascripts/dist/<%= pkg.name %>-<%= pkg.version %>.min.js':
require('./support.json')
.concat('public/javascripts/dist/generated/**/*.js')
}
}
},
karma: {
unit: {
configFile: 'karma.conf.js'
}
},
watch: {
scripts: {
files: [
'public/javascripts/zoltar/**/*.js',
'public/javascripts/support/**/*.js',
'!public/javascripts/dist/**/*.js'
],
tasks: ['ngmin', 'uglify', 'docular']
},
serverTests: {
files: ['config/**/*.js', 'server.js', 'models/**/*.js',
'routes/**/*.js', 'utils/**/*.js', 'spec/**/*.js', 'public/schemas/**/*.json'],
tasks: ['jasmine_node']
},
clientTests: {
files: ['public/test/spec/**/*.js', 'public/javascripts/zoltar/**/*.js', 'public/javascripts/support/**/*.js'],
tasks: ['karma']
}
},

concurrent: {
target: {
tasks: ['build', 'watch', 'nodemon'],
options: {
logConcurrentOutput: true
}
}
},
jasmine_node: {
projectRoot: "./spec"
},
nodemon: {
dev: {
options: {
file: 'server.js',
watchedExtensions: ['js', 'json'],
ignoredFiles: ['node_modules/**', 'public/**'],
nodeArgs: ['--debug']
}
}
},

docular: {
showAngularDocs: true,
groups: [
{
groupTitle: 'Zoltar Client',
groupId: 'client',
sections: [
{
id: 'zoltar',
title: 'Application',
showSource: true,
scripts: [
'public/javascripts/zoltar/zoltar.js'
]
},
{
id: 'zoltarIndex',
title: 'Main Page',
showSource: true,
scripts: [
'public/javascripts/zoltar/index/index.js'
]
},
{
id: 'zoltarAdmin',
title: 'Admin Console',
showSource: true,
scripts: [
'public/javascripts/zoltar/admin/admin.js'
]
},
{
id: 'zoltarCommon',
title: 'Common Functionality',
showSource: true,
scripts: [
'public/javascripts/zoltar/common/loginctrl.js',
'public/javascripts/zoltar/common/headerctrl.js',
'public/javascripts/zoltar/common/socket.js',
'public/javascripts/zoltar/common/placeholder.js',
'public/javascripts/zoltar/common/ladda.js',
'public/javascripts/zoltar/common/schema.js',
'public/javascripts/zoltar/common/schemaform.js'

]
}
]
},
{
groupTitle: 'Zoltar Server',
groupId: 'server',
showSource: false,
sections: [
{
id: 'utils',
title: 'Utilities',
scripts: [

]
},
{
id: 'models',
title: 'Models',
scripts: [
'models/index.js'
]
}
]
}
]
}
});

grunt.loadNpmTasks('grunt-karma');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-nodemon');
grunt.loadNpmTasks('grunt-concurrent');
grunt.loadNpmTasks('grunt-docular');
grunt.loadNpmTasks('grunt-ngmin');
grunt.loadNpmTasks('grunt-jasmine-node');

grunt.registerTask('test', ['jasmine_node', 'karma']);
grunt.registerTask('build', ['ngmin', 'uglify', 'docular']);
grunt.registerTask('default', ['build', 'test']);
grunt.registerTask('start', ['concurrent']);

};
[/code]

Each section, in order of how they appear, which is in no particular order:

pkg

[code lang="js"]
pkg: require('./package.json'),
[/code]

You are probably always going to want this, but it’s not required. With this line, we include our package.json file, which has references to the app’s name and version in it. We use this name and version to generate filenames later on. There’s no other reason for me to be doing this other than to get that information, so if you want you can simply hardcode it.

ngmin

[code lang="js"]
ngmin: {
zoltar: {
cwd: 'public/javascripts/zoltar', // not absolutely necessary, but builds "src" below relative to it
expand: true, // allows you to output multiple files
src: ['**/*.js'], // files we're looking for within "cwd"
dest: 'public/javascripts/dist/generated' // path to generated files
}
},
[/code]

grunt-ngmin is a task for Grunt that runs ngmin. ngmin allows you to write your AngularJS code like this:

[code lang="js"]
angular.module('myApp', []).controller('myCtrl', function($scope, $location, $timeout) {
// etc
});
[/code]

Of course, if you didn’t know, this breaks AngularJS’ dependency injection upon minification (more about that later). To fix this breakage, what you need to do is use the more verbose syntax:

[code lang="js"]
angular.module('myApp', []).controller('myCtrl', ['$scope', '$location', '$timeout',
function($scope, $location, $timeout) {
// etc
}]);
[/code]

But who wants to write that? Pretty much nobody unless you really like typing. ngmin takes the first example and outputs the second example, which is all ready for minification.

zoltar within the ngmin block is simply a target; its name can be whatever you want, as long as it’s a valid object key name. expand: true is necessary only if you want to parse multiple files. If you only have one JS file (say, because of concatenation, or you just like to put everything in one file for whatever reason) you don’t need expand and your dest will be a single file name instead of a directory.

uglify

[code lang="js"]
uglify: {
options: {
report: 'min',
sourceMap: 'public/javascripts/dist/<%= pkg.name %>-<%= pkg.version %>.map.js',
sourceMapRoot: '/',
sourceMapPrefix: 1,
sourceMappingURL: '/javascripts/dist/<%= pkg.name %>-<%= pkg.version %>.map.js'
},
dist: {
files: {
'public/javascripts/dist/<%= pkg.name %>-<%= pkg.version %>.min.js':
require('./support.json')
.concat('public/javascripts/dist/generated/**/*.js')
}
}
},
[/code]

Ah, the fun stuff. grunt-contrib-uglify uses the Uglify2 tool to compress JS. Why do you want this? Especially with “fat” client apps, this will save you a lot of KB and reduce loading time in the browser (note: we don’t need to compress our server-side code, just the client-side code).

Unless you are using source maps (see next paragraph), I recommend not compressing your JS while developing. Compression takes time on the wall clock, especially if you have a lot of 3rd-party dependencies, and your workflow will be quicker without it. I’ve configured Zoltar to use the raw JS files in development mode, and it uses the compressed JS in production mode. So while Grunt is configured to compress automatically, I don’t have to wait for it to complete before reloading my browser to see my code changes.

This article explains what source maps are. The benefit is obvious; if you are running compressed code, it is incredibly difficult to debug due to line breaks being removed and variables being renamed.

options

[code lang="js"]
options: {
report: 'min',
sourceMap: 'public/javascripts/dist/<%= pkg.name %>-<%= pkg.version %>.map.js',
sourceMapRoot: '/',
sourceMapPrefix: 1,
sourceMappingURL: '/javascripts/dist/<%= pkg.name %>-<%= pkg.version %>.map.js'
},
[/code]

I had some trouble setting up source maps, so this might not be quite right, but it is currently working.

The report property just says “tell me how much KB I saved, and log it to std out”. Nice to know.

sourceMap tells Uglify we want to generate a source map. It won’t do this without the property. The value gathers our app name and version from the pkg property mentioned earlier, and it puts a file called (for example) zoltar-0.0.1.map.js in the public/javascripts/dist/ directory.

sourceMapRoot says “start with the root project folder”. You’ll probably want this unless your sourceMap path is relative to something else.

sourceMapPrefix says “remove the first chunk of the path when declaring files in the source map”. The source map will have references to all of the JS files it describes, and it needs to be a valid relative path for the web server. My static files are all in public/, so all valid paths to JS start with javascripts/, therefore I need to drop public/.

sourceMappingURL is the URL put in the compressed JS file referencing the source map. Browsers will find this special comment and look for the source map when you open up your dev tools. Uglify was generating invalid paths and the source map could not be found, so I used this option to hardcode the path. Use this if you have to.

dist

[code lang="js"]
dist: {
files: {
'public/javascripts/dist/<%= pkg.name %>-<%= pkg.version %>.min.js':
require('./support.json')
.concat('public/javascripts/dist/generated/**/*.js')
}
}
[/code]

dist is just another identifier; it can be anything. The files object will be a key/value pair of generated file names to an array of file globs; the right-hand side of the declaration will be a description of all the files you want compressed, and the left-hand side will be the resulting file.

Often with 3rd-party libraries, you must load them in a particular order. I keep this order in a file called support.json:

[code lang="js"]
[
"public/javascripts/support/es5-sham.min.js",
"public/javascripts/support/custom.modernizr.js",
"public/javascripts/support/jquery-1.10.2.js",
"public/javascripts/support/angular.js",
"public/javascripts/support/ui-bootstrap-0.5.0.js",
"public/javascripts/support/spin.min.js",
"public/javascripts/support/ladda.min.js",
"public/javascripts/support/underscore.js",
"public/javascripts/support/restangular.js",
"public/javascripts/support/socket.io.js",
"public/javascripts/support/validator-min.js"
]
[/code]

For instance, if I want AngularJS to use jQuery instead of jqLite, I must include jQuery before it. I’m not sure how others solve this problem, but this works for me.

Here, I build an array using the files from support.json, then I append a glob that references all of the files generated by ngmin (in the generated/) directory. This loads my 3rd-party libs first, then my app second.

karma

[code lang="js"]
karma: {
unit: {
configFile: 'karma.conf.js'
}
},
[/code]

Karma is my test runner for my client-side code. It launches a browser (Chrome, but this is configurable elsewhere, which is beyond the scope of this post) and executes my tests in it.

unit is simply an identifier and can be anything. All this says is “read my karma.conf.js” and figure out how to run the tests. Again, the contents of this file are beyond the scope here, so assuming you have a legit config file, this is all you need to run tests. Of course there are other options you can specify here, but I don’t need them. This is about my Gruntfile.js after all. :D

watch

[code lang="js"]
watch: {
scripts: {
files: [
'public/schemas/*.json', 'public/javascripts/zoltar/**/*.js',
'public/javascripts/support/**/*.js',
'!public/javascripts/dist/**/*.js'
],
tasks: ['ngmin', 'uglify', 'docular']
},
serverTests: {
files: ['config/**/*.js', 'server.js', 'models/**/*.js',
'routes/**/*.js', 'utils/**/*.js', 'spec/**/*.js', 'public/schemas/**/*.json'],
tasks: ['jasmine_node']
},
clientTests: {
files: ['public/test/spec/**/*.js', 'public/javascripts/zoltar/**/*.js', 'public/javascripts/support/**/*.js'],
tasks: ['karma']
}
},
[/code]

grunt-contrib-watch simply watches files and executes more Grunt tasks when the files change. This is a major part of my workflow.

scripts

[code lang="js"]
scripts: {
files: [
'public/javascripts/zoltar/**/*.js',
'public/javascripts/support/**/*.js',
'!public/javascripts/dist/**/*.js'
],
tasks: ['ngmin', 'uglify', 'docular']
},
[/code]

scripts is an identifier. Each of these sections has two properties; files which describes which files to watch, and tasks, which describes what tasks to execute when those files change.

This section handles client-side code. Probably could have given it a more descriptive name, but whatever.

  • public/schemas/zoltar/**/*.js: This is my application code. It changes, we need to recompress the files. We execute ngmin and uglify to do this, then we execute docular to update the API documentation (more on this later).
  • public/schemas/support/**/*.js: Third-party libraries. We don’t really need to generate the documentation, but we still compress everything, so ngmin/uglify needs to run.
  • !public/javascripts/dist/**/*.js This says don’t watch anything in the dist/ directory. Reason being is that all this code is generated by Grunt. There may be protections against this, but we don’t want to be generating files then generating more files because those generated files change. I don’t think this would be an infinite loop but who wants to take that chance??

serverTests

[code lang="js"]
serverTests: {
files: ['config/**/*.js', 'server.js', 'models/**/*.js',
'routes/**/*.js', 'utils/**/*.js', 'spec/**/*.js', 'public/schemas/**/*.json'],
tasks: ['jasmine_node']
},
[/code]

When we update anything on the server side, I want to rerun my tests. I use jasmine-node to run tests, so I get to write both my server- and client-side tests in Jasmine. (I’m including the mongoose-gen schema files in this watch, but I may not need to since I don’t test the schemas directly.)

clientTests

[code lang="js"]
clientTests: {
files: ['public/test/spec/**/*.js', 'public/javascripts/zoltar/**/*.js', 'public/javascripts/support/**/*.js'],
tasks: ['karma']
}
[/code]

All of our client tests are in public/test/spec/ so if any of these files change I want to rerun the tests. Likewise if any of the app code or 3rd-party libs change (like if we have a new version), we should rerun the tests.

concurrent

[code lang="js"]
concurrent: {
target: {
tasks: ['build', 'watch', 'nodemon'],
options: {
logConcurrentOutput: true
}
}
},
[/code]

grunt-concurrent allows you to run several commands at once. To start my server, I need to first build it, then we set up our watches, and finally launch the server with nodemon. build doesn’t need to be concurrent, since the task simply executes and ends, but watch and nodemon are both continuous processes.

logConcurrentOutput just means to output anything the concurrent tasks output. There really isn’t anything else interesting about this.

jasmine_node

[code lang="js"]
jasmine_node: {
projectRoot: "./spec"
},
[/code]

All this says is “look in the spec/ dir for my server-side tests”. Omit this and the task will look through your entire application structure, and if you have any files with “spec” in them, they will be executed too. If you are using Jasmine for client tests, you will likely have these files, but you don’t want them executed by jasmine-node (they get run by Karma).

nodemon

[code lang="js"]
nodemon: {
dev: {
options: {
file: 'server.js',
watchedExtensions: ['js', 'json'],
ignoredFiles: ['node_modules/**', 'public/**'],
nodeArgs: ['--debug']
}
}
[/code]

nodemon is just a handy tool for running your app, as it automatically restarts if files change, and it has some other features. Here, I have only one target, dev. You don’t need nodemon to run a production server.

Regarding the options:

  • file: This is your main app script; in my case, server.js.
  • watchedExtensions: Files with these extensions will cause the server to reload.
  • ignoredFiles: globs representing stuff to not watch for restarting. Don’t need to watch the public files (though I should probably be watching the schema definitions) and there’s probably all sorts of stuff happening in node_modules as my app runs that I don’t need to worry about.
  • nodeArgs: Arguments to pass directly to the node binary. In this case, we want to launch a debug server, so we specify the --debug parameter.

docular

[code lang="js"]
docular: {
showAngularDocs: true,
groups: [
{
groupTitle: 'Zoltar Client',
groupId: 'client',
sections: [
{
id: 'zoltar',
title: 'Application',
showSource: true,
scripts: [
'public/javascripts/zoltar/zoltar.js'
]
},
{
id: 'zoltarIndex',
title: 'Main Page',
showSource: true,
scripts: [
'public/javascripts/zoltar/index/index.js'
]
},
{
id: 'zoltarAdmin',
title: 'Admin Console',
showSource: true,
scripts: [
'public/javascripts/zoltar/admin/admin.js'
]
},
{
id: 'zoltarCommon',
title: 'Common Functionality',
showSource: true,
scripts: [
'public/javascripts/zoltar/common/loginctrl.js',
'public/javascripts/zoltar/common/headerctrl.js',
'public/javascripts/zoltar/common/socket.js',
'public/javascripts/zoltar/common/placeholder.js',
'public/javascripts/zoltar/common/ladda.js',
'public/javascripts/zoltar/common/schema.js',
'public/javascripts/zoltar/common/schemaform.js'

]
}
]
},
{
groupTitle: 'Zoltar Server',
groupId: 'server',
showSource: false,
sections: [
{
id: 'utils',
title: 'Utilities',
scripts: [

]
},
{
id: 'models',
title: 'Models',
scripts: [
'models/index.js'
]
}
]
}
]
}
[/code]

Finally, we have the Docular task options. Docular generates API documentation based on inline comments (with a JSDoc-style syntax) and other files. Docular is very much an unfinished product and I’ve had lots of problems getting it to work properly, but this basically works. There are other options for this (like grunt-ngdocs) but I found Docular to be the most full-featured, even though some of those features are wonky.

The showAngularDocs property tells Docular to generate AngularJS documentation. If I could figure out how to link to it, that would be even better, so you can link up your injected AngularJS services (using @requires) to their actual documentation. If anyone knows how to reference these files from within my own application docs, please help!

The main thing we define is groups. Documentation is arranged into groups and sections. We have two groups; server- and client-side code. Each of these is then further divided into sections.

The anatomy of my groups:

  • groupTitle: This group needs a name.
  • groupId: Unique identifier for this group. Appears in URLs.
  • showSource: Either true or false; enables the reader to see the source code behind each documented component. If you are documenting client-side code, this is certainly OK to use, since your JS is exposed anyway. You may or may not want to hide source for your server-side code (closed-source server?), but since mine’s on Github I don’t think it matters.
  • sections: Each group is subdivided into these sections. Like groups each section has a title and a unique id. In addition you will have a list of script files to read for inline (comment-based) documentation. I don’t think I had any success using globs here, so I just list all of the files that need to be read.

There are of course many other options for Docular, such as including docs that are not inline (like a README or overview or something), but these are the ones I’m using.

Addendum: Workflow

So basically all I do is issue a grunt start at my command line. This builds, sets up a watch, and runs the server. The watch of course takes care of running my unit tests. If I want to just run my tests, a grunt test works out great.

If I want to read my docs, I issue grunt docular-server which launches a web server on a different port that serves up my documentation. For whatever reason Docular works this way; I guess I’m used to documentation generators that just make static files.

Anyway, that’s about it. Hope this helps somebody.

Update 1

I was originally using grunt.file.readJSON to read my JSON files; it turns out require does basically the same thing. As I understand readJSON has more verbose logging, so if you are having trouble reading your JSON files, give that a try.

Update 2

Fixed references when using require: you will need a ./ in front of your filename.

2 thoughts on “My Gruntfile.js: An example Gruntfile and My Workflow

  1. Permalink  ⋅ Reply

    Pedja

    April 25, 2014 at 8:19am

    Just wanted to thank you for thorough explanation of Gruntfile.js and for posting your workflow – you saved me days of figuring everything out and also this is almost the exact same workflow I had in mind :)
    I do want to point out that grunt-nodemon has changed recently so Gruntfile.js options should be updated:
    file is now script and is pulled outside of options object,
    watchedExtensions is now ext,
    ignoredFiles is now ignore.
    Thanks to answer at stackoverflow for this.
    Thanks again :)

Leave a Reply

Your email will not be published. Name and Email fields are required.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>