Clean Up Your Gruntfile

I really don't like looking at a gruntfile with 20 config entries and everything all defined in a gigantic config object. Today, we'll look at a couple techniques for cleaning up the Gruntfile.


Clean up Task Configs

At the root of your project, create a directory to hold all our grunt-specific stuff. This will hold configs for tasks we pull in from npm, as well as custom tasks we write. I'll call mine grunt-tasks. Within that directory, let's create one more named configs.

Now we need to start getting all of our configuration data out of our gruntfile. First, create a javascript file in our grunt-tasks/configs directory for each config entry in your grunt config, this is the object passed into grunt.initConfig({... cfg object ...}) in the Gruntfile. Make sure to name the file exactly the same as the config section. Fo instance, if my first config entry looked like this, I'd create a file in grunt-tasks/configs/ named copy.js:

grunt.initConfig({
	copy: {
    	main: {
        	files: [
            	{src: ['file1.js', 'file2.js'], dest: 'dist/'}
            ]
        }
    }
});

Since we're pulling these configs out into separate files, we'll need to "require" them from within our gruntfile. So, let's make sure to write our copy.js file appropriately. We simply want to make it so that module.exports is set to the object that copy is equal to in our Gruntfile snippet above.

//copy.js - config info for grunt-copy task
module.exports = {
	main: {
    	files: [
        	{src: ['file1.js', 'file2.js'], dest: 'dist/'}
        ]
    }
};

Perfect, now we just need to do this for all other config entries in our Gruntfile.


Updating the Gruntfile

Now that we've pulled the configurations out into their own files, we can remove them all from the grunt.initConfig() call. We do, however, need to generate the config object inside the Gruntfile by "requiring" each of those files. Fortunately, Grunt has some built in file system utils.

//Gruntfile.js
module.exports = function(grunt) {
	//create initial grunt config and put in any base info needed, i.e. environment
	var gruntCfg = {
    	env: process.env
    }; 
    
    //function to build up a config object from a given directory
    function buildGruntConfig(path) {
    	//config object to modify
        var config = {};
        //will hold the current property we're populating in the config
        var key;
        
        //use grunt's file helpers to get all the files in this directory
        grunt.file.recurse(path, function loadConfigFromFile(absPath, rootDir, subDir, fileName) {
        	//populate the key, i.e. "copy.js" => "copy" as key
            key = fileName.replace(/\.js$/, '');
            //get the value using require
            var configVal = require(path+fileName);
            //add the value to the object, with the correct property name
            config[key] = configVal;
        });
        
        return config;
    }
    
    //extend gruntCfg with config built dynamically
    grunt.util._.extend(gruntCfg, buildGruntConfig('./grunt-tasks/configs/');
    
    //init grunt with our config
    grunt.initConfig(gruntCfg);
};

Then, any further configs you add to the directory will be picked up and put into the grunt config. Easy enough, but what about more complex task configs?


A more complex config

Okay, so this config example is a bit overly simplistic. SomeMany times, you need a more robust config for a task. One that can have access to grunt, some utility function(s), etc... Let's look at one method of getting those items injected into our config definitions, and still keeping our files clean and easily maintained.


Modify copy.js

First step is to modify our copy.js file to export a function that takes in parameters. We could just name the parameters, function(grunt, util, helper, ...), but that would be very difficult to deal with when trying to dynamically pull these configs into our grunt config. So, I've opted for it to just take in 1 parameter: deps (for dependencies).

//copy.js
module.exports = function(deps) {
	//here's where we can check that our deps has what we need.  This one just needs access to the grunt object
	var grunt = deps.grunt;
	if(!grunt) {
		//throw exception, write to log, etc...
	}

	//returning the same config as before, with the addition of a "process" function called on every file that writes out to the grunt log
	return {
		main: {
			files: [
				{src: ['file1.js', 'file2.js'], dest: 'dist/'}
			],
            //NEW STUFF
            options: {
            	process: function(content, srcPath) {
                	//we can manipulate the file here, or just write updates to the grunt log...
                    grunt.log.write("Processing file " + srcPath);
                    
                    return content;                    
                }
            }
		}
	};
};

Modify Gruntfile.js

Now, we just need to update a couple items in the gruntfile. First, we need to create an object that holds all dependencies for our configs. After that, we just need to check to see if the file content returns a function and invoke it with dependencies object if so, otherwise use the object returned.

//Gruntfile.js

//updates to buildGruntConfig function
function buildGrundConfig(path) {
	//ADDITION: Create config dependencies objecet
    var configDeps = {
    	grunt: grunt,
        myFsUtils: require('/utils/fs-utils.js'),
        //etc...
    };
    
    //....... inside grunt.file.recurse
    var configVal = require(path + fileName);
    //ADDITION: Check to see if configVal is a function
    if(typeof configVal === 'function') {
    	//invoke the function with the dependencies
        configVal = configVal(configDeps);
    }
    //..... everything else is same
}

And it's that easy. We now have our Gruntfile cleaned up and loading its own configs dynamically from the path specified. Configs can be as simple or as complex as needed, and only affect their own files rather than continuously cluttering up the Gruntfile.