Dual grunt tasks

Grunt tasks that accept default options or can be configured.

grunt is a great tool, and I have written lots of plugins for it. One problem that bothered me was the separation between regular tasks and multi-purpose tasks.

A regular task requires no configuration, just needs to be listed by name in the pipeline. For example my grunt-deps-ok can be used very easily

1
2
grunt.loadNpmTasks('grunt-deps-ok');
grunt.registerTask('default', ['deps-ok', rest of the tasks...]);

The default pipeline itself was created using registerTask function!

Most grunt plugins on the other hand use configurable multi task method. A single task can have multiple options configured under different names, providing great flexibility. For example, you can use different jshint options for source and test files

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
grunt.initConfig({
jshint: {
options: {
// options to apply to every target
},
source: ['src/**/*.js'],
test_files: {
options: {
// extends the options above
curly: false,
undef: true,
},
files: {
src: ['src/**/*spec.js']
},
}
},
});
grunt.loadNpmTasks('grunt-contrib-jshint');

Using multi tasks allows to run each target separately: grunt jshint:source or grunt jshint:test_files.

The problem is when the majority of use cases can be covered by the default options, and only a small number of cases needs additional configuration. In this case, multi task adds boilerplate code. For example, my grunt-nice-package plugin caused a lot of projects to copy / paste empty boilerplate code

1
2
3
4
5
6
7
8
9
grunt.initConfig({
'nice-package': {
all: {
options: {}
}
}
});
grunt.loadNpmTasks('grunt-nice-package');
grunt.registerTask('default', ['nice-package', ...]);

Omitting nice-package property from the grunt config prevented the task from running. Finally I found the way to initial same task both as a multi task and as a regular task depending on the config object.

Place loadNpmTasks call AFTER grunt.initConfig call to allow plugin access to the config data. I usually use matchdep to load tasks:

1
2
3
grunt.initConfig({ ... });
var plugins = require('matchdep').filterDev(['grunt-*', '!grunt-cli']);
plugins.forEach(grunt.loadNpmTasks);

When a grunt task is registered it receives an instance of grunt object, which includes the configuration information. We can inspect the config to see if the user has specified options or not:

grunt-nice-package.js
1
2
3
4
5
6
7
8
9
10
11
module.exports = function (grunt) {
if (grunt.config.data['nice-package']) {
grunt.registerMultiTask('nice-package', 'package.json validator', function() {
// grab options using this.options() method provided by grunt
});
} else {
grunt.registerTask('nice-package', 'package.json validator', function () {
// use default options
}
}
}

You can see a working example in nice-package.js.

Naturally, both tasks reuse the same function internally to validate package.json. We get the best of two worls: simple to use default task and the ability to pass any additional options if needed.

Passing options

If same task has to work as multi task and default task, you need to make sure verbose and force command line arguments still work. In multi task, we use this.options call to combine command line and task config options.

1
2
3
4
5
6
7
8
9
if (grunt.config.data[taskName]) {
grunt.registerMultiTask(taskName, taskDescription, function configuredCheckDeps() {
var options = this.options({
verbose: false,
force: false
});
...
});
}

But default task does NOT have this.options method. Thus if you need command line arguments, you must parse them yourself

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function isVerbose(option) {
return option === '--verbose' || option === '-v';
}
function isForce(option) {
return option === '--force' || option === '-f';
}
if (!grunt.config.data[taskName]) {
grunt.registerTask(taskName, taskDescription, function defaultTask() {
grunt.verbose.writeln('deps-ok default task');
// get arguments from command line manually
var verbose = process.argv.some(isVerbose);
var force = process.argv.some(isForce);
var options = {
verbose: verbose,
force: force
// any other options
}
...
});
}