grunt-contrib-watch/tasks/lib/taskrunner.js

337 lines
10 KiB
JavaScript

/*
* grunt-contrib-watch
* http://gruntjs.com/
*
* Copyright (c) 2013 "Cowboy" Ben Alman, contributors
* Licensed under the MIT license.
*/
'use strict';
var path = require('path');
var EE = require('events').EventEmitter;
var util = require('util');
// Track which targets to run after reload
var reloadTargets = [];
module.exports = function(grunt) {
var TaskRun = require('./taskrun')(grunt);
var livereload = require('./livereload')(grunt);
function Runner() {
EE.call(this);
// Name of the task
this.name = 'watch';
// Options for the runner
this.options = {};
// Function to close the task
this.done = function() {};
// Targets available to task run
this.targets = Object.create(null);
// The queue of task runs
this.queue = [];
// Whether we're actively running tasks
this.running = false;
// If a nospawn task has ran (and needs the watch to restart)
this.nospawn = false;
// Set to true before run() to reload task
this.reload = false;
// For re-queuing arguments with the task that originally ran this
this.nameArgs = [];
// A list of changed files to feed to task runs for livereload
this.changedFiles = Object.create(null);
}
util.inherits(Runner, EE);
// Init a task for taskrun
Runner.prototype.init = function init(name, defaults, done) {
var self = this;
self.name = name || grunt.task.current.name || 'watch';
self.options = self._options(grunt.config([self.name, 'options']) || {}, defaults || {});
self.reload = false;
self.nameArgs = (grunt.task.current.nameArgs) ? grunt.task.current.nameArgs : self.name;
// Function to call when closing the task
self.done = done || grunt.task.current.async();
// If a default livereload server for all targets
// Use task level unless target level overrides
var taskLRConfig = grunt.config([self.name, 'options', 'livereload']);
if (self.options.target && taskLRConfig) {
var targetLRConfig = grunt.config([self.name, self.options.target, 'options', 'livereload']);
if (targetLRConfig) {
// Dont use task level as target level will be used instead
taskLRConfig = false;
}
}
if (taskLRConfig) {
self.livereload = livereload(taskLRConfig);
}
// Return the targets normalized
var targets = self._getTargets(self.name);
if (self.running) {
// If previously running, complete the last run
self.complete();
} else if (reloadTargets.length > 0) {
// If not previously running but has items in the queue, needs run
self.queue = reloadTargets;
reloadTargets = [];
self.run();
} else {
// Check whether target's tasks should run at start w/ atBegin option
self.queue = targets.filter(function(tr) {
return tr.options.atBegin === true && tr.tasks.length > 0;
}).map(function(tr) {
return tr.name;
});
if (self.queue.length > 0) {
self.run();
}
}
return targets;
};
// Normalize targets from config
Runner.prototype._getTargets = function _getTargets(name) {
var self = this;
grunt.task.current.requiresConfig(name);
var config = grunt.config(name);
var onlyTarget = (self.options.target) ? self.options.target : false;
var targets = (onlyTarget ? [onlyTarget] : Object.keys(config)).filter(function(key) {
if (key === 'options') { return false; }
return typeof config[key] !== 'string' && !Array.isArray(config[key]);
}).map(function(target) {
// Fail if any required config properties have been omitted
grunt.task.current.requiresConfig([name, target, 'files']);
var cfg = grunt.config([name, target]);
cfg.name = target;
cfg.options = self._options(cfg.options || {}, self.options);
self.add(cfg);
return cfg;
}, self);
// Allow "basic" non-target format
if (typeof config.files === 'string' || Array.isArray(config.files)) {
var cfg = {
files: config.files,
tasks: config.tasks,
name: 'default',
options: self._options(config.options || {}, self.options),
};
targets.push(cfg);
self.add(cfg);
}
return targets;
};
// Default options
Runner.prototype._options = function _options() {
var args = Array.prototype.slice.call(arguments).concat({
// The cwd to spawn within
cwd: process.cwd(),
// Additional cli args to append when spawning
cliArgs: grunt.util._.without.apply(null, [[].slice.call(process.argv, 2)].concat(grunt.cli.tasks)),
interrupt: false,
nospawn: false,
spawn: true,
atBegin: false,
event: ['all'],
target: null,
});
return grunt.util._.defaults.apply(grunt.util._, args);
};
// Run the current queue of task runs
Runner.prototype.run = grunt.util._.debounce(function run() {
var self = this;
if (self.queue.length < 1) {
self.running = false;
return;
}
// Re-grab task options in case they changed between runs
self.options = self._options(grunt.config([self.name, 'options']) || {}, self.options);
// If we should interrupt
if (self.running === true) {
var shouldInterrupt = true;
self.queue.forEach(function(name) {
var tr = self.targets[name];
if (tr && tr.options.interrupt !== true) {
shouldInterrupt = false;
return false;
}
});
if (shouldInterrupt === true) {
self.interrupt();
} else {
// Dont interrupt the tasks running
return;
}
}
// If we should reload
if (self.reload) { return self.reloadTask(); }
// Trigger that tasks runs have started
self.emit('start');
self.running = true;
// Run each target
var shouldComplete = true;
grunt.util.async.forEachSeries(self.queue, function(name, next) {
var tr = self.targets[name];
if (!tr) { return next(); }
// Re-grab options in case they changed between runs
tr.options = self._options(grunt.config([self.name, name, 'options']) || {}, tr.options, self.options);
if (tr.options.spawn === false || tr.options.nospawn === true) {
shouldComplete = false;
}
tr.run(next);
}, function() {
if (shouldComplete) {
self.complete();
} else {
grunt.task.mark().run(self.nameArgs);
self.done();
}
});
}, 250);
// Push targets onto the queue
Runner.prototype.add = function add(target) {
if (!this.targets[target.name || 0]) {
var tr = new TaskRun(target);
// Add livereload to task runs
// Get directly from config as task level options are merged.
// We only want a single default LR server and then
// allow each target to override their own.
var lrconfig = grunt.config([this.name, target.name || 0, 'options', 'livereload']);
if (lrconfig) {
tr.livereload = livereload(lrconfig);
} else if (this.livereload && lrconfig !== false) {
tr.livereload = this.livereload;
}
return this.targets[tr.name] = tr;
}
return false;
};
// Do this when queued task runs have completed/scheduled
Runner.prototype.complete = function complete() {
var self = this;
if (self.running === false) { return; }
self.running = false;
var time = 0;
for (var i = 0, len = self.queue.length; i < len; ++i) {
var name = self.queue[i];
var target = self.targets[name];
if (!target) { return; }
if (target.startedAt !== false) {
time += target.complete();
self.queue.splice(i--, 1);
len--;
// if we're just livereloading and no tasks
// it can happen too fast and we dont report it
if (target.options.livereload && target.tasks.length < 1) {
time += 0.0001;
}
}
}
var elapsed = (time > 0) ? Number(time / 1000) : 0;
self.changedFiles = Object.create(null);
self.emit('end', elapsed);
};
// Run through completing every target in the queue
Runner.prototype._completeQueue = function _completeQueue() {
var self = this;
self.queue.forEach(function(name) {
var target = self.targets[name];
if (!target) { return; }
target.complete();
});
};
// Interrupt the running tasks
Runner.prototype.interrupt = function interrupt() {
var self = this;
self._completeQueue();
grunt.task.clearQueue();
self.emit('interrupt');
};
// Attempt to make this task run forever
Runner.prototype.forever = function forever() {
var self = this;
function rerun() {
// Clear queue and rerun to prevent failing
self._completeQueue();
grunt.task.clearQueue();
grunt.task.run(self.nameArgs);
self.running = false;
}
grunt.warn = grunt.fail.warn = function(e) {
var message = typeof e === 'string' ? e : e.message;
grunt.log.writeln(('Warning: ' + message).yellow);
if (!grunt.option('force')) {
rerun();
}
};
grunt.fatal = grunt.fail.fatal = function(e) {
var message = typeof e === 'string' ? e : e.message;
grunt.log.writeln(('Fatal error: ' + message).red);
rerun();
};
};
// Clear the require cache for all passed filepaths.
Runner.prototype.clearRequireCache = function() {
// If a non-string argument is passed, it's an array of filepaths, otherwise
// each filepath is passed individually.
var filepaths = typeof arguments[0] !== 'string' ? arguments[0] : grunt.util.toArray(arguments);
// For each filepath, clear the require cache, if necessary.
filepaths.forEach(function(filepath) {
var abspath = path.resolve(filepath);
if (require.cache[abspath]) {
grunt.verbose.write('Clearing require cache for "' + filepath + '" file...').ok();
delete require.cache[abspath];
}
});
};
// Reload this watch task, like when a Gruntfile is edited
Runner.prototype.reloadTask = function() {
var self = this;
// Which targets to run after reload
reloadTargets = self.queue;
self.emit('reload', reloadTargets);
// Re-init the watch task config
grunt.task.init([self.name]);
// Complete all running tasks
self._completeQueue();
// Run the watch task again
grunt.task.run(self.nameArgs);
self.done();
};
return new Runner();
};