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