Small Grunts

August 06, 2013

At Square, we encourage people to not limit their ideas to things they already know how to do. There are plenty of opportunities for people to expand their skills, like programming classes for engineers and non-engineers. Here's a post I shared on my personal blog for people who want to learn the basics of Grunt.

Let's say you're a front-end dev who wants to leverage the power of Grunt. You're making a Wordpress theme, and you want to concatenate and minify your JavaScript files so your site will load faster. We'll assume that the site is using jQuery, a few jQuery plugins that rely on jQuery, and a site-wide script that relies on both the plugins and jQuery.

How difficult is that for someone who's proficient in jQuery, maybe has even written a couple plugins, but has never used JS outside the browser?

Gotta Walk Before You Crawl

When you go to the Getting Started page on the Grunt site, the first thing you are told to do is run:

npm install -g grunt-cli

This is a sign of things to come. The main thing I learned while writing this article is that in order to be comfortable with Grunt, you need to be comfortable with node.js---which means also being comfortable with:

  • node.js's package manager, npm
  • the package.json file that npm uses
  • node.js's module.exports syntax

It's not mind-bendingly difficult stuff once you get your feet wet, but still new knowledge you're going to have to take on before you can be slinging Grunt tasks like a pro.

The upside is once you know these things, you gain amazing Grunt powers on your development machine---and you get to do it all with that programming language you know and love: JavaScript. You might even become interested in using node.js on the server.

Best to start from the beginning.

npm

The node package manager lets you install node.js packages in a local directory so they can be found by node.js programs when they use the require() method.

"But I'm not using node.js," you say? Oh yes, you are. Grunt runs on node, and you'll be using npm to install any Grunt plugins you want to use. So it's time to learn a bit more about npm.

By including a package.json file in your project, you can specify exactly which modules (and which versions of those modules) to install via npm. This file has to be true JSON, and it must include the required keys of "name" and "version". You can run npm init and answer a bunch of questions to get a package.json with more than you really need in it. Or, you can just copy this JSON right here:

{
  "name": "my-project-name",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-jshint": "~0.6.0",
    "grunt-contrib-uglify": "~0.2.2",
    "grunt-contrib-concat": "~0.3.0",
    "grunt-contrib-qunit": "~0.2.2",
    "grunt-contrib-watch": "~0.5.1"
  }
}

Plop that in a package.json file in the root of your project, and then run npm install. Voila! You have installed Grunt in your project, along with some helpful plugins.

Isn't Grunt already installed?

Why is "grunt" in package.json if we already installed it with the npm command? What we installed before was grunt-cli, (some very simple code to make the grunt command available everywhere). The local Grunt code installed via package.json and npm install is what actually runs your project’s tasks, defined in your project's Gruntfile.

We include both the Gruntfile and package.json in our project to be sure we are using a version of Grunt that is compatible with our Gruntfile.

More about npm install

If npm install is called without any specific package name, it will go through your package.json file and install all of your dependencies---and unless you tell it otherwise, your devDependencies as well.

To install new Grunt plugins, you have two options. First, you can add a new line to your package.json file that mentions the new plugin and then run npm install. Or (and this way is my favorite), you can run:

npm install grunt-contrib-requirejs --save-dev

This command will download the specific node package, (in our case, it's grunt-contrib-requirejs, a node package which happens to be a Grunt plugin), and also adds that package's information to the devDependencies section of our package.json file.

If you used --save instead of --save-dev, the packages get added to the dependencies section instead of devDependencies. This would be important if you were making your own node package. For this example, it really doesn't make a difference, but devDependencies is more semantically correct.

Gruntfile.(js|coffee)

When you run a grunt command, Grunt looks in your current directory for a Gruntfile that defines tasks it knows how to run. A Gruntfile is a file named either Gruntfile.js (written in JavaScript) or Gruntfile.coffee (written in CoffeeScript).

If there isn't a Gruntfile in your current directory, Grunt will look in each directory above it. So, if you put your Gruntfile in the root of your project, you'll be able to use the tasks defined in that Gruntfile anywhere within your project.

If there isn't a Gruntfile anywhere above your current directory, you'll get an error: A valid Gruntfile could not be found.

Here's the very simplest Gruntfile:

module.exports = function(grunt) {
  // Why am I even here?
};

In fact, this Gruntfile is so simple it contains no tasks. Any attempt to use it will result in a warning: Warning: Task "foo" not found.

But we can still see the structure of a Gruntfile. A Gruntfile is actually a node module. Node modules signal what they want to export by assigning it to module.exports. In this case, we're exporting a function that receives a grunt object when it's called. This grunt object provides methods that allow you to configure Grunt. It also gives you access to things like grunt.log. You can find out everything you can do with the grunt object in the API docs.

Here's a Gruntfile with a task you can actually run. The task is still useless, but whatevs...

module.exports = function(grunt) {
  grunt.registerTask('pig', 'My "pig" task.', function() {
    grunt.log.writeln('Oink! Oink!');
  });
};

We call the registerTask method on the grunt object and pass a task name, description (option), and a function that defines the actual task.

Save that into your Gruntfile.js and then run grunt pig and you'll see your output. Tada!

Piggybacking on the Hocks of Giants

Now we know how to register our own tasks with Grunt. This is amazingly powerful -- not only do you have the entire Grunt API to work with, you also have all of node! The sky is the limit when writing your own tasks.

But why reinvent the wheel? There are existing Grunt plugins that handle most common tasks. One of those is the concat plugin, and that's what we'll use to solve our original problem of concatenating our JavaScript into a single file.

Here's the structure of our JavaScript files.

/js
├── plugins
│   ├── jquery.cycle.all.js
│   └── jquery.timeago.js
├── jquery-2.0.3.min.js
└── site.js

To use the concat plugin, the first thing we have to do is to make sure we actually have the code available. If you used the sample package.json file above, you already have it installed. If not, install it and add it to your package.json with:

npm install grunt-contrib-concat --save-dev

Now that the plugin is available, we can load that plugin by calling the loadNpmTasks method on the grunt object. Here's our Gruntfile now:

module.exports = function(grunt) {
  grunt.loadNpmTasks('grunt-contrib-concat');
};

If you run grunt -h, Grunt will show you some help text, along with a list of available tasks. You'll see that concat is now available. However, if you run grunt concat, you'll get an error: No "concat" targets found.

We need to add some configuration, and we do that via the grunt object's initConfig method. You pass it a hash, and if you want to configure a specific task you use the task's name as a key in that hash.

module.exports = function(grunt) {
  grunt.initConfig({
    concat: {
      // json here to configure the concat task
    }
  });
};

Figuring out how to format the parameters to concat is a little hit or miss. While it's not the easiest to read through, I recommend checking out the examples in the concat documentation---and thankfully, you have this tutorial!

Here we're creating a subkey called js, which is a target of the concat task. Some tasks (called "multi-tasks") allow you define a range of targets. Many plugins are multi-tasks, including concat. If you run grunt concat:js it'll concat using that target's config. If you just run grunt concat it will run all targets.

For our js target, we define a list of source files in the order we want to concat them, and a destination file:

module.exports = function(grunt) {
  grunt.initConfig({
    concat: {
      js: {
        src: ['js/jquery-2.0.3.min.js',
              'js/plugins/*.js',
              'js/site.js'],
        dest: 'build.js'
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-concat');
};

Try out grunt concat and you'll find that a build.js file has been created.

Note that we're able to use a wildcard *.js to grab all the JS files in the plugins directory. You can also use some/path/**/*.js to grab all the JS files in some/path and its subdirectories, no matter how deep!

Since a lot of Grunt tasks deal with sets of input files and sets of output files, Grunt has some standard ways of representing source-destination file mappings. If you find yourself using Grunt a lot, you'll want to read through the documentation at some point.

In our src definition above, we put the jQuery file first, then the plugins and our site file so that they concat in the right order. This assumes that none of the plugins depend on each other. If you have to, you can name some files explicitly. Grunt concat is smart enough to not duplicate files grabbed by wildcards. In the following example, jquery.timeago.js would only appear once in the built file.

src: ['js/jquery-2.0.3.min.js',
      'js/plugins/jquery.timeago.js',
      'js/plugins/*.js',
      'js/site.js'],

The concat plugin also has a few options you can set in order to add banners to the built file, strip banners from source files, set separator characters, and even process source files as if they're Lo-Dash templates. You can check those out at your leisure.

Making it Easy

We can now type grunt concat and build our files. This is great, but y'know what's even better? Less typing!

Let's set a default task in our Gruntfile by adding this line:

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

Now we can just run grunt and Grunt will run our concat task.

Getting Ugly

We've concatenated our JavaScript together, but let's go one step further and minify them using the uglify plugin.

First, we make sure we've installed the plugin locally:

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

Then we add the tasks to our Gruntfile:

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

Then we add the configuration for uglify, and also make sure we use a ; for a separator when we concat our JS files (just to be safe).

  grunt.initConfig({
    concat: {
      js: {
        src: ['js/jquery-2.0.3.min.js',
              'js/plugins/*.js',
              'js/site.js'],
        dest: 'build.js',
        options: {
          separator: ';',
        }
      }
    },
    uglify: {
      js: {
        src: 'build.js',
        dest: 'build.min.js'
      }
    }
  });

Now we can run grunt uglify:js or grunt uglify.

I've chosen the name "js" for the uglify target. This is purely coincidence. It doesn't have to be the same target name as the one we used for concat.

Lastly, let's update our default task:

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

We can now run grunt anywhere in our project, and Grunt will concatenate our JS files and minify them.

Even Less Typing!

We're now doing a lot of work by just typing a single five letter command, but what if we could type even less!? Using the watch plugin, we can run Grunt tasks automatically whenever your project's files change.

By now this should be old hat. Install the plugin:

npm install grunt-contrib-watch --save-dev

Add its tasks to the Gruntfile:

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

And add some config:

  grunt.initConfig({
    // ...
    watch: {
      js: {
        files: ['js/**/*.js'],
        tasks: ['default'],
      }
    }
  });

Now we can run the grunt watch task, and Grunt will sit there watching our files. If any of the matched files change, Grunt will run the associated task(s).

$ grunt watch
Running "watch" task
Waiting...OK
>> File "js/site.js" changed.

Running "concat:js" (concat) task
File "build.js" created.

Running "uglify:js" (uglify) task
File "build.min.js" created.

Done, without errors.
Completed in 2.313s at Sat Jul 27 2013 20:49:10 GMT-0700 (PDT) - Waiting...

Amazing!

The End Result

Here's our complete Gruntfile:

module.exports = function(grunt) {
  grunt.initConfig({
    concat: {
      js: {
        src: ['js/jquery-2.0.3.min.js',
              'js/plugins/*.js',
              'js/site.js'],
        dest: 'build.js',
        options: {
          separator: ';'
        }
      }
    },
    uglify: {
      js: {
        src: 'build.js',
        dest: 'build.min.js'
      }
    },
    watch: {
      js: {
        files: ['js/**/*.js'],
        tasks: ['default'],
      }
    }
  });

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

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

In Conclusion

Grunt was created specifically for JavaScript developers. If you're at the level where you're writing unit tests for your JavaScript, you'll definitely want to start working with Grunt on a daily basis. Combining the watch plugin with the jshint and qunit plugins gives you constant feedback about the quality of your code.

If you're not yet at that level with your JavaScript, Grunt may still be for you. It'll depend on whether you're willing to put in the time to learn how to set up a Gruntfile, or find a suitable template. Grunt gives you an incredibly powerful set of tools, but it's up to you to put them to use.

The ability to create templates with grunt-init is cool, too. I strongly recommend the grunt-init-jquery template if you're ever making a jQuery plugin. It includes some great code examples.

Contact me on Twitter or Github if you have questions!

Eric Strathmeyer
I fight for the users.

Comments

Get support help at squareup.com/support. We'll delete off-topic comments.