grunt-thrall v0.0.4
grunt-thrall
Grunt task orchestrator/warchief
"The beginning of wisdom is the statement 'I do not know.' The person who cannot make that statement is one who will never learn anything. And I have prided myself on my ability to learn." - Thrall in Cycle of Hatred, page 77
Why?
When a project uses lots of Grunt Tasks, the Gruntfile.js tends to get really messy and
hard to maintain.
With grunt-angular-toolbox, we tried to extract this complexity into a toolbox that handles all grunt related things for a main project.
This works fine, but the toolbox itself was still spaghetti-code-ish and hard to understand and maintain for most users.
Thrall contains all orchestration logic and is 100% test covered. This allows consuming packages to focus on task definition without having to worry to much about configuration loading and option handling.
Usage
Install the module:
npm install grunt-thrall --save-dev
// gruntfile.js
module.exports = function(grunt) {
var thrall = require('grunt-thrall');
thrall.init({
/* see config */
});
};This can be used for any project or grunt plugin. See:
API
thrall.init(config)
- Load all grunt plugins, specified in the projects
package.json(heavily inspired by load-grunt-tasks) - merge defaults, provided in
configwith settings isgrunt.config - Search for custom tasks, specified in the
tasks/directory - Dynamically load configuration for grunt plugins used by those tasks from
config/directory
Config
Required string: dir
thrall.init({dir: __dirname + 'myTasks' /* ,... */ });The basic directory for custom tasks and grunt plugin configuration.
Expects subdir tasks/ for custom tasks and config/ for grunt plugin configuration
to be present.
Required string: basePath
thrall.init({basePath: __dirname /* ,... */ });The projects base path.
Used to findup node_modules/grunt-*/tasks/* when auto-loading grunt plugins.
Required object: grunt
thrall.init({grunt: grunt /* ,... */ });The currently running grunt instance.
string: name
thrall.init({name: 'myProject' /* ,... */ });Defaults to config.pkg.name project name from package.json
This is also the key for custom configuration that is merged with the defaults
// pseudo-code
var config = _.merge(config.getDefaults(), grunt.config(config.name));boolean: loadDevDependencies
thrall.init({loadDevDependencies: false /* ,... */ });Default: true
Whether or not to include devDependencies from package.json when auto-loading grunt plugins.
boolean: loadDependencies
thrall.init({loadDependencies: true /* ,... */ });Default: false
Whether or not to include dependencies from package.json when auto-loading grunt plugins.
object: module
thrall.init({
module: {
myHelper: ['factory', require('./helpers/myHelper')]
}
/* ,... */
});Default: {}
Task definitions, grunt plugin configurations and getDefaults are being invoked using node-di providing basic node modules.
When you need a custom helper, it can be registered here.
See DI for further informations.
function: getDefaults
thrall.init({
getDefaults: function(/* di here */) {
return {
foo: 'bar'
}
}
/* ,... */
});Default configuration will be merged and be available as grunt.config(config.name).
Task Factories
every file in config.dir/tasks/ is expected to export a factory function, returning
a task definition object. The name will be generated by the path relative to the tasks dir.
Factories are being invoked using node-di, see DI for further informations.
Naming
// tasks/foo/bar.js
module.exports = function(/* di here */) {
return {};
};will register task foo:bar that, when can be called with grunt foo:bar and does nothing.
string/array: description
module.exports = function() {
return {
description: [
'this is the bar tasks',
'it will foo.'
]
/* ... */
};
};Task description, which is displayed by grunt --help.
Arrays will be .join('\n')-ed.
array: run
Subtasks to run by this task.
// tasks/foo/bar.js
module.exports = function() {
return {
/* ... */
run: [
'jshint:src',
'mochaTest'
]
};
};Will load for grunt plugin configurations from
config/jshint/src.js and config/mochaTest.js (see
Configuration Factories)
and execute both subtasks when grunt foo:bar is called.
runIf blocks
A runIf block can add tasks to the cue based on grunt configuration.
module.exports = function() {
return {
/* ... */
run: [
'other:task',
{
if: 'coverage.enabled',
task: ['coverage']
},
{
if: [
(null != 1),
'foo.bar'
],
task: 'report',
else: 'say:goodbye'
}
]
};
};In the above example:
- the
coveragetask will only run whengrunt.config.get('coverage.enabled')returns a truthy value. - the
reporttask will run whengrunt.config.get('foo.bar')is truthy, (andnull != 1which is of cause true, too) - when
grunt.config.get('foo.bar')is falsy thesay:goodbyetask runs instead
All expressions and config values in an if-array need to be true in order to run the task.
There is no OR operator or !-negation.
This works well with options.
object: options
Map CLI options, environment variables and grunt modifiers to grunt config.
// tasks/foo/bar.js
module.exports = function() {
return {
/* ... */
options: {
coverage: 'coverage.enabled'
}
};
};grunt foo:bar --coverage will set the grunt.config('coverage.enabled') to true.
// tasks/foo/bar.js
module.exports = function() {
return {
/* ... */
options: {
'demo-port': {
env: 'DEMO_PORT',
alias: 'port',
key: 'foo.demoPort'
}
}
};
};either of
grunt foo:bar --demo-port=7000grunt foo:bar --demo=7000DEMO_PORT=7000 grunt foo:bar
will set the grunt.config('foo.demoPort') to 7000.
// tasks/foo/bar.js
module.exports = function() {
return {
/* ... */
options: {
coverage: {
grunt: ':coverage',
key: 'coverage.enabled'
}
}
};
};grunt foo:bar:coverage will set grunt.config('coverage.enabled') to true.
function: runFilter
Filter that may manipulate the tasks cue before execution.
// tasks/foo/bar.js
module.exports = function() {
return {
/* ... */
run: ['foo', 'bar'],
runFilter: function(tasks, args) {
if (args[0] === 'baz') {
tasks.shift();
}
return tasks;
}
};
};In this case, when grunt foo:bar:baz is called, only the foo subtask will run.
Configuration Factories
every file in config.dir/config/ is expected to export a factory function, returning
a configuration object. The name has to match the path that this configuration will
be placed at, in the grunt config.
Factories are being invoked using node-di, see DI for further informations.
// config/jshint/src.js
module.exports = function(/* di here */) {
return {
options: {
ignores: ['**/*.coffee'],
jshintrc: true,
},
src: [
'<%= my.src.files.js %>'
]
};
};This is similar to the following standard configuration, only that it's split in to a lot of small files, with is more easy to maintain for big projects.
grunt.initConfig({
jshint: {
src: {
options: {
ignores: ['**/*.coffee'],
jshintrc: true,
},
src: [
'<%= my.src.files.js %>'
]
}
}
});DI
getDefaults, Task Factories and
Configuration Factories are being invoked with a
node-di module, providing the following
services:
_: lodashcliOptions: CLI Options using minimistdel: delfindupSync: node-findup-syncfs: node modulegetobject: node-getobjectglob: node-globgrunt: currently running grunt instancemkdirp: node-mkdirppath: node modulemerged: (getDefaultsONLY) See merged callbackname: (Task Factories ONLY) name of the taskrootTask: (Task Factories ONLY) the name of the task that has actually been called
merged callback
/* ... */
getDefaults: function(merged) {
merged(function(mergedConfig) {
mergedConfig.foo = 'baz';
});
return {foo: 'bar'};
}