123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- (function(exports) {
- 'use strict';
- var grunt = require('../grunt');
- // Construct-o-rama.
- function Task() {
- // Information about the currently-running task.
- this.current = {};
- // Tasks.
- this._tasks = {};
- // Task queue.
- this._queue = [];
- // Queue placeholder (for dealing with nested tasks).
- this._placeholder = {placeholder: true};
- // Queue marker (for clearing the queue programmatically).
- this._marker = {marker: true};
- // Options.
- this._options = {};
- // Is the queue running?
- this._running = false;
- // Success status of completed tasks.
- this._success = {};
- }
- // Expose the constructor function.
- exports.Task = Task;
- // Create a new Task instance.
- exports.create = function() {
- return new Task();
- };
- // If the task runner is running or an error handler is not defined, throw
- // an exception. Otherwise, call the error handler directly.
- Task.prototype._throwIfRunning = function(obj) {
- if (this._running || !this._options.error) {
- // Throw an exception that the task runner will catch.
- throw obj;
- } else {
- // Not inside the task runner. Call the error handler and abort.
- this._options.error.call({name: null}, obj);
- }
- };
- // Register a new task.
- Task.prototype.registerTask = function(name, info, fn) {
- // If optional "info" string is omitted, shuffle arguments a bit.
- if (fn == null) {
- fn = info;
- info = null;
- }
- // String or array of strings was passed instead of fn.
- var tasks;
- if (typeof fn !== 'function') {
- // Array of task names.
- tasks = this.parseArgs([fn]);
- // This task function just runs the specified tasks.
- fn = this.run.bind(this, fn);
- fn.alias = true;
- // Generate an info string if one wasn't explicitly passed.
- if (!info) {
- info = 'Alias for "' + tasks.join('", "') + '" task' +
- (tasks.length === 1 ? '' : 's') + '.';
- }
- } else if (!info) {
- info = 'Custom task.';
- }
- // Add task into cache.
- this._tasks[name] = {name: name, info: info, fn: fn};
- // Make chainable!
- return this;
- };
- // Is the specified task an alias?
- Task.prototype.isTaskAlias = function(name) {
- return !!this._tasks[name].fn.alias;
- };
- // Has the specified task been registered?
- Task.prototype.exists = function(name) {
- return name in this._tasks;
- };
- // Rename a task. This might be useful if you want to override the default
- // behavior of a task, while retaining the old name. This is a billion times
- // easier to implement than some kind of in-task "super" functionality.
- Task.prototype.renameTask = function(oldname, newname) {
- if (!this._tasks[oldname]) {
- throw new Error('Cannot rename missing "' + oldname + '" task.');
- }
- // Rename task.
- this._tasks[newname] = this._tasks[oldname];
- // Update name property of task.
- this._tasks[newname].name = newname;
- // Remove old name.
- delete this._tasks[oldname];
- // Make chainable!
- return this;
- };
- // Argument parsing helper. Supports these signatures:
- // fn('foo') // ['foo']
- // fn('foo', 'bar', 'baz') // ['foo', 'bar', 'baz']
- // fn(['foo', 'bar', 'baz']) // ['foo', 'bar', 'baz']
- Task.prototype.parseArgs = function(args) {
- // Return the first argument if it's an array, otherwise return an array
- // of all arguments.
- return Array.isArray(args[0]) ? args[0] : [].slice.call(args);
- };
- // Split a colon-delimited string into an array, unescaping (but not
- // splitting on) any \: escaped colons.
- Task.prototype.splitArgs = function(str) {
- if (!str) { return []; }
- // Store placeholder for \\ followed by \:
- str = str.replace(/\\\\/g, '\uFFFF').replace(/\\:/g, '\uFFFE');
- // Split on :
- return str.split(':').map(function(s) {
- // Restore place-held : followed by \\
- return s.replace(/\uFFFE/g, ':').replace(/\uFFFF/g, '\\');
- });
- };
- // Given a task name, determine which actual task will be called, and what
- // arguments will be passed into the task callback. "foo" -> task "foo", no
- // args. "foo:bar:baz" -> task "foo:bar:baz" with no args (if "foo:bar:baz"
- // task exists), otherwise task "foo:bar" with arg "baz" (if "foo:bar" task
- // exists), otherwise task "foo" with args "bar" and "baz".
- Task.prototype._taskPlusArgs = function(name) {
- // Get task name / argument parts.
- var parts = this.splitArgs(name);
- // Start from the end, not the beginning!
- var i = parts.length;
- var task;
- do {
- // Get a task.
- task = this._tasks[parts.slice(0, i).join(':')];
- // If the task doesn't exist, decrement `i`, and if `i` is greater than
- // 0, repeat.
- } while (!task && --i > 0);
- // Just the args.
- var args = parts.slice(i);
- // Maybe you want to use them as flags instead of as positional args?
- var flags = {};
- args.forEach(function(arg) { flags[arg] = true; });
- // The task to run and the args to run it with.
- return {task: task, nameArgs: name, args: args, flags: flags};
- };
- // Append things to queue in the correct spot.
- Task.prototype._push = function(things) {
- // Get current placeholder index.
- var index = this._queue.indexOf(this._placeholder);
- if (index === -1) {
- // No placeholder, add task+args objects to end of queue.
- this._queue = this._queue.concat(things);
- } else {
- // Placeholder exists, add task+args objects just before placeholder.
- [].splice.apply(this._queue, [index, 0].concat(things));
- }
- };
- // Enqueue a task.
- Task.prototype.run = function() {
- // Parse arguments into an array, returning an array of task+args objects.
- var things = this.parseArgs(arguments).map(this._taskPlusArgs, this);
- // Throw an exception if any tasks weren't found.
- var fails = things.filter(function(thing) { return !thing.task; });
- if (fails.length > 0) {
- this._throwIfRunning(new Error('Task "' + fails[0].nameArgs + '" not found.'));
- return this;
- }
- // Append things to queue in the correct spot.
- this._push(things);
- // Make chainable!
- return this;
- };
- // Add a marker to the queue to facilitate clearing it programmatically.
- Task.prototype.mark = function() {
- this._push(this._marker);
- // Make chainable!
- return this;
- };
- // Run a task function, handling this.async / return value.
- Task.prototype.runTaskFn = function(context, fn, done, asyncDone) {
- // Async flag.
- var async = false;
- // Update the internal status object and run the next task.
- var complete = function(success) {
- var err = null;
- if (success === false) {
- // Since false was passed, the task failed generically.
- err = new Error('Task "' + context.nameArgs + '" failed.');
- } else if (success instanceof Error || {}.toString.call(success) === '[object Error]') {
- // An error object was passed, so the task failed specifically.
- err = success;
- success = false;
- } else {
- // The task succeeded.
- success = true;
- }
- // The task has ended, reset the current task object.
- this.current = {};
- // A task has "failed" only if it returns false (async) or if the
- // function returned by .async is passed false.
- this._success[context.nameArgs] = success;
- // If task failed, call error handler.
- if (!success && this._options.error) {
- this._options.error.call({name: context.name, nameArgs: context.nameArgs}, err);
- }
- // only call done async if explicitly requested to
- // see: https://github.com/gruntjs/grunt/pull/1026
- if (asyncDone) {
- process.nextTick(function() {
- done(err, success);
- });
- } else {
- done(err, success);
- }
- }.bind(this);
- // When called, sets the async flag and returns a function that can
- // be used to continue processing the queue.
- context.async = function() {
- async = true;
- // The returned function should execute asynchronously in case
- // someone tries to do this.async()(); inside a task (WTF).
- return grunt.util._.once(function(success) {
- setTimeout(function() { complete(success); }, 1);
- });
- };
- // Expose some information about the currently-running task.
- this.current = context;
- try {
- // Get the current task and run it, setting `this` inside the task
- // function to be something useful.
- var success = fn.call(context);
- // If the async flag wasn't set, process the next task in the queue.
- if (!async) {
- complete(success);
- }
- } catch (err) {
- complete(err);
- }
- };
- // Begin task queue processing. Ie. run all tasks.
- Task.prototype.start = function(opts) {
- if (!opts) {
- opts = {};
- }
- // Abort if already running.
- if (this._running) { return false; }
- // Actually process the next task.
- var nextTask = function() {
- // Get next task+args object from queue.
- var thing;
- // Skip any placeholders or markers.
- do {
- thing = this._queue.shift();
- } while (thing === this._placeholder || thing === this._marker);
- // If queue was empty, we're all done.
- if (!thing) {
- this._running = false;
- if (this._options.done) {
- this._options.done();
- }
- return;
- }
- // Add a placeholder to the front of the queue.
- this._queue.unshift(this._placeholder);
- // Expose some information about the currently-running task.
- var context = {
- // The current task name plus args, as-passed.
- nameArgs: thing.nameArgs,
- // The current task name.
- name: thing.task.name,
- // The current task arguments.
- args: thing.args,
- // The current arguments, available as named flags.
- flags: thing.flags
- };
- // Actually run the task function (handling this.async, etc)
- this.runTaskFn(context, function() {
- return thing.task.fn.apply(this, this.args);
- }, nextTask, !!opts.asyncDone);
- }.bind(this);
- // Update flag.
- this._running = true;
- // Process the next task.
- nextTask();
- };
- // Clear remaining tasks from the queue.
- Task.prototype.clearQueue = function(options) {
- if (!options) { options = {}; }
- if (options.untilMarker) {
- this._queue.splice(0, this._queue.indexOf(this._marker) + 1);
- } else {
- this._queue = [];
- }
- // Make chainable!
- return this;
- };
- // Test to see if all of the given tasks have succeeded.
- Task.prototype.requires = function() {
- this.parseArgs(arguments).forEach(function(name) {
- var success = this._success[name];
- if (!success) {
- throw new Error('Required task "' + name +
- '" ' + (success === false ? 'failed' : 'must be run first') + '.');
- }
- }.bind(this));
- };
- // Override default options.
- Task.prototype.options = function(options) {
- Object.keys(options).forEach(function(name) {
- this._options[name] = options[name];
- }.bind(this));
- };
- }(typeof exports === 'object' && exports || this));
|