1 | /*
|
2 | * grunt
|
3 | * https://github.com/cowboy/grunt
|
4 | *
|
5 | * Copyright (c) 2012 "Cowboy" Ben Alman
|
6 | * Licensed under the MIT license.
|
7 | * http://benalman.com/about/license/
|
8 | */
|
9 |
|
10 | (function(exports) {
|
11 |
|
12 | // Construct-o-rama.
|
13 | function Task() {
|
14 | // Information about the currently-running task.
|
15 | this.current = {};
|
16 | // Helpers.
|
17 | this._helpers = {};
|
18 | // Tasks.
|
19 | this._tasks = {};
|
20 | // Task queue.
|
21 | this._queue = [];
|
22 | // Queue placeholder (for dealing with nested tasks).
|
23 | this._placeholder = {};
|
24 | // Options.
|
25 | this._options = {};
|
26 | // Is the queue running?
|
27 | this._running = false;
|
28 | // Success status of completed tasks.
|
29 | this._success = {};
|
30 | }
|
31 |
|
32 | // Expose the constructor function.
|
33 | exports.Task = Task;
|
34 |
|
35 | // Create a new Task instance.
|
36 | exports.create = function() {
|
37 | return new Task();
|
38 | };
|
39 |
|
40 | // Error constructors.
|
41 | function TaskError(message) {
|
42 | this.name = 'TaskError';
|
43 | this.message = message;
|
44 | }
|
45 | TaskError.prototype = new Error();
|
46 |
|
47 | function HelperError(message) {
|
48 | this.name = 'HelperError';
|
49 | this.message = message;
|
50 | }
|
51 | HelperError.prototype = new Error();
|
52 |
|
53 | // Expose the ability to create a new taskError.
|
54 | Task.prototype.taskError = function(message, e) {
|
55 | var error = new TaskError(message);
|
56 | error.origError = e;
|
57 | return error;
|
58 | };
|
59 |
|
60 | // Register a new helper.
|
61 | Task.prototype.registerHelper = function(name, fn) {
|
62 | // Add task into cache.
|
63 | this._helpers[name] = fn;
|
64 | // Make chainable!
|
65 | return this;
|
66 | };
|
67 |
|
68 | // Rename a helper. This might be useful if you want to override the default
|
69 | // behavior of a helper, while retaining the old name (to avoid having to
|
70 | // completely recreate an already-made task just because you needed to
|
71 | // override or extend a built-in helper).
|
72 | Task.prototype.renameHelper = function(oldname, newname) {
|
73 | // Rename helper.
|
74 | this._helpers[newname] = this._helpers[oldname];
|
75 | // Remove old name.
|
76 | delete this._helpers[oldname];
|
77 | // Make chainable!
|
78 | return this;
|
79 | };
|
80 |
|
81 | // If the task runner is running or an error handler is not defined, throw
|
82 | // an exception. Otherwise, call the error handler directly.
|
83 | Task.prototype._throwIfRunning = function(obj) {
|
84 | if (this._running || !this._options.error) {
|
85 | // Throw an exception that the task runner will catch.
|
86 | throw obj;
|
87 | } else {
|
88 | // Not inside the task runner. Call the error handler and abort.
|
89 | this._options.error.call({name: null}, obj);
|
90 | }
|
91 | };
|
92 |
|
93 | // Execute a helper.
|
94 | Task.prototype.helper = function(isDirective, name) {
|
95 | var args = [].slice.call(arguments, 1);
|
96 | if (isDirective !== true) {
|
97 | name = isDirective;
|
98 | isDirective = false;
|
99 | } else {
|
100 | args = args.slice(1);
|
101 | }
|
102 | var helper = this._helpers[name];
|
103 | if (!helper) {
|
104 | // Helper not found.
|
105 | this._throwIfRunning(new HelperError('Helper "' + name + '" not found.'));
|
106 | return;
|
107 | }
|
108 | // Provide a few useful values on this.
|
109 | var context = {args: args, flags: {}};
|
110 | // Maybe you want to use flags instead of positional args?
|
111 | args.forEach(function(arg) { context.flags[arg] = true; });
|
112 | // Let the user know if it was used as a directive.
|
113 | if (isDirective) { context.directive = true; }
|
114 | // Invoke helper with any remaining arguments and return its value.
|
115 | return helper.apply(context, args);
|
116 | };
|
117 |
|
118 | // If a <foo:bar:baz> directive is passed, return ["foo", "bar", "baz"],
|
119 | // otherwise null.
|
120 | var directiveRe = /^<(.*)>$/;
|
121 | Task.prototype.getDirectiveParts = function(str) {
|
122 | var matches = str.match(directiveRe);
|
123 | // If the string doesn't look like a directive, return null.
|
124 | if (!matches) { return null; }
|
125 | // Split the name into parts.
|
126 | var parts = matches[1].split(':');
|
127 | // If a matching helper was found, return the parts, otherwise null.
|
128 | return this._helpers[parts[0]] ? parts : null;
|
129 | };
|
130 |
|
131 | // If value matches the <handler:arg1:arg2> format, and the specified handler
|
132 | // exists, it's a directive. Execute the matching handler and return its
|
133 | // value. If not, but an optional callback was passed, pass the value into
|
134 | // the callback and return its result. If no callback was specified, return
|
135 | // the value.
|
136 | Task.prototype.directive = function(value, fn) {
|
137 | // Get parts if a string was passed and it looks like a directive.
|
138 | var directive = typeof value === 'string' ? this.getDirectiveParts(value) : null;
|
139 |
|
140 | if (directive) {
|
141 | // If it looks like a directive and a matching helper exists, call the
|
142 | // helper by applying all directive parts and return its value.
|
143 | return this.helper.apply(this, [true].concat(directive));
|
144 | } else if (fn) {
|
145 | // Not a directive, but a callback was passed. Pass the value into the
|
146 | // callback and return its result.
|
147 | return fn(value);
|
148 | }
|
149 | // A callback wasn't specified or a valid handler wasn't found, so just
|
150 | // return the value.
|
151 | return value;
|
152 | };
|
153 |
|
154 | // Register a new task.
|
155 | Task.prototype.registerTask = function(name, info, fn) {
|
156 | // If optional "info" string is omitted, shuffle arguments a bit.
|
157 | if (fn == null) {
|
158 | fn = info;
|
159 | info = '';
|
160 | }
|
161 | // String or array of strings was passed instead of fn.
|
162 | var tasks;
|
163 | if (typeof fn !== 'function') {
|
164 | // Array of task names.
|
165 | tasks = this.parseArgs([fn]);
|
166 | // This task function just runs the specified tasks.
|
167 | fn = this.run.bind(this, fn);
|
168 | fn.alias = true;
|
169 | // Generate an info string if one wasn't explicitly passed.
|
170 | if (!info) {
|
171 | info = 'Alias for "' + tasks.join(' ') + '" task' +
|
172 | (tasks.length === 1 ? '' : 's') + '.';
|
173 | }
|
174 | }
|
175 | // Add task into cache.
|
176 | this._tasks[name] = {name: name, info: info, fn: fn};
|
177 | // Make chainable!
|
178 | return this;
|
179 | };
|
180 |
|
181 | // Is the specified task an alias?
|
182 | Task.prototype.isTaskAlias = function(name) {
|
183 | return !!this._tasks[name].fn.alias;
|
184 | };
|
185 |
|
186 | // Rename a task. This might be useful if you want to override the default
|
187 | // behavior of a task, while retaining the old name. This is a billion times
|
188 | // easier to implement than some kind of in-task "super" functionality.
|
189 | Task.prototype.renameTask = function(oldname, newname) {
|
190 | // Rename helper.
|
191 | this._tasks[newname] = this._tasks[oldname];
|
192 | // Update name property of task.
|
193 | this._tasks[newname].name = newname;
|
194 | // Remove old name.
|
195 | delete this._tasks[oldname];
|
196 | // Make chainable!
|
197 | return this;
|
198 | };
|
199 |
|
200 | // Argument parsing helper. Supports these signatures:
|
201 | // fn('foo') // ['foo']
|
202 | // fn('foo bar baz') // ['foo', 'bar', 'baz']
|
203 | // fn('foo', 'bar', 'baz') // ['foo', 'bar', 'baz']
|
204 | // fn(['foo', 'bar', 'baz']) // ['foo', 'bar', 'baz']
|
205 | Task.prototype.parseArgs = function(_arguments) {
|
206 | // If there are multiple (or zero) arguments, convert the _arguments object
|
207 | // into an array and return that.
|
208 | return _arguments.length !== 1 ? [].slice.call(_arguments) :
|
209 | // Return the first argument if it's an Array.
|
210 | Array.isArray(_arguments[0]) ? _arguments[0] :
|
211 | // Split the first argument on space.
|
212 | typeof _arguments[0] === 'string' ? _arguments[0].split(/\s+/) :
|
213 | // Just return an array containing the first argument. (todo: deprecate)
|
214 | [_arguments[0]];
|
215 | };
|
216 |
|
217 | // Given a task name, determine which actual task will be called, and what
|
218 | // arguments will be passed into the task callback. "foo" -> task "foo", no
|
219 | // args. "foo:bar:baz" -> task "foo:bar:baz" with no args (if "foo:bar:baz"
|
220 | // task exists), otherwise task "foo:bar" with arg "baz" (if "foo:bar" task
|
221 | // exists), otherwise task "foo" with args "bar" and "baz".
|
222 | Task.prototype._taskPlusArgs = function(name) {
|
223 | // Task name / argument parts.
|
224 | var parts = name.split(':');
|
225 | // Start from the end, not the beginning!
|
226 | var i = parts.length;
|
227 | var task;
|
228 | do {
|
229 | // Get a task.
|
230 | task = this._tasks[parts.slice(0, i).join(':')];
|
231 | // If the task doesn't exist, decrement `i`, and if `i` is greater than
|
232 | // 0, repeat.
|
233 | } while (!task && --i > 0);
|
234 | // Just the args.
|
235 | var args = parts.slice(i);
|
236 | // Maybe you want to use them as flags instead of as positional args?
|
237 | var flags = {};
|
238 | args.forEach(function(arg) { flags[arg] = true; });
|
239 | // The task to run and the args to run it with.
|
240 | return {task: task, nameArgs: name, args: args, flags: flags};
|
241 | };
|
242 |
|
243 | // Enqueue a task.
|
244 | Task.prototype.run = function() {
|
245 | // Parse arguments into an array, returning an array of task+args objects.
|
246 | var things = this.parseArgs(arguments).map(this._taskPlusArgs, this);
|
247 | // Throw an exception if any tasks weren't found.
|
248 | var fails = things.filter(function(thing) { return !thing.task; });
|
249 | if (fails.length > 0) {
|
250 | this._throwIfRunning(new TaskError('Task "' + fails[0].nameArgs + '" not found.'));
|
251 | return this;
|
252 | }
|
253 | // Get current placeholder index.
|
254 | var index = this._queue.indexOf(this._placeholder);
|
255 | if (index === -1) {
|
256 | // No placeholder, add task+args objects to end of queue.
|
257 | this._queue = this._queue.concat(things);
|
258 | } else {
|
259 | // Placeholder exists, add task+args objects just before placeholder.
|
260 | [].splice.apply(this._queue, [index, 0].concat(things));
|
261 | }
|
262 | // Make chainable!
|
263 | return this;
|
264 | };
|
265 |
|
266 | // Begin task queue processing. Ie. run all tasks.
|
267 | Task.prototype.start = function() {
|
268 | // Abort if already running.
|
269 | if (this._running) { return false; }
|
270 | // Actually process the next task.
|
271 | var nextTask = function() {
|
272 | // Async flag.
|
273 | var async = false;
|
274 | // Get next task+args object from queue.
|
275 | var thing;
|
276 | // Skip any placeholders.
|
277 | do { thing = this._queue.shift(); } while (thing === this._placeholder);
|
278 | // If queue was empty, we're all done.
|
279 | if (!thing) {
|
280 | this._running = false;
|
281 | if (this._options.done) {
|
282 | this._options.done();
|
283 | }
|
284 | return;
|
285 | }
|
286 | // Add a placeholder to the front of the queue.
|
287 | this._queue.unshift(this._placeholder);
|
288 | // Update the internal status object and run the next task.
|
289 | var complete = function(status, errorObj) {
|
290 | this.current = {};
|
291 | // A task has "failed" only if it returns false (async) or if the
|
292 | // function returned by .async is passed false.
|
293 | this._success[thing.nameArgs] = status !== false;
|
294 | // If task failed, call error handler.
|
295 | if (status === false && this._options.error) {
|
296 | this._options.error.call({name: thing.task.name, nameArgs: thing.nameArgs}, errorObj ||
|
297 | new TaskError('Task "' + thing.nameArgs + '" failed.'));
|
298 | }
|
299 | // Run the next task.
|
300 | nextTask();
|
301 | }.bind(this);
|
302 |
|
303 | // Expose some information about the currently-running task.
|
304 | this.current = {
|
305 | // The current task name plus args, as-passed.
|
306 | nameArgs: thing.nameArgs,
|
307 | // The current task name.
|
308 | name: thing.task.name,
|
309 | // The current task arguments.
|
310 | args: thing.args,
|
311 | // The current arguments, available as named flags.
|
312 | flags: thing.flags,
|
313 | // When called, sets the async flag and returns a function that can
|
314 | // be used to continue processing the queue.
|
315 | async: function() {
|
316 | async = true;
|
317 | return complete;
|
318 | }
|
319 | };
|
320 |
|
321 | try {
|
322 | // Get the current task and run it, setting `this` inside the task
|
323 | // function to be something useful.
|
324 | var status = thing.task.fn.apply(this.current, thing.args);
|
325 | // If the async flag wasn't set, process the next task in the queue.
|
326 | if (!async) {
|
327 | complete(status);
|
328 | }
|
329 | } catch (e) {
|
330 | if (e instanceof TaskError || e instanceof HelperError) {
|
331 | complete(false, e);
|
332 | } else {
|
333 | throw e;
|
334 | }
|
335 | }
|
336 | }.bind(this);
|
337 | // Update flag.
|
338 | this._running = true;
|
339 | // Process the next task.
|
340 | nextTask();
|
341 | };
|
342 |
|
343 | // Clear all remaining tasks from the queue, or a subset.
|
344 | Task.prototype.clearQueue = function() {
|
345 | this._queue = [];
|
346 | // Make chainable!
|
347 | return this;
|
348 | };
|
349 |
|
350 | // Test to see if all of the given tasks have succeeded.
|
351 | Task.prototype.requires = function() {
|
352 | this.parseArgs(arguments).forEach(function(name) {
|
353 | var success = this._success[name];
|
354 | if (!success) {
|
355 | throw new TaskError('Required task "' + name +
|
356 | '" ' + (success === false ? 'failed' : 'missing') + '.');
|
357 | }
|
358 | }.bind(this));
|
359 | };
|
360 |
|
361 | // Override default options.
|
362 | Task.prototype.options = function(options) {
|
363 | Object.keys(options).forEach(function(name) {
|
364 | this._options[name] = options[name];
|
365 | }.bind(this));
|
366 | };
|
367 |
|
368 | }(typeof exports === 'object' && exports || this));
|