Readme.md
grunt-thrall
============
[![Build Status](https://travis-ci.org/Xiphe/Thrall.svg?branch=master)](https://travis-ci.org/Xiphe/Thrall)
[![Code Climate](https://codeclimate.com/github/Xiphe/Thrall/badges/gpa.svg)](https://codeclimate.com/github/Xiphe/Thrall)
[![Test Coverage](https://codeclimate.com/github/Xiphe/Thrall/badges/coverage.svg)](https://codeclimate.com/github/Xiphe/Thrall/coverage)
[![Dependency Status](https://david-dm.org/Xiphe/Thrall.svg)](https://david-dm.org/Xiphe/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](https://github.com/Jimdo/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`
```js
// 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:
- [Example project](https://github.com/Xiphe/Thrall/tree/master/sample_project)
- [Grunt Plugin](https://github.com/Jimdo/grunt-angular-toolbox)
API
---
### `thrall.init(config)`
- Load all grunt plugins, specified in the projects `package.json`
(heavily inspired by [load-grunt-tasks](https://github.com/sindresorhus/load-grunt-tasks))
- merge defaults, provided in `config` with settings is `grunt.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`
```js
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`
```js
thrall.init({basePath: __dirname /* ,... */ });
```
The projects base path.
Used to findup `node_modules/grunt-*/tasks/*` when auto-loading grunt plugins.
### Required _object_: `grunt`
```js
thrall.init({grunt: grunt /* ,... */ });
```
The currently running grunt instance.
### _string_: `name`
```js
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
```js
// pseudo-code
var config = _.merge(config.getDefaults(), grunt.config(config.name));
```
### _boolean_: `loadDevDependencies`
```js
thrall.init({loadDevDependencies: false /* ,... */ });
```
Default: `true`
Whether or not to include `devDependencies` from `package.json` when auto-loading grunt plugins.
### _boolean_: `loadDependencies`
```js
thrall.init({loadDependencies: true /* ,... */ });
```
Default: `false`
Whether or not to include `dependencies` from `package.json` when auto-loading grunt plugins.
### _object_: `module`
```js
thrall.init({
module: {
myHelper: ['factory', require('./helpers/myHelper')]
}
/* ,... */
});
```
Default: `{}`
Task definitions, grunt plugin configurations and getDefaults are being invoked using
[node-di](https://github.com/vojtajina/node-di) providing basic node modules.
When you need a custom helper, it can be registered here.
See [DI](#DI) for further informations.
### _function_: `getDefaults`
```js
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](#DI) for further informations.
### Naming
```js
// 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`
```js
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.
```js
// 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](#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.
```js
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 `coverage` task will only run when `grunt.config.get('coverage.enabled')` returns a truthy value.
- the `report` task will run when `grunt.config.get('foo.bar')` is truthy,
(and `null != 1` which is of cause true, too)
- when `grunt.config.get('foo.bar')` is falsy the `say:goodbye` task 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).
### _object_: `options`
Map CLI options, environment variables and grunt modifiers to grunt config.
```js
// 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.
```js
// 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=7000`
- `grunt foo:bar --demo=7000`
- `DEMO_PORT=7000 grunt foo:bar`
will set the `grunt.config('foo.demoPort')` to 7000.
```js
// 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.
```js
// 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](#DI) for further informations.
```js
// 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.
```js
grunt.initConfig({
jshint: {
src: {
options: {
ignores: ['**/*.coffee'],
jshintrc: true,
},
src: [
'<%= my.src.files.js %>'
]
}
}
});
```
DI
--
[`getDefaults`](#function-getdefaults), [Task Factories](#task-factories) and
[Configuration Factories](#task-factories) are being invoked with a
[node-di](https://github.com/vojtajina/node-di) module, providing the following
services:
- `_`: [lodash](https://github.com/lodash/lodash)
- `cliOptions`: CLI Options using [minimist](https://github.com/substack/minimist)
- `del`: [del](https://github.com/sindresorhus/del)
- `findupSync`: [node-findup-sync](https://github.com/cowboy/node-findup-sync)
- `fs`: node module
- `getobject`: [node-getobject](https://github.com/cowboy/node-getobject)
- `glob`: [node-glob](https://github.com/isaacs/node-glob)
- `grunt`: currently running grunt instance
- `mkdirp`: [node-mkdirp](https://github.com/substack/node-mkdirp)
- `path`: node module
- `merged`: (`getDefaults` ONLY) See [merged callback](#merged-callback)
- `name`: (Task Factories ONLY) name of the task
- `rootTask`: (Task Factories ONLY) the name of the task that has actually been called
#### merged callback
```js
/* ... */
getDefaults: function(merged) {
merged(function(mergedConfig) {
mergedConfig.foo = 'baz';
});
return {foo: 'bar'};
}
```
## [MIT License](https://raw.github.com/Xiphe/thrall/master/LICENSE)