0.0.4 • Published 9 years ago

grunt-thrall v0.0.4

Weekly downloads
1
License
MIT
Repository
github
Last release
9 years ago

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