123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- /*
- * grunt-contrib-watch
- * http://gruntjs.com/
- *
- * Copyright (c) 2014 "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 {
- // 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: 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;
- }
- 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();
- };
|