Blog

Grunt.js: Tips & Tricks

I want to share with you intermediate Grunt.js tips and tricks for bending Grunt.js to your will, and give you an idea of how you might use them. If you don’t know what Grunt is, or want a tour of the basics check our my earlier post.

Templating

DRY (Don’t Repeat Yourself) can apply to your build tools too. One tool grunt gives us for reducing duplication is templating. When we have strings in our config objects grunt will process them for template commands. Let’s take a look at how we can use them to reduce duplication or improve automation.

Simple Repetition

A common source of duplication are paths to files and folders used during the build. If we can factor these out and reduce duplication we also make them easier to change. How can we do that? Templates!

grunt.initConfig({
    dirs: {
        output: 'build'
    },

    copy: {
        vendor: {
            src: 'vendor/**',
            dest: '<%= dirs.output %>/assets/'
        }
    }
});

Templates have access to the config object, so here we’ve added a dirs config object with an output key. We use this value as part of the destination of our copy task (the <%= %> tags represent a value). When the copy tasks get’s the value of dest the value of dirs.output will be substituted in and it will get build/assets/.

External Data

Sometimes the data we want to use is already stored somewhere else, for example in a npm module the package.json file will already contain the name of the project and the current version number. Grunt gives us a clever way to use those values instead of duplicating them in our Gruntfile.js:

grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),

    copy: {
        dist: {
            src: 'build/<%= pkg.name %>.js',
            dest: 'dist/<%= pkg.name %>-<%= pkg.version %>.js'
        }
    }
});

The cool thing here is the line: pkg: grunt.file.readJSON('package.json'). We read and parse our package.json and store it as part of our config under the pkg key. This way we have acccess to that data from within our templates. Then we define a task which looks for a javascript file with the same name as our package under the build directory and copies it to the dist directory, naming the result with both the package name and version. Now we don’t have to update our Gruntfile when we release a new version, it just gets the version from package.json!

Dynamic Data

Maybe our project is an app and doesn’t have version numbers, but we want to track the source over time. How about we put today’s date in the filename. Once again, templates come to the rescue!

grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),

    copy: {
        dist: {
            src: 'build/awesome_code.js',
            dest: 'dist/awesome_code-<%= grunt.template.today("yyyy-mm-dd") %>.js'
        }
    }
});

As you can see templates don’t have to used fixed values, they can have any javascript code you want in them! Now when we run grunt copy:dist our awesome_code.js file will be copied to the dist directory with today’s data in the filename.

It’s all javascript

Templates are great but sometimes we need more complicated repetitive or dynamic behavior. The declarative nature of grunt.js can make it easy to forget that we’re writing javascript not json, but there’s no reason we can’t write some javascript to create more complicated config objects.

CAUTION: I think there are many cool uses for dynamically generating config in your Gruntfile, but like most powerful tools it comes at a cost (often complexity and confusion). Always consider the tradeoffs, does it remove noise, simplify maintenance and make the build easier to understand, or does it add complexity and make the process impossible to follow?

Breaking down the config

Up until now I’ve done my configuration using the grunt.initConfig method. This reset’s the grunt config to whatever we pass in. This is great, but as I’ve experimented with more complicated Gruntfiles I find it starts to make it harder to keep things organized. Grunt also has a method grunt.config which can be used to get and set values in the config object. We can use this to split up our config.

module.exports = function(grunt) {
    grunt.initConfig({});

    grunt.loadNpmTasks('jshint');
    grunt.config('jshint', {
        sources: 'sources/**.js',
        tests: 'tests/**.js'
    });

    grunt.loadNpmTasks('less');
    grunt.config('less', {
        styles: {
            src: 'styles/index.less',
            dest: 'build/styles.css'
        }
    });

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

By splitting each task out, we’ve added a small amount of repetition, but code for a given set of tasks is more localized.

Dynamic Tasks

Say you’re building an application with i18n support. You’re using require.js and need to pass in an option to the build telling it which locale to build. We could write a require task for each locale… or we can generate them with javascript!

module.exports = function(grunt) {
    grunt.initConfig({});

    grunt.loadNpmTasks('grunt-contrib-requirejs');
    var locales = ['en-us', 'fr-fr'];
    for(var i = 0; i < locales.length; i++) {
        var locale = locales[i];

        grunt.config(['requirejs', locale], {
            options: {
                mainConfigFile: 'require-config.js',
                out: locale === 'en-us' ? 'build/main.js' : 'build/main.' + locale + '.js',
                i18n: {
                    locale: locale
                }
            }
        });
    }
};

This code loops over an array of locales, and adds a require.js tasks to our config for each one. We add some logic to customize the output name based on the locale and pass the locale in as a config option to our task. Now when we run grunt requirejs it will build multiple files, one for each of our locales.

I want to point out that for many cases this is overkill and there may be better ways to solve the problem, so don’t go crazy, but it can be a very powerful tool to reduce repetition.

Options

Any large project will want to minify their javascript files for production, but minified javascript is painful for development and debugging. One way to handle these differences between development and production is to define multiple tasks, one set configured for production, one for development. It works, but it’s easy to end up with lots of duplication in your tasks. Continuing the theme of DRY, let’s look at another option for handling development/production differences.

Option Flags

Grunt parses options from the command line and gives us a way to access them. We can use this to modify our configuration based on the options used when we run grunt. Let’s take a look at how we can use this to adapt to development and production.

module.exports = function(grunt) {

    grunt.config('env', grunt.option('env') || process.env.GRUNT_ENV || 'development');
    grunt.config('compress', grunt.config('env') === 'production');

    grunt.loadNpmTasks('less');
    grunt.config('less', {
        styles: {
            src: 'styles/index.less',
            dest: 'build/styles.css',
            options: {
                compress: grunt.config('compress')
            }
        }
    });

    grunt.loadNpmTasks('uglify');
    grunt.config('uglify', {
        sources: {
            src: 'scripts/index.js',
            dest: 'build/index.min.js'
        }
    });

    var defaultTasks = ['less'];
    if(grunt.config('compress')) defaultTasks.push('uglify');
    grunt.registerTask('default', defaultTasks);

};

First we determine the environment we’re building for, we first check for an --env option supplied by the user using the grunt.option command (allowing the user to run grunt --env=production when they want to build for production). If no such option is supplied we check for the environment variable GRUNT_ENV handy if you say have a bug that only shows up in production so we want to work only in production for a while. Finally we default to the value development.

We then derive a setting compress that tells us if we want to do minification and other compression tasks. In our case we want to compress only in the production env… but you could use more complicated logic if needed.

In the case of less (our css pre-processor) we can set the compress option directly from the config value we created earlier. This way less will generate minimal css files only when the environment is set to production.

The last thing we do is dynamically setup our default task. It always runs the less:* tasks, but we add uglify to the list of default tasks if we are using compression.

Templating Files

I want to quickly point out a very cool feature of grunt-contrib-copy that I find comes in handy. When building a javascript app it’s pretty common to want to make some small tweaks to your index.html file during the build. There are quite a few heavyweight grunt plugins or tools with lots of cool features, but sometimes you don’t need all that. The grunt-contrib-copy tasks gives you the ability to add a hook to modify the contents of a file however you see fit before it’s written back to disk.

module.exports = function(grunt) {
    grunt.initConfig({});

    grunt.config('options.api', grunt.option('api-server') || 'http://localhost:3000/');

    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.config('copy.index', {
        src: 'index.html.ejs',
        dest: 'index.html',
        options: {
            process: function(content, path) {
                return grunt.template.process(content);
            }
        }
    });
}

Combine that with an index.html.ejs file looking something like:

<!doctype html>
<html>
    <head>
        <title>My Cool Project</title>
        <script>
            window.config = <%= JSON.stringify({
                api: options.api
            }) %>;
        </script>
        <script src="app.js"></script>
    </head>
    <body></body>
</html>

The key is the process option that the copy task takes, we give it a function that takes the content and path of the file. Whatever our function returns will be written to the destination in place of the original content. We combine that with grunt’s utility function for processing templates, and get an easy way to template our index.html with access to our project config. Now when you run grunt copy:index it will process the content for templates using the same templates grunt supports for config values. In this example we combine that with JSON.stringify to safely serialize a config object that we put right into our built index.html.

Conclusion

Hopefully this gives you some ideas on to help when you have a complex build task to automate. Keep an eye out for an article on writing custom tasks in grunt.