You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

323 lines
11 KiB

  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. (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 programatically).
  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. // Rename a task. This might be useful if you want to override the default
  80. // behavior of a task, while retaining the old name. This is a billion times
  81. // easier to implement than some kind of in-task "super" functionality.
  82. Task.prototype.renameTask = function(oldname, newname) {
  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 programatically.
  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) {
  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. done(err, success);
  199. }.bind(this);
  200. // When called, sets the async flag and returns a function that can
  201. // be used to continue processing the queue.
  202. context.async = function() {
  203. async = true;
  204. // The returned function should execute asynchronously in case
  205. // someone tries to do this.async()(); inside a task (WTF).
  206. return function(success) {
  207. setTimeout(function() { complete(success); }, 1);
  208. };
  209. };
  210. // Expose some information about the currently-running task.
  211. this.current = context;
  212. try {
  213. // Get the current task and run it, setting `this` inside the task
  214. // function to be something useful.
  215. var success = fn.call(context);
  216. // If the async flag wasn't set, process the next task in the queue.
  217. if (!async) {
  218. complete(success);
  219. }
  220. } catch (err) {
  221. complete(err);
  222. }
  223. };
  224. // Begin task queue processing. Ie. run all tasks.
  225. Task.prototype.start = function() {
  226. // Abort if already running.
  227. if (this._running) { return false; }
  228. // Actually process the next task.
  229. var nextTask = function() {
  230. // Get next task+args object from queue.
  231. var thing;
  232. // Skip any placeholders or markers.
  233. do {
  234. thing = this._queue.shift();
  235. } while (thing === this._placeholder || thing === this._marker);
  236. // If queue was empty, we're all done.
  237. if (!thing) {
  238. this._running = false;
  239. if (this._options.done) {
  240. this._options.done();
  241. }
  242. return;
  243. }
  244. // Add a placeholder to the front of the queue.
  245. this._queue.unshift(this._placeholder);
  246. // Expose some information about the currently-running task.
  247. var context = {
  248. // The current task name plus args, as-passed.
  249. nameArgs: thing.nameArgs,
  250. // The current task name.
  251. name: thing.task.name,
  252. // The current task arguments.
  253. args: thing.args,
  254. // The current arguments, available as named flags.
  255. flags: thing.flags
  256. };
  257. // Actually run the task function (handling this.async, etc)
  258. this.runTaskFn(context, function() {
  259. return thing.task.fn.apply(this, this.args);
  260. }, nextTask);
  261. }.bind(this);
  262. // Update flag.
  263. this._running = true;
  264. // Process the next task.
  265. nextTask();
  266. };
  267. // Clear remaining tasks from the queue.
  268. Task.prototype.clearQueue = function(options) {
  269. if (!options) { options = {}; }
  270. if (options.untilMarker) {
  271. this._queue.splice(0, this._queue.indexOf(this._marker) + 1);
  272. } else {
  273. this._queue = [];
  274. }
  275. // Make chainable!
  276. return this;
  277. };
  278. // Test to see if all of the given tasks have succeeded.
  279. Task.prototype.requires = function() {
  280. this.parseArgs(arguments).forEach(function(name) {
  281. var success = this._success[name];
  282. if (!success) {
  283. throw new Error('Required task "' + name +
  284. '" ' + (success === false ? 'failed' : 'must be run first') + '.');
  285. }
  286. }.bind(this));
  287. };
  288. // Override default options.
  289. Task.prototype.options = function(options) {
  290. Object.keys(options).forEach(function(name) {
  291. this._options[name] = options[name];
  292. }.bind(this));
  293. };
  294. }(typeof exports === 'object' && exports || this));