| [9af201e] | 1 | let EventEmitter = require('events').EventEmitter;
|
|---|
| 2 | let async = require('async');
|
|---|
| 3 | let pc = require('picocolors');
|
|---|
| 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
|
|---|
| 8 | let UNDEFINED_VALUE;
|
|---|
| 9 |
|
|---|
| 10 | const ROOT_TASK_NAME = '__rootTask__';
|
|---|
| 11 | const POLLING_INTERVAL = 100;
|
|---|
| 12 |
|
|---|
| 13 | // Parse any positional args attached to the task-name
|
|---|
| 14 | function 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 | */
|
|---|
| 43 | class 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 | jake.emit('started', {
|
|---|
| 312 | name: this.fullName,
|
|---|
| 313 | task: this,
|
|---|
| 314 | });
|
|---|
| 315 | console.log("Starting '" + pc.green(this.fullName) + "'...");
|
|---|
| 316 | }
|
|---|
| 317 |
|
|---|
| 318 | this.startTime = (new Date()).getTime();
|
|---|
| 319 | this.emit('start');
|
|---|
| 320 |
|
|---|
| 321 | jake.currentRunningTask = this;
|
|---|
| 322 |
|
|---|
| 323 | if (hasAction) {
|
|---|
| 324 | try {
|
|---|
| 325 | if (this.directory) {
|
|---|
| 326 | process.chdir(this.directory);
|
|---|
| 327 | }
|
|---|
| 328 |
|
|---|
| 329 | val = this.action.apply(this, this.args);
|
|---|
| 330 |
|
|---|
| 331 | if (typeof val == 'object' && typeof val.then == 'function') {
|
|---|
| 332 | this.async = true;
|
|---|
| 333 |
|
|---|
| 334 | val.then(
|
|---|
| 335 | (result) => {
|
|---|
| 336 | setImmediate(() => {
|
|---|
| 337 | this.complete(result);
|
|---|
| 338 | });
|
|---|
| 339 | },
|
|---|
| 340 | (err) => {
|
|---|
| 341 | setImmediate(() => {
|
|---|
| 342 | this.errorOut(err);
|
|---|
| 343 | });
|
|---|
| 344 | });
|
|---|
| 345 | }
|
|---|
| 346 | }
|
|---|
| 347 | catch (err) {
|
|---|
| 348 | this.errorOut(err);
|
|---|
| 349 | return; // Bail out, not complete
|
|---|
| 350 | }
|
|---|
| 351 | }
|
|---|
| 352 |
|
|---|
| 353 | if (!(hasAction && this.async)) {
|
|---|
| 354 | setImmediate(() => {
|
|---|
| 355 | this.complete(val);
|
|---|
| 356 | });
|
|---|
| 357 | }
|
|---|
| 358 | }
|
|---|
| 359 | }
|
|---|
| 360 |
|
|---|
| 361 | errorOut(err) {
|
|---|
| 362 | this.taskStatus = Task.runStatuses.ERROR;
|
|---|
| 363 | this._invocationChain.chainStatus = Task.runStatuses.ERROR;
|
|---|
| 364 | this.emit('error', err);
|
|---|
| 365 | }
|
|---|
| 366 |
|
|---|
| 367 | complete(val) {
|
|---|
| 368 |
|
|---|
| 369 | if (Array.isArray(this._waitForChains)) {
|
|---|
| 370 | let stillWaiting = this._waitForChains.some((chain) => {
|
|---|
| 371 | return !(chain.chainStatus == Task.runStatuses.DONE ||
|
|---|
| 372 | chain.chainStatus == Task.runStatuses.ERROR);
|
|---|
| 373 | });
|
|---|
| 374 | if (stillWaiting) {
|
|---|
| 375 | let now = (new Date()).getTime();
|
|---|
| 376 | let elapsed = now - this.startTime;
|
|---|
| 377 | if (elapsed > jake._taskTimeout) {
|
|---|
| 378 | return jake.fail(`Timed out waiting for task: ${this.name} with status of ${this.taskStatus}. Elapsed: ${elapsed}`);
|
|---|
| 379 | }
|
|---|
| 380 | setTimeout(() => {
|
|---|
| 381 | this.complete(val);
|
|---|
| 382 | }, POLLING_INTERVAL);
|
|---|
| 383 | return;
|
|---|
| 384 | }
|
|---|
| 385 | }
|
|---|
| 386 |
|
|---|
| 387 | jake._invocationChain.splice(jake._invocationChain.indexOf(this), 1);
|
|---|
| 388 |
|
|---|
| 389 | if (this._invocationChainRoot) {
|
|---|
| 390 | this._invocationChain.chainStatus = Task.runStatuses.DONE;
|
|---|
| 391 | }
|
|---|
| 392 |
|
|---|
| 393 | this._currentPrereqIndex = 0;
|
|---|
| 394 |
|
|---|
| 395 | // If 'complete' getting called because task has been
|
|---|
| 396 | // run already, value will not be passed -- leave in place
|
|---|
| 397 | if (!this._skipped) {
|
|---|
| 398 | this.taskStatus = Task.runStatuses.DONE;
|
|---|
| 399 | this.value = val;
|
|---|
| 400 |
|
|---|
| 401 | this.emit('complete', this.value);
|
|---|
| 402 | this.emit('_done');
|
|---|
| 403 |
|
|---|
| 404 | this.endTime = (new Date()).getTime();
|
|---|
| 405 | let taskTime = this.endTime - this.startTime;
|
|---|
| 406 |
|
|---|
| 407 | if (!(this._internal || jake.program.opts.quiet)) {
|
|---|
| 408 | jake.emit('finished', {
|
|---|
| 409 | name: this.fullName,
|
|---|
| 410 | task: this,
|
|---|
| 411 | time: taskTime,
|
|---|
| 412 | });
|
|---|
| 413 | console.log("Finished '" + pc.green(this.fullName) + "' after " + pc.magenta(taskTime + ' ms'));
|
|---|
| 414 | }
|
|---|
| 415 |
|
|---|
| 416 | }
|
|---|
| 417 | }
|
|---|
| 418 |
|
|---|
| 419 | _getFullName() {
|
|---|
| 420 | let ns = this.namespace;
|
|---|
| 421 | let path = (ns && ns.path) || '';
|
|---|
| 422 | path = (path && path.split(':')) || [];
|
|---|
| 423 | if (this.namespace !== jake.defaultNamespace) {
|
|---|
| 424 | path.push(this.namespace.name);
|
|---|
| 425 | }
|
|---|
| 426 | path.push(this.name);
|
|---|
| 427 | return path.join(':');
|
|---|
| 428 | }
|
|---|
| 429 |
|
|---|
| 430 | _getParams() {
|
|---|
| 431 | if (!this.action) return "";
|
|---|
| 432 | let params = (new RegExp('(?:'+this.action.name+'\\s*|^)\\s*\\((.*?)\\)').exec(this.action.toString().replace(/\n/g, '')) || [''])[1].replace(/\/\*.*?\*\//g, '').replace(/ /g, '');
|
|---|
| 433 | return params;
|
|---|
| 434 | }
|
|---|
| 435 |
|
|---|
| 436 | static getBaseNamespacePath(fullName) {
|
|---|
| 437 | return fullName.split(':').slice(0, -1).join(':');
|
|---|
| 438 | }
|
|---|
| 439 |
|
|---|
| 440 | static getBaseTaskName(fullName) {
|
|---|
| 441 | return fullName.split(':').pop();
|
|---|
| 442 | }
|
|---|
| 443 | }
|
|---|
| 444 |
|
|---|
| 445 | Task.runStatuses = {
|
|---|
| 446 | UNSTARTED: 'unstarted',
|
|---|
| 447 | DONE: 'done',
|
|---|
| 448 | STARTED: 'started',
|
|---|
| 449 | ERROR: 'error'
|
|---|
| 450 | };
|
|---|
| 451 |
|
|---|
| 452 | Task.ROOT_TASK_NAME = ROOT_TASK_NAME;
|
|---|
| 453 |
|
|---|
| 454 | exports.Task = Task;
|
|---|
| 455 |
|
|---|
| 456 | // Required here because circular deps
|
|---|
| 457 | require('../rule');
|
|---|
| 458 |
|
|---|