You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

322 lines
9.6 KiB

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