1 | (function(exports) {
|
2 |
|
3 | ;
|
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));
|