taskrunner.js 11 KB

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