1 let EventEmitter = require('events').EventEmitter;
2 let async = require('async');
3 let chalk = require('chalk');
4 // 'rule' module is required at the bottom because circular deps
6 // Used for task value, so better not to use
7 // null, since value should be unset/uninitialized
10 const ROOT_TASK_NAME = '__rootTask__';
11 const POLLING_INTERVAL = 100;
13 // Parse any positional args attached to the task-name
14 function parsePrereqName(name) {
15 let taskArr = name.split('[');
16 let taskName = taskArr[0];
19 taskArgs = taskArr[1].replace(/\]$/, '');
20 taskArgs = taskArgs.split(',');
32 @description A Jake Task
34 @param {String} name The name of the Task
35 @param {Array} [prereqs] Prerequisites to be run before this task
36 @param {Function} [action] The action to perform for this task
37 @param {Object} [opts]
38 @param {Array} [opts.asyc=false] Perform this task asynchronously.
39 If you flag a task with this option, you must call the global
40 `complete` method inside the task's action, for execution to proceed
43 class Task extends EventEmitter {
45 constructor(name, prereqs, action, options) {
46 // EventEmitter ctor takes no args
49 if (name.indexOf(':') > -1) {
50 throw new Error('Task name cannot include a colon. It is used internally as namespace delimiter.');
52 let opts = options || {};
54 this._currentPrereqIndex = 0;
55 this._internal = false;
56 this._skipped = false;
59 this.prereqs = prereqs;
62 this.taskStatus = Task.runStatuses.UNSTARTED;
63 this.description = null;
65 this.value = UNDEFINED_VALUE;
67 this.startTime = null;
69 this.directory = null;
70 this.namespace = null;
72 // Support legacy async-flag -- if not explicitly passed or falsy, will
73 // be set to empty-object
74 if (typeof opts == 'boolean' && opts === true) {
81 if (opts.concurrency) {
82 this.concurrency = opts.concurrency;
86 //Do a test on self dependencies for this task
87 if(Array.isArray(this.prereqs) && this.prereqs.indexOf(this.name) !== -1) {
88 throw new Error("Cannot use prereq " + this.name + " as a dependency of itself");
93 return this._getFullName();
96 _initInvocationChain() {
97 // Legacy global invocation chain
98 jake._invocationChain.push(this);
101 if (!this._invocationChain) {
102 this._invocationChainRoot = true;
103 this._invocationChain = [];
104 if (jake.currentRunningTask) {
105 jake.currentRunningTask._waitForChains = jake.currentRunningTask._waitForChains || [];
106 jake.currentRunningTask._waitForChains.push(this._invocationChain);
112 @name jake.Task#invoke
114 @description Runs prerequisites, then this task. If the task has already
115 been run, will not run the task again.
118 this._initInvocationChain();
120 this.args = Array.prototype.slice.call(arguments);
121 this.reenabled = false
126 @name jake.Task#execute
128 @description Run only this task, without prereqs. If the task has already
129 been run, *will* run the task again.
132 this._initInvocationChain();
134 this.args = Array.prototype.slice.call(arguments);
136 this.reenabled = true
141 if (this.prereqs && this.prereqs.length) {
143 if (this.concurrency > 1) {
144 async.eachLimit(this.prereqs, this.concurrency,
147 let parsed = parsePrereqName(name);
149 let prereq = this.namespace.resolveTask(parsed.name) ||
150 jake.attemptRule(name, this.namespace, 0) ||
151 jake.createPlaceholderFileTask(name, this.namespace);
154 throw new Error('Unknown task "' + name + '"');
157 //Test for circular invocation
158 if(prereq === this) {
159 setImmediate(function () {
160 cb(new Error("Cannot use prereq " + prereq.name + " as a dependency of itself"));
164 if (prereq.taskStatus == Task.runStatuses.DONE) {
165 //prereq already done, return
169 //wait for complete before calling cb
170 prereq.once('_done', () => {
171 prereq.removeAllListeners('_done');
174 // Start the prereq if we are the first to encounter it
175 if (prereq.taskStatus === Task.runStatuses.UNSTARTED) {
176 prereq.taskStatus = Task.runStatuses.STARTED;
177 prereq.invoke.apply(prereq, parsed.args);
183 //async callback is called after all prereqs have run.
188 setImmediate(this.run.bind(this));
194 setImmediate(this.nextPrereq.bind(this));
198 setImmediate(this.run.bind(this));
204 let index = this._currentPrereqIndex;
205 let name = this.prereqs[index];
211 parsed = parsePrereqName(name);
213 prereq = this.namespace.resolveTask(parsed.name) ||
214 jake.attemptRule(name, this.namespace, 0) ||
215 jake.createPlaceholderFileTask(name, this.namespace);
218 throw new Error('Unknown task "' + name + '"');
222 if (prereq.taskStatus == Task.runStatuses.DONE) {
223 self.handlePrereqDone(prereq);
226 prereq.once('_done', () => {
227 this.handlePrereqDone(prereq);
228 prereq.removeAllListeners('_done');
230 if (prereq.taskStatus == Task.runStatuses.UNSTARTED) {
231 prereq.taskStatus = Task.runStatuses.STARTED;
232 prereq._invocationChain = this._invocationChain;
233 prereq.invoke.apply(prereq, parsed.args);
240 @name jake.Task#reenable
242 @description Reenables a task so that it can be run again.
247 this._skipped = false;
248 this.taskStatus = Task.runStatuses.UNSTARTED;
249 this.value = UNDEFINED_VALUE;
250 if (deep && this.prereqs) {
251 prereqs = this.prereqs;
252 for (let i = 0, ii = prereqs.length; i < ii; i++) {
253 prereq = jake.Task[prereqs[i]];
255 prereq.reenable(deep);
261 handlePrereqDone(prereq) {
262 this._currentPrereqIndex++;
263 if (this._currentPrereqIndex < this.prereqs.length) {
264 setImmediate(this.nextPrereq.bind(this));
267 setImmediate(this.run.bind(this));
273 if (this.taskStatus == Task.runStatuses.DONE) {
281 let hasAction = typeof this.action == 'function';
283 if (!this.isNeeded()) {
288 if (this._invocationChain.length) {
289 previous = this._invocationChain[this._invocationChain.length - 1];
290 // If this task is repeating and its previous is equal to this, don't check its status because it was set to UNSTARTED by the reenable() method
291 if (!(this.reenabled && previous == this)) {
292 if (previous.taskStatus != Task.runStatuses.DONE) {
293 let now = (new Date()).getTime();
294 if (now - this.startTime > jake._taskTimeout) {
295 return jake.fail(`Timed out waiting for task: ${previous.name} with status of ${previous.taskStatus}`);
297 setTimeout(this.run.bind(this), POLLING_INTERVAL);
302 if (!(this.reenabled && previous == this)) {
303 this._invocationChain.push(this);
306 if (!(this._internal || jake.program.opts.quiet)) {
307 console.log("Starting '" + chalk.green(this.fullName) + "'...");
310 this.startTime = (new Date()).getTime();
313 jake.currentRunningTask = this;
317 if (this.directory) {
318 process.chdir(this.directory);
321 val = this.action.apply(this, this.args);
323 if (typeof val == 'object' && typeof val.then == 'function') {
329 this.complete(result);
341 return; // Bail out, not complete
345 if (!(hasAction && this.async)) {
354 this.taskStatus = Task.runStatuses.ERROR;
355 this._invocationChain.chainStatus = Task.runStatuses.ERROR;
356 this.emit('error', err);
361 if (Array.isArray(this._waitForChains)) {
362 let stillWaiting = this._waitForChains.some((chain) => {
363 return !(chain.chainStatus == Task.runStatuses.DONE ||
364 chain.chainStatus == Task.runStatuses.ERROR);
367 let now = (new Date()).getTime();
368 let elapsed = now - this.startTime;
369 if (elapsed > jake._taskTimeout) {
370 return jake.fail(`Timed out waiting for task: ${this.name} with status of ${this.taskStatus}. Elapsed: ${elapsed}`);
374 }, POLLING_INTERVAL);
379 jake._invocationChain.splice(jake._invocationChain.indexOf(this), 1);
381 if (this._invocationChainRoot) {
382 this._invocationChain.chainStatus = Task.runStatuses.DONE;
385 this._currentPrereqIndex = 0;
387 // If 'complete' getting called because task has been
388 // run already, value will not be passed -- leave in place
389 if (!this._skipped) {
390 this.taskStatus = Task.runStatuses.DONE;
393 this.emit('complete', this.value);
396 this.endTime = (new Date()).getTime();
397 let taskTime = this.endTime - this.startTime;
399 if (!(this._internal || jake.program.opts.quiet)) {
400 console.log("Finished '" + chalk.green(this.fullName) + "' after " + chalk.magenta(taskTime + ' ms'));
407 let ns = this.namespace;
408 let path = (ns && ns.path) || '';
409 path = (path && path.split(':')) || [];
410 if (this.namespace !== jake.defaultNamespace) {
411 path.push(this.namespace.name);
413 path.push(this.name);
414 return path.join(':');
417 static getBaseNamespacePath(fullName) {
418 return fullName.split(':').slice(0, -1).join(':');
421 static getBaseTaskName(fullName) {
422 return fullName.split(':').pop();
427 UNSTARTED: 'unstarted',
433 Task.ROOT_TASK_NAME = ROOT_TASK_NAME;
437 // Required here because circular deps