388 lines
12 KiB
JavaScript
388 lines
12 KiB
JavaScript
/*
|
|
* grunt-contrib-watch
|
|
* http://gruntjs.com/
|
|
*
|
|
* Copyright (c) 2018 "Cowboy" Ben Alman, contributors
|
|
* Licensed under the MIT license.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var path = require('path');
|
|
var EE = require('events').EventEmitter;
|
|
var util = require('util');
|
|
var _ = require('lodash');
|
|
var async = require('async');
|
|
|
|
// Track which targets to run after reload
|
|
var reloadTargets = [];
|
|
|
|
// A default target name for config where targets are not used (keep this unique)
|
|
var defaultTargetName = '_$_default_$_';
|
|
|
|
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;
|
|
|
|
// Normalize cwd option
|
|
if (typeof self.options.cwd === 'string') {
|
|
self.options.cwd = {files: self.options.cwd, spawn: self.options.cwd};
|
|
}
|
|
|
|
// 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 {
|
|
if (!self.hadError) {
|
|
// 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;
|
|
});
|
|
} else {
|
|
// There was an error in atBegin task, we can't re-run it, as this would
|
|
// create an infinite loop of failing tasks
|
|
// See https://github.com/gruntjs/grunt-contrib-watch/issues/169
|
|
self.queue = [];
|
|
self.hadError = false;
|
|
}
|
|
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: defaultTargetName,
|
|
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: _.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 _.defaults.apply(_, args);
|
|
};
|
|
|
|
// Run the current queue of task runs
|
|
Runner.prototype.run = _.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;
|
|
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) {
|
|
var self = this;
|
|
if (!this.targets[target.name || 0]) {
|
|
|
|
// Private method for getting latest config for a watch target
|
|
target._getConfig = function(name) {
|
|
var cfgPath = [self.name];
|
|
if (target.name !== defaultTargetName) {
|
|
cfgPath.push(target.name);
|
|
}
|
|
if (name) {
|
|
cfgPath.push(name);
|
|
}
|
|
return grunt.config(cfgPath);
|
|
};
|
|
|
|
// Create a new TaskRun instance
|
|
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;
|
|
// Mark that there was an error and we needed to rerun
|
|
self.hadError = true;
|
|
}
|
|
grunt.fail.forever_warncount = 0;
|
|
grunt.fail.forever_errorcount = 0;
|
|
grunt.warn = grunt.fail.warn = function(e) {
|
|
grunt.fail.forever_warncount ++;
|
|
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) {
|
|
grunt.fail.forever_errorcount ++;
|
|
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] : Array.prototype.slice(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();
|
|
};
|