5 * Copyright (c) 2016 "Cowboy" Ben Alman, contributors
6 * Licensed under the MIT license.
11 var path = require('path');
12 var EE = require('events').EventEmitter;
13 var util = require('util');
14 var _ = require('lodash');
15 var async = require('async');
17 // Track which targets to run after reload
18 var reloadTargets = [];
20 // A default target name for config where targets are not used (keep this unique)
21 var defaultTargetName = '_$_default_$_';
23 module.exports = function(grunt) {
25 var TaskRun = require('./taskrun')(grunt);
26 var livereload = require('./livereload')(grunt);
32 // Options for the runner
34 // Function to close the task
35 this.done = function() {};
36 // Targets available to task run
37 this.targets = Object.create(null);
38 // The queue of task runs
40 // Whether we're actively running tasks
42 // If a nospawn task has ran (and needs the watch to restart)
44 // Set to true before run() to reload task
46 // For re-queuing arguments with the task that originally ran this
48 // A list of changed files to feed to task runs for livereload
49 this.changedFiles = Object.create(null);
51 util.inherits(Runner, EE);
53 // Init a task for taskrun
54 Runner.prototype.init = function init(name, defaults, done) {
57 self.name = name || grunt.task.current.name || 'watch';
58 self.options = self._options(grunt.config([self.name, 'options']) || {}, defaults || {});
60 self.nameArgs = grunt.task.current.nameArgs ? grunt.task.current.nameArgs : self.name;
62 // Normalize cwd option
63 if (typeof self.options.cwd === 'string') {
64 self.options.cwd = {files: self.options.cwd, spawn: self.options.cwd};
67 // Function to call when closing the task
68 self.done = done || grunt.task.current.async();
70 // If a default livereload server for all targets
71 // Use task level unless target level overrides
72 var taskLRConfig = grunt.config([self.name, 'options', 'livereload']);
73 if (self.options.target && taskLRConfig) {
74 var targetLRConfig = grunt.config([self.name, self.options.target, 'options', 'livereload']);
76 // Dont use task level as target level will be used instead
81 self.livereload = livereload(taskLRConfig);
84 // Return the targets normalized
85 var targets = self._getTargets(self.name);
88 // If previously running, complete the last run
90 } else if (reloadTargets.length > 0) {
91 // If not previously running but has items in the queue, needs run
92 self.queue = reloadTargets;
97 // Check whether target's tasks should run at start w/ atBegin option
98 self.queue = targets.filter(function(tr) {
99 return tr.options.atBegin === true && tr.tasks.length > 0;
100 }).map(function(tr) {
104 // There was an error in atBegin task, we can't re-run it, as this would
105 // create an infinite loop of failing tasks
106 // See https://github.com/gruntjs/grunt-contrib-watch/issues/169
108 self.hadError = false;
110 if (self.queue.length > 0) {
118 // Normalize targets from config
119 Runner.prototype._getTargets = function _getTargets(name) {
122 grunt.task.current.requiresConfig(name);
123 var config = grunt.config(name);
124 var onlyTarget = self.options.target ? self.options.target : false;
126 var targets = (onlyTarget ? [onlyTarget] : Object.keys(config)).filter(function(key) {
127 if (key === 'options') {
130 return typeof config[key] !== 'string' && !Array.isArray(config[key]);
131 }).map(function(target) {
132 // Fail if any required config properties have been omitted
133 grunt.task.current.requiresConfig([name, target, 'files']);
134 var cfg = grunt.config([name, target]);
136 cfg.options = self._options(cfg.options || {}, self.options);
141 // Allow "basic" non-target format
142 if (typeof config.files === 'string' || Array.isArray(config.files)) {
146 name: defaultTargetName,
147 options: self._options(config.options || {}, self.options)
157 Runner.prototype._options = function _options() {
158 var args = Array.prototype.slice.call(arguments).concat({
159 // The cwd to spawn within
161 // Additional cli args to append when spawning
162 cliArgs: _.without.apply(null, [[].slice.call(process.argv, 2)].concat(grunt.cli.tasks)),
170 return _.defaults.apply(_, args);
173 // Run the current queue of task runs
174 Runner.prototype.run = _.debounce(function run() {
176 if (self.queue.length < 1) {
177 self.running = false;
181 // Re-grab task options in case they changed between runs
182 self.options = self._options(grunt.config([self.name, 'options']) || {}, self.options);
184 // If we should interrupt
185 if (self.running === true) {
186 var shouldInterrupt = true;
187 self.queue.forEach(function(name) {
188 var tr = self.targets[name];
189 if (tr && tr.options.interrupt !== true) {
190 shouldInterrupt = false;
194 if (shouldInterrupt === true) {
197 // Dont interrupt the tasks running
202 // If we should reload
204 return self.reloadTask();
207 // Trigger that tasks runs have started
212 var shouldComplete = true;
213 async.forEachSeries(self.queue, function(name, next) {
214 var tr = self.targets[name];
219 // Re-grab options in case they changed between runs
220 tr.options = self._options(grunt.config([self.name, name, 'options']) || {}, tr.options, self.options);
222 if (tr.options.spawn === false || tr.options.nospawn === true) {
223 shouldComplete = false;
227 if (shouldComplete) {
230 grunt.task.mark().run(self.nameArgs);
236 // Push targets onto the queue
237 Runner.prototype.add = function add(target) {
239 if (!this.targets[target.name || 0]) {
241 // Private method for getting latest config for a watch target
242 target._getConfig = function(name) {
243 var cfgPath = [self.name];
244 if (target.name !== defaultTargetName) {
245 cfgPath.push(target.name);
250 return grunt.config(cfgPath);
253 // Create a new TaskRun instance
254 var tr = new TaskRun(target);
256 // Add livereload to task runs
257 // Get directly from config as task level options are merged.
258 // We only want a single default LR server and then
259 // allow each target to override their own.
260 var lrconfig = grunt.config([this.name, target.name || 0, 'options', 'livereload']);
262 tr.livereload = livereload(lrconfig);
263 } else if (this.livereload && lrconfig !== false) {
264 tr.livereload = this.livereload;
267 return this.targets[tr.name] = tr;
272 // Do this when queued task runs have completed/scheduled
273 Runner.prototype.complete = function complete() {
275 if (self.running === false) {
278 self.running = false;
280 for (var i = 0, len = self.queue.length; i < len; ++i) {
281 var name = self.queue[i];
282 var target = self.targets[name];
286 if (target.startedAt !== false) {
287 time += target.complete();
288 self.queue.splice(i--, 1);
291 // if we're just livereloading and no tasks
292 // it can happen too fast and we dont report it
293 if (target.options.livereload && target.tasks.length < 1) {
298 var elapsed = (time > 0) ? Number(time / 1000) : 0;
299 self.changedFiles = Object.create(null);
300 self.emit('end', elapsed);
303 // Run through completing every target in the queue
304 Runner.prototype._completeQueue = function _completeQueue() {
306 self.queue.forEach(function(name) {
307 var target = self.targets[name];
315 // Interrupt the running tasks
316 Runner.prototype.interrupt = function interrupt() {
318 self._completeQueue();
319 grunt.task.clearQueue();
320 self.emit('interrupt');
323 // Attempt to make this task run forever
324 Runner.prototype.forever = function forever() {
327 // Clear queue and rerun to prevent failing
328 self._completeQueue();
329 grunt.task.clearQueue();
330 grunt.task.run(self.nameArgs);
331 self.running = false;
332 // Mark that there was an error and we needed to rerun
333 self.hadError = true;
335 grunt.fail.forever_warncount = 0;
336 grunt.fail.forever_errorcount = 0;
337 grunt.warn = grunt.fail.warn = function(e) {
338 grunt.fail.forever_warncount ++;
339 var message = typeof e === 'string' ? e : e.message;
340 grunt.log.writeln(('Warning: ' + message).yellow);
341 if (!grunt.option('force')) {
345 grunt.fatal = grunt.fail.fatal = function(e) {
346 grunt.fail.forever_errorcount ++;
347 var message = typeof e === 'string' ? e : e.message;
348 grunt.log.writeln(('Fatal error: ' + message).red);
353 // Clear the require cache for all passed filepaths.
354 Runner.prototype.clearRequireCache = function() {
355 // If a non-string argument is passed, it's an array of filepaths, otherwise
356 // each filepath is passed individually.
357 var filepaths = typeof arguments[0] !== 'string' ? arguments[0] : Array.prototype.slice(arguments);
358 // For each filepath, clear the require cache, if necessary.
359 filepaths.forEach(function(filepath) {
360 var abspath = path.resolve(filepath);
361 if (require.cache[abspath]) {
362 grunt.verbose.write('Clearing require cache for "' + filepath + '" file...').ok();
363 delete require.cache[abspath];
368 // Reload this watch task, like when a Gruntfile is edited
369 Runner.prototype.reloadTask = function() {
371 // Which targets to run after reload
372 reloadTargets = self.queue;
373 self.emit('reload', reloadTargets);
375 // Re-init the watch task config
376 grunt.task.init([self.name]);
378 // Complete all running tasks
379 self._completeQueue();
381 // Run the watch task again
382 grunt.task.run(self.nameArgs);