1let EventEmitter = require('events').EventEmitter; 2let async = require('async'); 3let chalk = require('chalk'); 4// 'rule' module is required at the bottom because circular deps 5 6// Used for task value, so better not to use 7// null, since value should be unset/uninitialized 8let UNDEFINED_VALUE; 9 10const ROOT_TASK_NAME = '__rootTask__'; 11const POLLING_INTERVAL = 100; 12 13// Parse any positional args attached to the task-name 14function parsePrereqName(name) { 15 let taskArr = name.split('['); 16 let taskName = taskArr[0]; 17 let taskArgs = []; 18 if (taskArr[1]) { 19 taskArgs = taskArr[1].replace(/\]$/, ''); 20 taskArgs = taskArgs.split(','); 21 } 22 return { 23 name: taskName, 24 args: taskArgs 25 }; 26} 27 28/** 29 @name jake.Task 30 @class 31 @extends EventEmitter 32 @description A Jake Task 33 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 41 to the next task. 42 */ 43class Task extends EventEmitter { 44 45 constructor(name, prereqs, action, options) { 46 // EventEmitter ctor takes no args 47 super(); 48 49 if (name.indexOf(':') > -1) { 50 throw new Error('Task name cannot include a colon. It is used internally as namespace delimiter.'); 51 } 52 let opts = options || {}; 53 54 this._currentPrereqIndex = 0; 55 this._internal = false; 56 this._skipped = false; 57 58 this.name = name; 59 this.prereqs = prereqs; 60 this.action = action; 61 this.async = false; 62 this.taskStatus = Task.runStatuses.UNSTARTED; 63 this.description = null; 64 this.args = []; 65 this.value = UNDEFINED_VALUE; 66 this.concurrency = 1; 67 this.startTime = null; 68 this.endTime = null; 69 this.directory = null; 70 this.namespace = null; 71 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) { 75 this.async = true; 76 } 77 else { 78 if (opts.async) { 79 this.async = true; 80 } 81 if (opts.concurrency) { 82 this.concurrency = opts.concurrency; 83 } 84 } 85 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"); 89 } 90 } 91 92 get fullName() { 93 return this._getFullName(); 94 } 95 96 get params() { 97 return this._getParams(); 98 } 99 100 _initInvocationChain() { 101 // Legacy global invocation chain 102 jake._invocationChain.push(this); 103 104 // New root chain 105 if (!this._invocationChain) { 106 this._invocationChainRoot = true; 107 this._invocationChain = []; 108 if (jake.currentRunningTask) { 109 jake.currentRunningTask._waitForChains = jake.currentRunningTask._waitForChains || []; 110 jake.currentRunningTask._waitForChains.push(this._invocationChain); 111 } 112 } 113 } 114 115 /** 116 @name jake.Task#invoke 117 @function 118 @description Runs prerequisites, then this task. If the task has already 119 been run, will not run the task again. 120 */ 121 invoke() { 122 this._initInvocationChain(); 123 124 this.args = Array.prototype.slice.call(arguments); 125 this.reenabled = false; 126 this.runPrereqs(); 127 } 128 129 /** 130 @name jake.Task#execute 131 @function 132 @description Run only this task, without prereqs. If the task has already 133 been run, *will* run the task again. 134 */ 135 execute() { 136 this._initInvocationChain(); 137 138 this.args = Array.prototype.slice.call(arguments); 139 this.reenable(); 140 this.reenabled = true; 141 this.run(); 142 } 143 144 runPrereqs() { 145 if (this.prereqs && this.prereqs.length) { 146 147 if (this.concurrency > 1) { 148 async.eachLimit(this.prereqs, this.concurrency, 149 150 (name, cb) => { 151 let parsed = parsePrereqName(name); 152 153 let prereq = this.namespace.resolveTask(parsed.name) || 154 jake.attemptRule(name, this.namespace, 0) || 155 jake.createPlaceholderFileTask(name, this.namespace); 156 157 if (!prereq) { 158 throw new Error('Unknown task "' + name + '"'); 159 } 160 161 //Test for circular invocation 162 if(prereq === this) { 163 setImmediate(function () { 164 cb(new Error("Cannot use prereq " + prereq.name + " as a dependency of itself")); 165 }); 166 } 167 168 if (prereq.taskStatus == Task.runStatuses.DONE) { 169 //prereq already done, return 170 setImmediate(cb); 171 } 172 else { 173 //wait for complete before calling cb 174 prereq.once('_done', () => { 175 prereq.removeAllListeners('_done'); 176 setImmediate(cb); 177 }); 178 // Start the prereq if we are the first to encounter it 179 if (prereq.taskStatus === Task.runStatuses.UNSTARTED) { 180 prereq.taskStatus = Task.runStatuses.STARTED; 181 prereq.invoke.apply(prereq, parsed.args); 182 } 183 } 184 }, 185 186 (err) => { 187 //async callback is called after all prereqs have run. 188 if (err) { 189 throw err; 190 } 191 else { 192 setImmediate(this.run.bind(this)); 193 } 194 } 195 ); 196 } 197 else { 198 setImmediate(this.nextPrereq.bind(this)); 199 } 200 } 201 else { 202 setImmediate(this.run.bind(this)); 203 } 204 } 205 206 nextPrereq() { 207 let self = this; 208 let index = this._currentPrereqIndex; 209 let name = this.prereqs[index]; 210 let prereq; 211 let parsed; 212 213 if (name) { 214 215 parsed = parsePrereqName(name); 216 217 prereq = this.namespace.resolveTask(parsed.name) || 218 jake.attemptRule(name, this.namespace, 0) || 219 jake.createPlaceholderFileTask(name, this.namespace); 220 221 if (!prereq) { 222 throw new Error('Unknown task "' + name + '"'); 223 } 224 225 // Do when done 226 if (prereq.taskStatus == Task.runStatuses.DONE) { 227 self.handlePrereqDone(prereq); 228 } 229 else { 230 prereq.once('_done', () => { 231 this.handlePrereqDone(prereq); 232 prereq.removeAllListeners('_done'); 233 }); 234 if (prereq.taskStatus == Task.runStatuses.UNSTARTED) { 235 prereq.taskStatus = Task.runStatuses.STARTED; 236 prereq._invocationChain = this._invocationChain; 237 prereq.invoke.apply(prereq, parsed.args); 238 } 239 } 240 } 241 } 242 243 /** 244 @name jake.Task#reenable 245 @function 246 @description Reenables a task so that it can be run again. 247 */ 248 reenable(deep) { 249 let prereqs; 250 let prereq; 251 this._skipped = false; 252 this.taskStatus = Task.runStatuses.UNSTARTED; 253 this.value = UNDEFINED_VALUE; 254 if (deep && this.prereqs) { 255 prereqs = this.prereqs; 256 for (let i = 0, ii = prereqs.length; i < ii; i++) { 257 prereq = jake.Task[prereqs[i]]; 258 if (prereq) { 259 prereq.reenable(deep); 260 } 261 } 262 } 263 } 264 265 handlePrereqDone(prereq) { 266 this._currentPrereqIndex++; 267 if (this._currentPrereqIndex < this.prereqs.length) { 268 setImmediate(this.nextPrereq.bind(this)); 269 } 270 else { 271 setImmediate(this.run.bind(this)); 272 } 273 } 274 275 isNeeded() { 276 let needed = true; 277 if (this.taskStatus == Task.runStatuses.DONE) { 278 needed = false; 279 } 280 return needed; 281 } 282 283 run() { 284 let val, previous; 285 let hasAction = typeof this.action == 'function'; 286 287 if (!this.isNeeded()) { 288 this.emit('skip'); 289 this.emit('_done'); 290 } 291 else { 292 if (this._invocationChain.length) { 293 previous = this._invocationChain[this._invocationChain.length - 1]; 294 // 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 295 if (!(this.reenabled && previous == this)) { 296 if (previous.taskStatus != Task.runStatuses.DONE) { 297 let now = (new Date()).getTime(); 298 if (now - this.startTime > jake._taskTimeout) { 299 return jake.fail(`Timed out waiting for task: ${previous.name} with status of ${previous.taskStatus}`); 300 } 301 setTimeout(this.run.bind(this), POLLING_INTERVAL); 302 return; 303 } 304 } 305 } 306 if (!(this.reenabled && previous == this)) { 307 this._invocationChain.push(this); 308 } 309 310 if (!(this._internal || jake.program.opts.quiet)) { 311 console.log("Starting '" + chalk.green(this.fullName) + "'..."); 312 } 313 314 this.startTime = (new Date()).getTime(); 315 this.emit('start'); 316 317 jake.currentRunningTask = this; 318 319 if (hasAction) { 320 try { 321 if (this.directory) { 322 process.chdir(this.directory); 323 } 324 325 val = this.action.apply(this, this.args); 326 327 if (typeof val == 'object' && typeof val.then == 'function') { 328 this.async = true; 329 330 val.then( 331 (result) => { 332 setImmediate(() => { 333 this.complete(result); 334 }); 335 }, 336 (err) => { 337 setImmediate(() => { 338 this.errorOut(err); 339 }); 340 }); 341 } 342 } 343 catch (err) { 344 this.errorOut(err); 345 return; // Bail out, not complete 346 } 347 } 348 349 if (!(hasAction && this.async)) { 350 setImmediate(() => { 351 this.complete(val); 352 }); 353 } 354 } 355 } 356 357 errorOut(err) { 358 this.taskStatus = Task.runStatuses.ERROR; 359 this._invocationChain.chainStatus = Task.runStatuses.ERROR; 360 this.emit('error', err); 361 } 362 363 complete(val) { 364 365 if (Array.isArray(this._waitForChains)) { 366 let stillWaiting = this._waitForChains.some((chain) => { 367 return !(chain.chainStatus == Task.runStatuses.DONE || 368 chain.chainStatus == Task.runStatuses.ERROR); 369 }); 370 if (stillWaiting) { 371 let now = (new Date()).getTime(); 372 let elapsed = now - this.startTime; 373 if (elapsed > jake._taskTimeout) { 374 return jake.fail(`Timed out waiting for task: ${this.name} with status of ${this.taskStatus}. Elapsed: ${elapsed}`); 375 } 376 setTimeout(() => { 377 this.complete(val); 378 }, POLLING_INTERVAL); 379 return; 380 } 381 } 382 383 jake._invocationChain.splice(jake._invocationChain.indexOf(this), 1); 384 385 if (this._invocationChainRoot) { 386 this._invocationChain.chainStatus = Task.runStatuses.DONE; 387 } 388 389 this._currentPrereqIndex = 0; 390 391 // If 'complete' getting called because task has been 392 // run already, value will not be passed -- leave in place 393 if (!this._skipped) { 394 this.taskStatus = Task.runStatuses.DONE; 395 this.value = val; 396 397 this.emit('complete', this.value); 398 this.emit('_done'); 399 400 this.endTime = (new Date()).getTime(); 401 let taskTime = this.endTime - this.startTime; 402 403 if (!(this._internal || jake.program.opts.quiet)) { 404 console.log("Finished '" + chalk.green(this.fullName) + "' after " + chalk.magenta(taskTime + ' ms')); 405 } 406 407 } 408 } 409 410 _getFullName() { 411 let ns = this.namespace; 412 let path = (ns && ns.path) || ''; 413 path = (path && path.split(':')) || []; 414 if (this.namespace !== jake.defaultNamespace) { 415 path.push(this.namespace.name); 416 } 417 path.push(this.name); 418 return path.join(':'); 419 } 420 421 _getParams() { 422 if (!this.action) return ""; 423 let params = (new RegExp('(?:'+this.action.name+'\\s*|^)\\s*\\((.*?)\\)').exec(this.action.toString().replace(/\n/g, '')) || [''])[1].replace(/\/\*.*?\*\//g, '').replace(/ /g, ''); 424 return params; 425 } 426 427 static getBaseNamespacePath(fullName) { 428 return fullName.split(':').slice(0, -1).join(':'); 429 } 430 431 static getBaseTaskName(fullName) { 432 return fullName.split(':').pop(); 433 } 434} 435 436Task.runStatuses = { 437 UNSTARTED: 'unstarted', 438 DONE: 'done', 439 STARTED: 'started', 440 ERROR: 'error' 441}; 442 443Task.ROOT_TASK_NAME = ROOT_TASK_NAME; 444 445exports.Task = Task; 446 447// Required here because circular deps 448require('../rule'); 449 450