/* * 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(); };