source: Dev/trunk/node_modules/grunt-contrib-watch/tasks/lib/taskrunner.js

Last change on this file was 533, checked in by hendrikvanantwerpen, 10 years ago

Add watch command to ease client development.

File size: 11.0 KB
Line 
1/*
2 * grunt-contrib-watch
3 * http://gruntjs.com/
4 *
5 * Copyright (c) 2014 "Cowboy" Ben Alman, contributors
6 * Licensed under the MIT license.
7 */
8
9'use strict';
10
11var path = require('path');
12var EE = require('events').EventEmitter;
13var util = require('util');
14var _ = require('lodash');
15var async = require('async');
16
17// Track which targets to run after reload
18var reloadTargets = [];
19
20// A default target name for config where targets are not used (keep this unique)
21var defaultTargetName = '_$_default_$_';
22
23module.exports = function(grunt) {
24
25  var TaskRun = require('./taskrun')(grunt);
26  var livereload = require('./livereload')(grunt);
27
28  function Runner() {
29    EE.call(this);
30    // Name of the task
31    this.name = 'watch';
32    // Options for the runner
33    this.options = {};
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
39    this.queue = [];
40    // Whether we're actively running tasks
41    this.running = false;
42    // If a nospawn task has ran (and needs the watch to restart)
43    this.nospawn = false;
44    // Set to true before run() to reload task
45    this.reload = false;
46    // For re-queuing arguments with the task that originally ran this
47    this.nameArgs = [];
48    // A list of changed files to feed to task runs for livereload
49    this.changedFiles = Object.create(null);
50  }
51  util.inherits(Runner, EE);
52
53  // Init a task for taskrun
54  Runner.prototype.init = function init(name, defaults, done) {
55    var self = this;
56
57    self.name = name || grunt.task.current.name || 'watch';
58    self.options = self._options(grunt.config([self.name, 'options']) || {}, defaults || {});
59    self.reload = false;
60    self.nameArgs = (grunt.task.current.nameArgs) ? grunt.task.current.nameArgs : self.name;
61
62    // Normalize cwd option
63    if (typeof self.options.cwd === 'string') {
64      self.options.cwd = { files: self.options.cwd, spawn: self.options.cwd };
65    }
66
67    // Function to call when closing the task
68    self.done = done || grunt.task.current.async();
69
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']);
75      if (targetLRConfig) {
76        // Dont use task level as target level will be used instead
77        taskLRConfig = false;
78      }
79    }
80    if (taskLRConfig) {
81      self.livereload = livereload(taskLRConfig);
82    }
83
84    // Return the targets normalized
85    var targets = self._getTargets(self.name);
86
87    if (self.running) {
88      // If previously running, complete the last run
89      self.complete();
90    } else if (reloadTargets.length > 0) {
91      // If not previously running but has items in the queue, needs run
92      self.queue = reloadTargets;
93      reloadTargets = [];
94      self.run();
95    } else {
96      // Check whether target's tasks should run at start w/ atBegin option
97      self.queue = targets.filter(function(tr) {
98        return tr.options.atBegin === true && tr.tasks.length > 0;
99      }).map(function(tr) {
100        return tr.name;
101      });
102      if (self.queue.length > 0) {
103        self.run();
104      }
105    }
106
107    return targets;
108  };
109
110  // Normalize targets from config
111  Runner.prototype._getTargets = function _getTargets(name) {
112    var self = this;
113
114    grunt.task.current.requiresConfig(name);
115    var config = grunt.config(name);
116    var onlyTarget = (self.options.target) ? self.options.target : false;
117
118    var targets = (onlyTarget ? [onlyTarget] : Object.keys(config)).filter(function(key) {
119      if (key === 'options') { return false; }
120      return typeof config[key] !== 'string' && !Array.isArray(config[key]);
121    }).map(function(target) {
122      // Fail if any required config properties have been omitted
123      grunt.task.current.requiresConfig([name, target, 'files']);
124      var cfg = grunt.config([name, target]);
125      cfg.name = target;
126      cfg.options = self._options(cfg.options || {}, self.options);
127      self.add(cfg);
128      return cfg;
129    }, self);
130
131    // Allow "basic" non-target format
132    if (typeof config.files === 'string' || Array.isArray(config.files)) {
133      var cfg = {
134        files: config.files,
135        tasks: config.tasks,
136        name: defaultTargetName,
137        options: self._options(config.options || {}, self.options),
138      };
139      targets.push(cfg);
140      self.add(cfg);
141    }
142
143    return targets;
144  };
145
146  // Default options
147  Runner.prototype._options = function _options() {
148    var args = Array.prototype.slice.call(arguments).concat({
149      // The cwd to spawn within
150      cwd: process.cwd(),
151      // Additional cli args to append when spawning
152      cliArgs: _.without.apply(null, [[].slice.call(process.argv, 2)].concat(grunt.cli.tasks)),
153      interrupt: false,
154      nospawn: false,
155      spawn: true,
156      atBegin: false,
157      event: ['all'],
158      target: null,
159    });
160    return _.defaults.apply(_, args);
161  };
162
163  // Run the current queue of task runs
164  Runner.prototype.run = _.debounce(function run() {
165    var self = this;
166    if (self.queue.length < 1) {
167      self.running = false;
168      return;
169    }
170
171    // Re-grab task options in case they changed between runs
172    self.options = self._options(grunt.config([self.name, 'options']) || {}, self.options);
173
174    // If we should interrupt
175    if (self.running === true) {
176      var shouldInterrupt = true;
177      self.queue.forEach(function(name) {
178        var tr = self.targets[name];
179        if (tr && tr.options.interrupt !== true) {
180          shouldInterrupt = false;
181          return false;
182        }
183      });
184      if (shouldInterrupt === true) {
185        self.interrupt();
186      } else {
187        // Dont interrupt the tasks running
188        return;
189      }
190    }
191
192    // If we should reload
193    if (self.reload) { return self.reloadTask(); }
194
195    // Trigger that tasks runs have started
196    self.emit('start');
197    self.running = true;
198
199    // Run each target
200    var shouldComplete = true;
201    async.forEachSeries(self.queue, function(name, next) {
202      var tr = self.targets[name];
203      if (!tr) { return next(); }
204
205      // Re-grab options in case they changed between runs
206      tr.options = self._options(grunt.config([self.name, name, 'options']) || {}, tr.options, self.options);
207
208      if (tr.options.spawn === false || tr.options.nospawn === true) {
209        shouldComplete = false;
210      }
211      tr.run(next);
212    }, function() {
213      if (shouldComplete) {
214        self.complete();
215      } else {
216        grunt.task.mark().run(self.nameArgs);
217        self.done();
218      }
219    });
220  }, 250);
221
222  // Push targets onto the queue
223  Runner.prototype.add = function add(target) {
224    var self = this;
225    if (!this.targets[target.name || 0]) {
226
227      // Private method for getting latest config for a watch target
228      target._getConfig = function(name) {
229        var cfgPath = [self.name];
230        if (target.name !== defaultTargetName) { cfgPath.push(target.name); }
231        if (name) { cfgPath.push(name); }
232        return grunt.config(cfgPath);
233      };
234
235      // Create a new TaskRun instance
236      var tr = new TaskRun(target);
237
238      // Add livereload to task runs
239      // Get directly from config as task level options are merged.
240      // We only want a single default LR server and then
241      // allow each target to override their own.
242      var lrconfig = grunt.config([this.name, target.name || 0, 'options', 'livereload']);
243      if (lrconfig) {
244        tr.livereload = livereload(lrconfig);
245      } else if (this.livereload && lrconfig !== false) {
246        tr.livereload = this.livereload;
247      }
248
249      return this.targets[tr.name] = tr;
250    }
251    return false;
252  };
253
254  // Do this when queued task runs have completed/scheduled
255  Runner.prototype.complete = function complete() {
256    var self = this;
257    if (self.running === false) { return; }
258    self.running = false;
259    var time = 0;
260    for (var i = 0, len = self.queue.length; i < len; ++i) {
261      var name = self.queue[i];
262      var target = self.targets[name];
263      if (!target) { return; }
264      if (target.startedAt !== false) {
265        time += target.complete();
266        self.queue.splice(i--, 1);
267        len--;
268
269        // if we're just livereloading and no tasks
270        // it can happen too fast and we dont report it
271        if (target.options.livereload && target.tasks.length < 1) {
272          time += 0.0001;
273        }
274      }
275    }
276    var elapsed = (time > 0) ? Number(time / 1000) : 0;
277    self.changedFiles = Object.create(null);
278    self.emit('end', elapsed);
279  };
280
281  // Run through completing every target in the queue
282  Runner.prototype._completeQueue = function _completeQueue() {
283    var self = this;
284    self.queue.forEach(function(name) {
285      var target = self.targets[name];
286      if (!target) { return; }
287      target.complete();
288    });
289  };
290
291  // Interrupt the running tasks
292  Runner.prototype.interrupt = function interrupt() {
293    var self = this;
294    self._completeQueue();
295    grunt.task.clearQueue();
296    self.emit('interrupt');
297  };
298
299  // Attempt to make this task run forever
300  Runner.prototype.forever = function forever() {
301    var self = this;
302    function rerun() {
303      // Clear queue and rerun to prevent failing
304      self._completeQueue();
305      grunt.task.clearQueue();
306      grunt.task.run(self.nameArgs);
307      self.running = false;
308    }
309    grunt.fail.forever_warncount = 0;
310    grunt.fail.forever_errorcount = 0;
311    grunt.warn = grunt.fail.warn = function(e) {
312      grunt.fail.forever_warncount ++;
313      var message = typeof e === 'string' ? e : e.message;
314      grunt.log.writeln(('Warning: ' + message).yellow);
315      if (!grunt.option('force')) {
316        rerun();
317      }
318    };
319    grunt.fatal = grunt.fail.fatal = function(e) {
320      grunt.fail.forever_errorcount ++;
321      var message = typeof e === 'string' ? e : e.message;
322      grunt.log.writeln(('Fatal error: ' + message).red);
323      rerun();
324    };
325  };
326
327  // Clear the require cache for all passed filepaths.
328  Runner.prototype.clearRequireCache = function() {
329    // If a non-string argument is passed, it's an array of filepaths, otherwise
330    // each filepath is passed individually.
331    var filepaths = typeof arguments[0] !== 'string' ? arguments[0] : Array.prototype.slice(arguments);
332    // For each filepath, clear the require cache, if necessary.
333    filepaths.forEach(function(filepath) {
334      var abspath = path.resolve(filepath);
335      if (require.cache[abspath]) {
336        grunt.verbose.write('Clearing require cache for "' + filepath + '" file...').ok();
337        delete require.cache[abspath];
338      }
339    });
340  };
341
342  // Reload this watch task, like when a Gruntfile is edited
343  Runner.prototype.reloadTask = function() {
344    var self = this;
345    // Which targets to run after reload
346    reloadTargets = self.queue;
347    self.emit('reload', reloadTargets);
348
349    // Re-init the watch task config
350    grunt.task.init([self.name]);
351
352    // Complete all running tasks
353    self._completeQueue();
354
355    // Run the watch task again
356    grunt.task.run(self.nameArgs);
357    self.done();
358  };
359
360  return new Runner();
361};
Note: See TracBrowser for help on using the repository browser.