source: Dev/trunk/node_modules/grunt/lib/util/task.js @ 484

Last change on this file since 484 was 484, checked in by hendrikvanantwerpen, 11 years ago

Commit node_modules, to make checkouts and builds more deterministic.

File size: 10.6 KB
Line 
1/*
2 * grunt
3 * http://gruntjs.com/
4 *
5 * Copyright (c) 2013 "Cowboy" Ben Alman
6 * Licensed under the MIT license.
7 * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT
8 */
9
10(function(exports) {
11
12  'use strict';
13
14  // Construct-o-rama.
15  function Task() {
16    // Information about the currently-running task.
17    this.current = {};
18    // Tasks.
19    this._tasks = {};
20    // Task queue.
21    this._queue = [];
22    // Queue placeholder (for dealing with nested tasks).
23    this._placeholder = {placeholder: true};
24    // Queue marker (for clearing the queue programmatically).
25    this._marker = {marker: true};
26    // Options.
27    this._options = {};
28    // Is the queue running?
29    this._running = false;
30    // Success status of completed tasks.
31    this._success = {};
32  }
33
34  // Expose the constructor function.
35  exports.Task = Task;
36
37  // Create a new Task instance.
38  exports.create = function() {
39    return new Task();
40  };
41
42  // If the task runner is running or an error handler is not defined, throw
43  // an exception. Otherwise, call the error handler directly.
44  Task.prototype._throwIfRunning = function(obj) {
45    if (this._running || !this._options.error) {
46      // Throw an exception that the task runner will catch.
47      throw obj;
48    } else {
49      // Not inside the task runner. Call the error handler and abort.
50      this._options.error.call({name: null}, obj);
51    }
52  };
53
54  // Register a new task.
55  Task.prototype.registerTask = function(name, info, fn) {
56    // If optional "info" string is omitted, shuffle arguments a bit.
57    if (fn == null) {
58      fn = info;
59      info = null;
60    }
61    // String or array of strings was passed instead of fn.
62    var tasks;
63    if (typeof fn !== 'function') {
64      // Array of task names.
65      tasks = this.parseArgs([fn]);
66      // This task function just runs the specified tasks.
67      fn = this.run.bind(this, fn);
68      fn.alias = true;
69      // Generate an info string if one wasn't explicitly passed.
70      if (!info) {
71        info = 'Alias for "' + tasks.join('", "') + '" task' +
72          (tasks.length === 1 ? '' : 's') + '.';
73      }
74    } else if (!info) {
75      info = 'Custom task.';
76    }
77    // Add task into cache.
78    this._tasks[name] = {name: name, info: info, fn: fn};
79    // Make chainable!
80    return this;
81  };
82
83  // Is the specified task an alias?
84  Task.prototype.isTaskAlias = function(name) {
85    return !!this._tasks[name].fn.alias;
86  };
87
88  // Rename a task. This might be useful if you want to override the default
89  // behavior of a task, while retaining the old name. This is a billion times
90  // easier to implement than some kind of in-task "super" functionality.
91  Task.prototype.renameTask = function(oldname, newname) {
92    // Rename task.
93    this._tasks[newname] = this._tasks[oldname];
94    // Update name property of task.
95    this._tasks[newname].name = newname;
96    // Remove old name.
97    delete this._tasks[oldname];
98    // Make chainable!
99    return this;
100  };
101
102  // Argument parsing helper. Supports these signatures:
103  //  fn('foo')                 // ['foo']
104  //  fn('foo', 'bar', 'baz')   // ['foo', 'bar', 'baz']
105  //  fn(['foo', 'bar', 'baz']) // ['foo', 'bar', 'baz']
106  Task.prototype.parseArgs = function(args) {
107    // Return the first argument if it's an array, otherwise return an array
108    // of all arguments.
109    return Array.isArray(args[0]) ? args[0] : [].slice.call(args);
110  };
111
112  // Split a colon-delimited string into an array, unescaping (but not
113  // splitting on) any \: escaped colons.
114  Task.prototype.splitArgs = function(str) {
115    if (!str) { return []; }
116    // Store placeholder for \\ followed by \:
117    str = str.replace(/\\\\/g, '\uFFFF').replace(/\\:/g, '\uFFFE');
118    // Split on :
119    return str.split(':').map(function(s) {
120      // Restore place-held : followed by \\
121      return s.replace(/\uFFFE/g, ':').replace(/\uFFFF/g, '\\');
122    });
123  };
124
125  // Given a task name, determine which actual task will be called, and what
126  // arguments will be passed into the task callback. "foo" -> task "foo", no
127  // args. "foo:bar:baz" -> task "foo:bar:baz" with no args (if "foo:bar:baz"
128  // task exists), otherwise task "foo:bar" with arg "baz" (if "foo:bar" task
129  // exists), otherwise task "foo" with args "bar" and "baz".
130  Task.prototype._taskPlusArgs = function(name) {
131    // Get task name / argument parts.
132    var parts = this.splitArgs(name);
133    // Start from the end, not the beginning!
134    var i = parts.length;
135    var task;
136    do {
137      // Get a task.
138      task = this._tasks[parts.slice(0, i).join(':')];
139      // If the task doesn't exist, decrement `i`, and if `i` is greater than
140      // 0, repeat.
141    } while (!task && --i > 0);
142    // Just the args.
143    var args = parts.slice(i);
144    // Maybe you want to use them as flags instead of as positional args?
145    var flags = {};
146    args.forEach(function(arg) { flags[arg] = true; });
147    // The task to run and the args to run it with.
148    return {task: task, nameArgs: name, args: args, flags: flags};
149  };
150
151  // Append things to queue in the correct spot.
152  Task.prototype._push = function(things) {
153    // Get current placeholder index.
154    var index = this._queue.indexOf(this._placeholder);
155    if (index === -1) {
156      // No placeholder, add task+args objects to end of queue.
157      this._queue = this._queue.concat(things);
158    } else {
159      // Placeholder exists, add task+args objects just before placeholder.
160      [].splice.apply(this._queue, [index, 0].concat(things));
161    }
162  };
163
164  // Enqueue a task.
165  Task.prototype.run = function() {
166    // Parse arguments into an array, returning an array of task+args objects.
167    var things = this.parseArgs(arguments).map(this._taskPlusArgs, this);
168    // Throw an exception if any tasks weren't found.
169    var fails = things.filter(function(thing) { return !thing.task; });
170    if (fails.length > 0) {
171      this._throwIfRunning(new Error('Task "' + fails[0].nameArgs + '" not found.'));
172      return this;
173    }
174    // Append things to queue in the correct spot.
175    this._push(things);
176    // Make chainable!
177    return this;
178  };
179
180  // Add a marker to the queue to facilitate clearing it programmatically.
181  Task.prototype.mark = function() {
182    this._push(this._marker);
183    // Make chainable!
184    return this;
185  };
186
187  // Run a task function, handling this.async / return value.
188  Task.prototype.runTaskFn = function(context, fn, done) {
189    // Async flag.
190    var async = false;
191
192    // Update the internal status object and run the next task.
193    var complete = function(success) {
194      var err = null;
195      if (success === false) {
196        // Since false was passed, the task failed generically.
197        err = new Error('Task "' + context.nameArgs + '" failed.');
198      } else if (success instanceof Error || {}.toString.call(success) === '[object Error]') {
199        // An error object was passed, so the task failed specifically.
200        err = success;
201        success = false;
202      } else {
203        // The task succeeded.
204        success = true;
205      }
206      // The task has ended, reset the current task object.
207      this.current = {};
208      // A task has "failed" only if it returns false (async) or if the
209      // function returned by .async is passed false.
210      this._success[context.nameArgs] = success;
211      // If task failed, call error handler.
212      if (!success && this._options.error) {
213        this._options.error.call({name: context.name, nameArgs: context.nameArgs}, err);
214      }
215      done(err, success);
216    }.bind(this);
217
218    // When called, sets the async flag and returns a function that can
219    // be used to continue processing the queue.
220    context.async = function() {
221      async = true;
222      // The returned function should execute asynchronously in case
223      // someone tries to do this.async()(); inside a task (WTF).
224      return function(success) {
225        setTimeout(function() { complete(success); }, 1);
226      };
227    };
228
229    // Expose some information about the currently-running task.
230    this.current = context;
231
232    try {
233      // Get the current task and run it, setting `this` inside the task
234      // function to be something useful.
235      var success = fn.call(context);
236      // If the async flag wasn't set, process the next task in the queue.
237      if (!async) {
238        complete(success);
239      }
240    } catch (err) {
241      complete(err);
242    }
243  };
244
245  // Begin task queue processing. Ie. run all tasks.
246  Task.prototype.start = function() {
247    // Abort if already running.
248    if (this._running) { return false; }
249    // Actually process the next task.
250    var nextTask = function() {
251      // Get next task+args object from queue.
252      var thing;
253      // Skip any placeholders or markers.
254      do {
255        thing = this._queue.shift();
256      } while (thing === this._placeholder || thing === this._marker);
257      // If queue was empty, we're all done.
258      if (!thing) {
259        this._running = false;
260        if (this._options.done) {
261          this._options.done();
262        }
263        return;
264      }
265      // Add a placeholder to the front of the queue.
266      this._queue.unshift(this._placeholder);
267
268      // Expose some information about the currently-running task.
269      var context = {
270        // The current task name plus args, as-passed.
271        nameArgs: thing.nameArgs,
272        // The current task name.
273        name: thing.task.name,
274        // The current task arguments.
275        args: thing.args,
276        // The current arguments, available as named flags.
277        flags: thing.flags
278      };
279
280      // Actually run the task function (handling this.async, etc)
281      this.runTaskFn(context, function() {
282        return thing.task.fn.apply(this, this.args);
283      }, nextTask);
284
285    }.bind(this);
286
287    // Update flag.
288    this._running = true;
289    // Process the next task.
290    nextTask();
291  };
292
293  // Clear remaining tasks from the queue.
294  Task.prototype.clearQueue = function(options) {
295    if (!options) { options = {}; }
296    if (options.untilMarker) {
297      this._queue.splice(0, this._queue.indexOf(this._marker) + 1);
298    } else {
299      this._queue = [];
300    }
301    // Make chainable!
302    return this;
303  };
304
305  // Test to see if all of the given tasks have succeeded.
306  Task.prototype.requires = function() {
307    this.parseArgs(arguments).forEach(function(name) {
308      var success = this._success[name];
309      if (!success) {
310        throw new Error('Required task "' + name +
311          '" ' + (success === false ? 'failed' : 'must be run first') + '.');
312      }
313    }.bind(this));
314  };
315
316  // Override default options.
317  Task.prototype.options = function(options) {
318    Object.keys(options).forEach(function(name) {
319      this._options[name] = options[name];
320    }.bind(this));
321  };
322
323}(typeof exports === 'object' && exports || this));
Note: See TracBrowser for help on using the repository browser.