Hi, I’m Amsul.

July 13, 2014

The Simplest Gruntfile

More often than not, I’ve come across Gruntfiles that are monstrous configuration blocks that are difficult to digest. This is most likely an artifact of how the official guides suggest constructing a Gruntfile, to make it easy for beginners to get started with Grunt.js.

However, there is a much cleaner and modular approach we can take by separating the tasks and configurations into their own individual modules. What that ends up as is a tiny Gruntfile whose sole purpose is to load tasks from a directory and initialize them with configurations from another.

Here’s the Gruntfile in it’s entirety:

/**
 * This Gruntfile is used to import configs and tasks
 * from the `node_configs` and `node_tasks` folders.
 */

'use strict';

module.exports = function(grunt) {
    initTasks(grunt, 'node_tasks')
    initConfigs(grunt, 'node_configs')
}

function initTasks(grunt, folderPath) {
    var pkg = grunt.file.readJSON('package.json')
    var tasks = pkg.devDependencies
    delete tasks.grunt
    for ( var task in tasks ) {
        grunt.loadNpmTasks(task)
    }
    grunt.loadTasks(folderPath)
}

function initConfigs(grunt, folderPath) {
    var config = {}
    grunt.file.expand(folderPath + '/**/*.js').forEach(function(filePath) {
        var fileName = filePath.split('/').pop().split('.')[0]
        var fileData = require('./' + filePath)
        config[fileName] = fileData
    })
    grunt.initConfig(config)
}

The breakdown

The first four lines are the gist of the work:

module.exports = function(grunt) {
    initTasks(grunt, 'node_tasks')
    initConfigs(grunt, 'node_configs')
}

We first initialize tasks from the node_tasks directory and then we initialize configurations for those tasks from the node_configs directory.

initTasks(grunt, folderPath)

Within the initTasks function, we first read the package.json file:

var pkg = grunt.file.readJSON('package.json')

…grab the devDependencies, which should only have Grunt tasks1:

var tasks = pkg.devDependencies
delete tasks.grunt

…and load the named NPM tasks and our own custom tasks and aliases:

for ( var task in tasks ) {
    grunt.loadNpmTasks(task)
}
grunt.loadTasks(folderPath)

initConfigs(grunt, folderPath)

Within the initConfigs function, we create an empty config object and glob through the node_configs directory:

var config = {}
grunt.file.expand(folderPath + '/**/*.js').forEach(...)

…grab each file’s name, which should be the name of the task, and add it to the config object:

var fileName = filePath.split('/').pop().split('.')[0]
var fileData = require('./' + filePath)
config[fileName] = fileData

…and finally, we initialize the configuration:

grunt.initConfig(config)

Setting up node_configs and node_tasks

These directories should look something like this:

node_configs/
    ├── autoprefixer.js
    ├── connect.js
    ├── handlebars.js
    ├── jshint.js
    ├── less.js
    ├── uglify.js
    └── watch.js
node_tasks/
    ├── aliases.js
    ├── custom-task.js
    └── another-custom-task.js

Each task gets it’s own configuration file in node_configs:

// node_configs/connect.js
'use strict';
var grunt = require('grunt')
module.exports = {
    options: {
        port: grunt.option('port') || 9001
    },
    server: {
        // ...
    }
}

Each custom task gets it’s own registration file in node_tasks:

// node_tasks/custom-task.js
'use strict';
module.exports = function(grunt) {
    grunt.registerMultiTask('custom-task', 'Make Grunt do something custom', function() {
        // ...
    })
}

And all the task aliases, such as the default task, live in node_tasks/aliases.js:

'use strict';
module.exports = function(grunt) {
    grunt.registerTask('default', ['less', 'autoprefixer'])
    grunt.registerTask('server', ['connect', 'watch'])
}

Brilliant bowtie

I’ve found this to be much more flexible and extremely simple to find your way around all the tasks and configurations for complex build steps.