|
|
/* * 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); // Flag to set if interrupt is false and a change happened during a task run
this.needsAnotherRun = false; } 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 = grunt.task.current.options(defaults || {}, { // 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)), }); 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); }
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(); }
// Return the targets normalized
return self._getTargets(self.name); };
// 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 = grunt.util._.defaults(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: grunt.util._.defaults(config.options || {}, self.options), }; targets.push(cfg); self.add(cfg); }
return targets; };
// 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; }
// 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
self.needsAnotherRun = true; 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(); } if (tr.options.nospawn) { shouldComplete = false; } tr.changedFiles = self.changedFiles; tr.run(next); }, function() { if (shouldComplete) { self.complete(); } else { grunt.task.mark().run(self.nameArgs); self.done(); } }); }, 250);
// Queue target names for running
Runner.prototype.queue = function queue(names) { var self = this; if (typeof names === 'string') { names = [names]; } names.forEach(function(name) { if (self._queue.indexOf(name) === -1) { self._queue.push(name); } }); return self._queue; };
// Push targets onto the queue
Runner.prototype.add = function add(target) { if (!this._targets[target.name || 0]) { var tr = new TaskRun(target, this.options);
// 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) { 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; self._queue.forEach(function(name, i) { var target = self._targets[name]; if (!target) { return; } if (target.startedAt !== false) { time += target.complete(); if (self.needsAnotherRun !== true) { self._queue[i] = null; } } });
// If it needs another run
if (self.needsAnotherRun === true) { self.needsAnotherRun = false; self.run(); return; }
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(); };
|