Everyday Grunt - Custom Tasks For The Common Code


I like things that make my life easier. I'm lazyefficient, so it bothers me to do the same task repetitively when it's something I can automate.

Grunt is a useful tool for those kinds of tasks. You can create custom tasks that handle things for you that you could do over and over... but c'mon, do you really want to?

Here are the specific examples we'll be covering:

  • Using HTML5 Application Cache Manifest and want the cache manifest file to change on every build so that I always get the latest files loaded in the browser. This includes live reload of the app from the Grunt Watch task.
  • Adjusting the URL paths to font files from a vendor css file after doing a prod build (i.e. FontAwesome Fonts or Bootstrap Glyphicons)
    • You'd want to do this if you are changing the structure in your build when you copy vendor files over. Maybe you have a few different themes (i.e. made with BootSwatchr), each with their own bootstrap.css file. The CSS file at styles/themes/theme1/bootstrap.css may use fonts at styles/fonts/glyphicons-halflings.... So, we'll want to update the bootstrap.css files for each theme to correctly path to the fonts: url('../../fonts/....') instead of url('../fonts/...'). This way we only have our fonts in 1 location, instead of having them in with each theme.

Application Cache Manifest

When you're using Application Cache to cache all your app files on the browser, the only way the browser knows to pull down new files is to see if the cache manifest file changed. When you're running the application locally, this can be annoying to have to change it each time you make a small change in order to see it take effect. Let's create a Grunt task to do this for us!

Create A New Grunt Task

In a prior post, I wrote about organizing your Grunt task config files. In that post, our structure was like this: /grunt-tasks/configs for the config objects for each task. We can now use this same main directory to put our custom tasks! So, create a new javascript file in the grunt-tasks directory and name it accordingly. For example: /grunt-tasks/appCache.js. Note that we have to tell Grunt to load the custom tasks from here by putting this in our gruntfile: grunt.loadTasks('grunt-tasks');.

The goal of this file is simply to modify our cache manifest file so that the browser sees it was changed and pulls down all files anew. This can simply be done in a comment (a line starting with a hash) that we always update to the current timestamp. Here's what that looks like:

//appCache.js
module.exports = function(grunt) {
	//register the task.  Params are: Name, [Description], Function to run
    grunt.registerTask("appCache", 
    	"Updates a timestamp in cache.manifest to force browser to pull all files anew", 
        function () {
        //tell grunt to not continue on to other tasks until this one is done
        var done = this.async();
        var now = (new Date()).toUTCString();
        grunt.log.writeln("Updating AppCache Timestamp to NOW: ", now);
        //Use Grunt's file utils to read in the file
        var appCache = grunt.file.read('cache.manifest');
        //look for the comment line that holds our timestamp
        var timeStampIdx = appCache.indexOf('#TIMESTAMP:');
        var updatedFile;
        if(timeStampIdx === -1) {
        	//comment doesn't exist, so add it to the bottom
        	updatedFile = appCache + '\n#TIMESTAMP:' + now;
        } else {
        	//comment exists, so update it with the current timestamp
        	updatedFile = appCache.substring(0, timeStampIdx + '#TIMESTAMP:'.length) + now;
        }
        //use Grunt's file util to write the file
        grunt.file.write('cache.manifest', updatedFile);
        //tell Grunt we're done and it can continue
        //mostly for when using grunt watch so the app doesn't refresh before this is done updating the file.
        done();
    });
};
Use The New Task

Now that we have our task defined, let's use it! First, let's add this to our build process. Find the config file for your build task, and then add this task into the list: grunt.registerTask('build', ['task1', 'task2', ... 'appCache']); Now, any time we type grunt build in the terminal, our cache manifest file will be updated with the latest timestamp.

What about when the app reloads due to the grunt watch task? I'm glad you asked - it's actually really simple thanks to being able to add an event handler on the "watch" event! I typically just put my "watch" event handler at the bottom of my gruntfile:

//gruntfile.js
...
	grunt.event.on('watch', function(action, filePath) {
    	//react to the watch event with running additional tasks, etc...
        //tell grunt to run the appCache task
        grunt.task.run('appCache');
    });
...

Themed Bootstrap CSS Files

Our next example is just to make all of our bootstrap.css files within our various style theme directories to all use a single set of glyphicons.

Create A New Custom Grunt Task

Back in our grunt-tasks directory, let's create a new file: grunt-tasks/bootstrapFonts.js.

This file will be less code, but is a bit more interesting than the last one. We basically want to go through all of our folders in styles/themes/ directory and update the font url path for any files whose name contains bootstrap and .css. The assumption here is that files made with a themeroller for Bootstrap will need to be named such that "bootstrap" is in the name (i.e. bootstrap-steelBlue.css), this way they are easy to identify from custom stylesheets in the same directory.

//bootstrapFonts.js
module.exports = function(grunt) {
	//We're structuring out the bootstrap fonts away from each theme folder, so there's only one copy of the fonts.
	//So, we need to update the boostrap url references pointing to those fonts to go to the appropriate place rather than the bootstrap default.
	grunt.registerTask('bootstrapFonts', 
    	'Helper to update the font-face urls in bootstrap files for our themes', 
        function bootstrapFontsFn() {
        
		var done = this.async();
		//use Grunt's file util to recurse through all items in our dist/styles/themes directory 
        //where our compiled css files are concatenated, minified, and copied to during build
		grunt.file.recurse('dist/styles/themes/', function updateBootstrapFontUrl(absPath, rootDir, subDir, fileName){
        	//check to make sure the file meets our requirements
			if(fileName.indexOf("bootstrap") !== -1 && fileName.indexOf(".css") !== -1) {
            	//read the contents into a string
				var file = grunt.file.read(absPath);
                //update the url accordingly
				file = file.replace("url('../fonts", "url('../../fonts");
                //use Grunt's file util to write the file with the updated content
				grunt.file.write(absPath, file);
			}
		});

		done();
	});
});

And we can now use this task just as we did with our appCache task. Add 'bootstrapFonts' as a task to your build task so that each time you do a new build, which presumably compiles your CSS files, you can update the outputs of your build so the paths are correct!


Have your own examples of Everyday Grunt Tasks or ideas on how these could be better? Please feel free to comment or hit me up on twitter!