GruntJS is a JavaScript Task runner which allows you to combine a series of tasks into workflows. This is commonly associated with the build process of a project, but in reality it can also be used for any supporting process:
- Initializing a project (after npm dependencies have been installed)
- Publishing a release to a production branch on Github
- Convert files that were formatted in one particular manner into another format.
- Increment the version number in package.json
- Clean out a build directory of cached files
At one point I found myself one of the top contributors to GruntJS questions on Stack Overflow. These days, I don't hear much chatter about Grunt by those who are coding on a daily basis, but I know it still does have an audience. Based on all my experiences with Grunt, here's what I loved about the tool:
What I Loved
Written in JavaScript
I'm a full-stack JavaScript engineer by trade, having my tooling using my strongest competency enabled me to become familiar with the Grunt API and extend it in ways that aren't always commonly known.
My Gruntfile
Unlike the majority of gruntfile examples you'll find on the internet, mine is modular, enabling the root file itself to be minimalist & simple to read. Plugins and Configurations are dynamically loaded, while tasks are defined towards the bottom of the file. I've toyed around with dynamically loading these, but opted for a manifest of available tasks being clearly defined in the gruntfile.
How this looks in a project:
grunt/configs/
- jslint.js
- less.js
- minify.js
grunt/tasks/
- import-batch.js
- build-native-project.js
- s3-import-avatars.js
gruntfile.js
And the gruntfile itself:
module.exports = function (grunt) {
function loadConfig(pattern) {
var config = {},
fileName,
fileData;
grunt.file.expand(pattern).forEach(function(filePath) {
fileName = filePath.split('/').pop().split('.')[0];
fileData = grunt.file.readJSON(filepath);
config[fileName] = fileData[fileName];
});
return config;
}
function init() {
var config = {
pkg: grunt.file.readJSON('package.json')
};
require('load-grunt-tasks')(grunt);
if (grunt.file.exists('grunt/tasks')) {
grunt.log.writeln('task directory found, loading tasks...');
grunt.loadTasks('grunt/tasks');
}
grunt.util._.extend(config, loadConfig('grunt/configs/**/*.js'));
grunt.initConfig(config);
}
init();
};
The plugins available
At one point there was a decently sized community around GruntJS and you could find almost any plugin you needed to automate a workflow. Whether's that linting, schema validation, minifying, compiling, or making API calls - there's was likely a plugin that provided a task.
Writing your own tasks
Can't find a plugin for a specific task? No problem! Grunt's documentation provides a foundation to use their framework to write your own tasks.
This came in handy for a variety of different processes over the years, and I even took it a step further by providing robust feedback through the Grunt CLI. Some examples of tasks I've written:
- A wrapper for npm dependencies that didn't have a grunt plugin
- Validator for an excel spreadsheet that required specific columns to be filled out
- A prompt which had you confirm settings before executing an expensive workflow.
- A final report which detailed any non-fatal feedback from all the prior steps combined.
Dynamic Configs & Workflows
As I found myself building out complex workflows, I started to encounter the question, "Can 1 step give me the configuration for a step later in the workflow?"
That answer is yes! I most often used this for queuing up a batch process, where an import task would parse the import file & validate the resources as it queued up each item in the batch for an individual process.
You can also use string templates in your static configuration document, and the config will reference another variable at the time the required configuration is used to run a task.