task.js 11 KB

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