Xiphe/Thrall

View on GitHub
Readme.md

Summary

Maintainability
Test Coverage
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)