task.js 11 KB

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