UNPKG

12.8 kBJavaScriptView Raw
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));