Introduction to Grunt - JavaScript Application Design: A Build First Approach (2015)

JavaScript Application Design: A Build First Approach (2015)

Appendix B. Introduction to Grunt

Grunt is a tool that allows you to write, configure, and automate tasks—such as minifying a JavaScript file or compiling a LESS style sheet—for your application.

LESS is a CSS preprocessor, which is covered in chapter 2. Minifying is essentially creating a smaller file by removing white space and many syntax tree optimizations. These tasks can also be related to code quality, such as running unit tests (covered in chapter 8) or executing a code coverage tool such as JSHint. They could certainly be related to the deployment process: maybe deploying the application over FTP, or preparing to deploy it, generating API documentation.

Grunt is merely a vehicle to execute your build tasks. These tasks are defined using plugins, as explained next.

B.1. Grunt plugins

Grunt only provides the framework; you’re in charge of picking the right plugins to perform the tasks you need. For example, you might use the grunt-contrib-concat to bundle assets together. You also need to configure these plugins to do what you want; for example, providing a list of files to bundle and the path to the resulting bundled file.

Plugins can define one or more Grunt tasks. These plugins are written and configured using JavaScript on the Node platform. The Node community developed and maintains hundreds of ready-made Grunt plugins, which you only need to configure, as you’ll see in a moment. You can also create Grunt plugins yourself if you can’t find one that suits your particular needs.

B.2. Tasks and targets

Tasks can be configured to conform to multiple targets, and each target is defined by adding more data when configuring the task. A common use of task targets is to compile an application for different distributions, as explained in chapter 3. Targets are useful for reusing the same task for slightly different purposes. LESS is an expressive language that compiles to CSS. You might have LESS task targets that compile different parts of your application. Maybe you need to use different targets because one of them makes debugging easier for you by adding source maps that point to the original LESS code, while the other target might go as far as minifying your style sheet.

B.3. Command-line interface

Grunt comes with a command-line interface (CLI), called grunt, which you can use to run your tasks. As an example of how this tool works, let’s analyze the following statement:

grunt less:debug mocha

Assuming you’ve already configured Grunt, which you’ll learn about in a moment, this statement would execute the debug target for the less task, and, if that task succeeded, then any targets configured for the mocha task would get executed. It’s important to note that if a Grunt task fails, Grunt won’t attempt to run any more tasks. Instead, it will exit after printing the reasons why it failed.

It’s worth mentioning that tasks are executed serially: the next task begins once the current task finishes. They don’t run in parallel. Instead of giving the CLI a full task list every time, you can use task aliases: tasks that execute a list of tasks. If you use the special name default when creating an alias, then the tasks assigned to that alias will be run whenever the grunt CLI is executed without any task arguments.

Enough theory! Let’s get our hands dirty with hands-on Grunting; you’ll start by installing Grunt and expand on all of the areas we’ve discussed. To install Grunt, the first thing you’ll need is Node, the platform Grunt works on. To install Node, head over to appendix A on Node, and then get right back here. I’ll wait.

Okay, let’s install the Grunt CLI. Using npm in your terminal, type the following command:

npm install --global grunt-cli

The --global flag tells npm that this isn’t a project-level package install, but rather a system-wide install. Essentially, this will ultimately enable you to use the package from your command line directly. You can verify the CLI was installed properly by running the following command:

grunt --version

That should output the version number for the currently installed version of the Grunt CLI. Great! Everything you did so far was a one-time thing; you don’t need to worry about doing any of those steps again. But how do you use Grunt?

B.4. Using Grunt in a project

Let’s say you have a PHP web application (although the server-side language doesn’t matter), and you want to automatically run a linter, which is a static analysis tool that can tell you about issues with the syntax you’re using, whenever you change a JavaScript file.

The first thing you’ll need is a package.json file in your project root. This is used by npm to keep a manifest of all the dependencies you have. This file doesn’t need much; it needs to be a valid JSON object, so {} will do. Change the directory to your application’s root directory and type the following into your terminal:

echo "{}" > package.json

Next, you’ll have to install a few dependencies. You’ll install grunt, which is the framework itself, not to be confused with grunt-cli, which looks things up and defers task execution to the locally installed grunt package. To get started, you’ll also install grunt-contrib-jshint, an easy-to-configure task to run JSHint, a JavaScript lint tool, as a Grunt task. The npm install command allows you to install more than one package at once, so let’s do that:

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

The --save-dev flag tells npm to include these packages in the package.json manifest, and tag them as development dependencies. It’s a best practice to mark as a development dependency anything that shouldn’t be executed in production servers. Build components should always run before executing the application.

You have the framework, the plugin, and the CLI; all that’s missing is configuring the tasks, so you can start using Grunt.

B.5. Configuring Grunt

To configure Grunt, you need to create a Gruntfile.js file. That’s where all your build task configuration and definitions will live. The following code is an example Gruntfile.js:

module.exports = function (grunt) {

grunt.initConfig({

jshint: {

browser: ['public/js/**/*.js']

}

});

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

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

};

As explained in appendix A on Node.js, where we discussed Common.JS modules, here the module exports a function, which Grunt will invoke, configuring your tasks. The initConfig method takes an object, which will serve as the configuration for all of your different tasks and targets. Each top-level property in this configuration object represents configuration for a particular task. For example, jshint contains the configuration for the jshint task. Properties in each task’s configuration represent target configuration.

In this case, you’re configuring the browser target for jshint to ['public/js/**/*.js']. This is called a globbing pattern, and it’s used to declare which files to target. You’ll learn all about globbing patterns in a moment; for now it should suffice to say that it’ll match any .js files in public/js or in a subdirectory.

The loadNpmTasks method tells Grunt, “Hey, load any tasks you can find in this Grunt plugin,” so it’s essentially loading the jshint task. You’ll learn how to write your own tasks later.

Last, registerTask can define task aliases by passing it a task name and an array of tasks it should execute. You’ll set it to jshint so it will run jshint:browser and any other jshint targets you might add in the future. The default name means that this task will be run whenever you execute grunt with no task arguments in the command line. Let’s try that!

grunt

Congratulations, you’ve executed your first Grunt task! However, you’re probably confused about the whole “globbing for files” thing; let’s fix that.

B.6. Globbing patterns

Using patterns such as ['public/js/**/*.js'] helps quickly define what files to work with. These patterns are easy to follow, as long as you understand how to use them appropriately. Glob allows you to write plain text to refer to real file system paths. For example, you could usedocs/api.txt without any special characters, and that would match the file at docs/api.txt. Note that this is a relative path, and that it’ll be relative to your Gruntfile.

If you add special characters into the mix, things get interesting. For instance, changing your last example to docs/*.txt helps us match all text files in the docs directory. If you’d like to include subdirectories as well, then you need to use **, known as the globstar pattern:docs/**/*.txt.

B.6.1. Brace expressions

Then there’s brace expansion. Suppose you want to match many different types of images; you might want to use something akin to the following pattern: images/*.{png,gif,jpg}. That’d match any images ending in .png, .gif, and .jpg. It’s not limited to extensions, although that’s the most common use case. You could also use brace expansion to match different directories: public/{js,css}/**/*. Note that we’re excluding the extension. That works fine; the star will match any file type and not be limited to one in particular.

B.6.2. Negation expressions

Last, there are negation expressions and these are somewhat tricky to get right. Negation expressions can be defined as “remove the matching results from what you’ve matched so far.” Patterns are processed in order, so inclusion and exclusion order is significant. Negation patterns begin with an !. Here’s a common use case: ['js/**/*.js', '!js/vendor/**/*.js']. That says, “Include everything that’s in the js directory, but not if it’s in js/vendor.” That’s useful for linting code you’ve authored, while leaving third-party libraries alone.

There’s one particular caveat of globbing I’d like to address; I often read people complaining about ['js', '!js/vendor'] “not working,” and the reason for that is rather simple to understand now that you know how globbing works. The first globbing pattern will match the js directory itself, and the !js/vendor won’t do anything. Later, the js directory will be expanded to every file in it, including those in js/vendor. A quick fix to this issue is to have the Globber expand the directories for you, using globstars: ['js/**/*.js', '!js/vendor/**'].

There are two more topics for you to gulp down: configuring tasks and creating your own ones. Let’s go ahead and see how we can configure Grunt to run a task from the ground up.

B.7. Setting up a task

Now you’re going to learn how to set up a random task...by browsing the internet! As a quick-start trick, let’s go back to the original example from section B.1. Remember how you configured it to run JSHint? Here’s the code you used:

module.exports = function (grunt) {

grunt.initConfig({

jshint: {

browser: ['public/js/**/*.js']

}

});

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

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

};

Let’s suppose you want to minify (covered in chapter 2) your CSS style sheets, and then concatenate them into a single file. You could Google around for grunt plugins to do that, or you might visit http://gruntjs.com/plugins and look around for yourself. Go ahead and visit that page and then type css. One of the first results you’ll see is grunt-contrib-cssmin, and it’ll link to the page for that package on the npm website.

On npm, you’ll usually find detailed README files, and links to the complete source code on GitHub repositories. In this case, it instructs you to install the package from npm and add a loadNpmTasks to your Gruntfile.js, as shown in the following code:

module.exports = function (grunt) {

grunt.initConfig({

jshint: {

browser: ['public/js/**/*.js']

}

});

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

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

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

};

You’d also have to install the package from npm, the way you did with grunt-contrib-jshint earlier:

npm install --save-dev grunt-contrib-cssmin

Now all you need to do is to configure it. Grunt projects are usually well documented, giving you a few configuration examples on their home pages, as well as detailed lists of all the options available to them. Packages named grunt-contrib-* were developed by the team behind Grunt itself, so they should mostly work without any problems. When canvassing for the right package for a task, move on if something doesn’t work, or isn’t well documented. You don’t have to marry them. Popularity (npm installs and GitHub stars) are good indicators of how good a package is.

It turns out that the first use example shows that you can also concatenate your CSS with this package, so you don’t need an extra task to do that. Here’s that example, showing how you can combine two files while minifying them using grunt-contrib-cssmin:

cssmin: {

combine: {

files: {

'path/to/output.css': ['path/to/input_one.css', 'path/to/input_two.css']

}

}

}

You can easily adapt and integrate that with your needs. You’ll also add a build task alias. Aliases are useful for defining workflows, as you’ll see throughout part 1. For instance, chapter 3 uses them to define the debug and release workflows:

module.exports = function (grunt) {

grunt.initConfig({

jshint: {

browser: ['public/js/**/*.js']

},

cssmin: {

all: {

files: { 'build/css/all.min.css': ['public/css/**/*.css'] }

}

}

});

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

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

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

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

};

That’s it! If you run grunt build in your terminal, it’ll bundle your CSS together and then minify it, writing it to the all.min.css file. You can find this example, along with the others we’ve discussed so far, in the accompanying source code samples, underappendix/introduction-to-grunt. Let’s wrap up this appendix by explaining how you can write your own Grunt task.

B.8. Creating custom tasks

Grunt has two kinds of tasks: multitasks and regular tasks. The difference, as you might suspect, is that multitasks allow consumers to set up different task targets and run them individually. In practice, almost all Grunt tasks are multitasks. Let’s walk through creating one!

You’ll create a task that can count words in a list of files, and then have it fail if it counts more words than what it expected. To begin with, let’s glance at this piece of code:

grunt.registerMultiTask('wordcount', function () {

var options = this.options({

threshold: 0

});

});

Here, you’re setting a default value for the threshold option, which can be overwritten when the task gets configured, as you’ll see in a minute. Because you used registerMultiTask, you can support multiple task targets. Now you need to go through the list of files, read them, and count the words in them:

var total = 0;

this.files.forEach(function (file) {

file.src.forEach(function (src) {

if (grunt.file.isDir(src)) {

return;

}

var data = grunt.file.read(src);

var words = data.split(/[^\w]+/g).length;

total += words;

grunt.verbose.writeln(src, 'contains', words, 'words.');

});

});

Grunt will provide a files object, which you can use to loop through the files, filtering out the directories and reading data out of the files. Once you’ve computed the word counts, you can print the result and fail if the threshold was exceeded:

if (options.threshold) {

if (total > options.threshold) {

grunt.log.error('Threshold of', options.threshold, 'exceeded. Found', total, 'words.');

grunt.fail.warn('Too many words');

} else {

grunt.log.ok(total, 'words found in total.');

}

} else {

grunt.log.writeln(total, 'words found in total.');

}

Last, all you have to do is configure a task target, the way you did before:

wordcount: {

capped: {

files: {

src: ['text/**/*.txt']

},

options: {

threshold: 3000

}

}

}

If the word count for all those files is more than 3,000, the task will fail. Note that if you hadn’t provided a threshold, it would use the default value of 0, which you specified in the task. This is enough information to understand Grunt, which we introduced in chapter 1. In chapter 2, you’ll get a deeper knowledge of build tasks themselves, how those should work, and how you can compose tasks to create a build workflow for development and another one for releases and deployments.