grunt-thrall

Grunt task orchestrator/warchief

Usage no npm install needed!

<script type="module">
  import gruntThrall from 'https://cdn.skypack.dev/grunt-thrall';
</script>

README

grunt-thrall

Build Status Code Climate Test Coverage Dependency Status

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 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

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 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

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=7000
  • grunt foo:bar --demo=7000
  • DEMO_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:

merged callback

/* ... */
getDefaults: function(merged) {
    merged(function(mergedConfig) {
        mergedConfig.foo = 'baz';
    });
    return {foo: 'bar'};
}

MIT License