UNPKG

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