watch.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. module.exports.watch = watch;
  2. module.exports.resetWatchers = resetWatchers;
  3. var debug = require('debug')('nodemon:watch');
  4. var debugRoot = require('debug')('nodemon');
  5. var chokidar = require('chokidar');
  6. var undefsafe = require('undefsafe');
  7. var config = require('../config');
  8. var path = require('path');
  9. var utils = require('../utils');
  10. var bus = utils.bus;
  11. var match = require('./match');
  12. var watchers = [];
  13. var debouncedBus;
  14. bus.on('reset', resetWatchers);
  15. function resetWatchers() {
  16. debugRoot('resetting watchers');
  17. watchers.forEach(function (watcher) {
  18. watcher.close();
  19. });
  20. watchers = [];
  21. }
  22. function watch() {
  23. if (watchers.length) {
  24. debug('early exit on watch, still watching (%s)', watchers.length);
  25. return;
  26. }
  27. var dirs = [].slice.call(config.dirs);
  28. debugRoot('start watch on: %s', dirs.join(', '));
  29. const rootIgnored = config.options.ignore;
  30. debugRoot('ignored', rootIgnored);
  31. var watchedFiles = [];
  32. const promise = new Promise(function (resolve) {
  33. const dotFilePattern = /[/\\]\./;
  34. var ignored = match.rulesToMonitor(
  35. [], // not needed
  36. Array.from(rootIgnored),
  37. config
  38. ).map(pattern => pattern.slice(1));
  39. const addDotFile = dirs.filter(dir => dir.match(dotFilePattern));
  40. // don't ignore dotfiles if explicitly watched.
  41. if (addDotFile.length === 0) {
  42. ignored.push(dotFilePattern);
  43. }
  44. var watchOptions = {
  45. ignorePermissionErrors: true,
  46. ignored: ignored,
  47. persistent: true,
  48. usePolling: config.options.legacyWatch || false,
  49. interval: config.options.pollingInterval,
  50. // note to future developer: I've gone back and forth on adding `cwd`
  51. // to the props and in some cases it fixes bugs but typically it causes
  52. // bugs elsewhere (since nodemon is used is so many ways). the final
  53. // decision is to *not* use it at all and work around it
  54. // cwd: ...
  55. };
  56. if (utils.isWindows) {
  57. watchOptions.disableGlobbing = true;
  58. }
  59. if (process.env.TEST) {
  60. watchOptions.useFsEvents = false;
  61. }
  62. var watcher = chokidar.watch(
  63. dirs,
  64. Object.assign({}, watchOptions, config.options.watchOptions || {})
  65. );
  66. watcher.ready = false;
  67. var total = 0;
  68. watcher.on('change', filterAndRestart);
  69. watcher.on('add', function (file) {
  70. if (watcher.ready) {
  71. return filterAndRestart(file);
  72. }
  73. watchedFiles.push(file);
  74. bus.emit('watching', file);
  75. debug('chokidar watching: %s', file);
  76. });
  77. watcher.on('ready', function () {
  78. watchedFiles = Array.from(new Set(watchedFiles)); // ensure no dupes
  79. total = watchedFiles.length;
  80. watcher.ready = true;
  81. resolve(total);
  82. debugRoot('watch is complete');
  83. });
  84. watcher.on('error', function (error) {
  85. if (error.code === 'EINVAL') {
  86. utils.log.error(
  87. 'Internal watch failed. Likely cause: too many ' +
  88. 'files being watched (perhaps from the root of a drive?\n' +
  89. 'See https://github.com/paulmillr/chokidar/issues/229 for details'
  90. );
  91. } else {
  92. utils.log.error('Internal watch failed: ' + error.message);
  93. process.exit(1);
  94. }
  95. });
  96. watchers.push(watcher);
  97. });
  98. return promise.catch(e => {
  99. // this is a core error and it should break nodemon - so I have to break
  100. // out of a promise using the setTimeout
  101. setTimeout(() => {
  102. throw e;
  103. });
  104. }).then(function () {
  105. utils.log.detail(`watching ${watchedFiles.length} file${
  106. watchedFiles.length === 1 ? '' : 's'}`);
  107. return watchedFiles;
  108. });
  109. }
  110. function filterAndRestart(files) {
  111. if (!Array.isArray(files)) {
  112. files = [files];
  113. }
  114. if (files.length) {
  115. var cwd = process.cwd();
  116. if (this.options && this.options.cwd) {
  117. cwd = this.options.cwd;
  118. }
  119. utils.log.detail(
  120. 'files triggering change check: ' +
  121. files
  122. .map(file => {
  123. const res = path.relative(cwd, file);
  124. return res;
  125. })
  126. .join(', ')
  127. );
  128. // make sure the path is right and drop an empty
  129. // filenames (sometimes on windows)
  130. files = files.filter(Boolean).map(file => {
  131. return path.relative(process.cwd(), path.relative(cwd, file));
  132. });
  133. if (utils.isWindows) {
  134. // ensure the drive letter is in uppercase (c:\foo -> C:\foo)
  135. files = files.map(f => {
  136. if (f.indexOf(':') === -1) { return f; }
  137. return f[0].toUpperCase() + f.slice(1);
  138. });
  139. }
  140. debug('filterAndRestart on', files);
  141. var matched = match(
  142. files,
  143. config.options.monitor,
  144. undefsafe(config, 'options.execOptions.ext')
  145. );
  146. debug('matched?', JSON.stringify(matched));
  147. // if there's no matches, then test to see if the changed file is the
  148. // running script, if so, let's allow a restart
  149. if (config.options.execOptions && config.options.execOptions.script) {
  150. const script = path.resolve(config.options.execOptions.script);
  151. if (matched.result.length === 0 && script) {
  152. const length = script.length;
  153. files.find(file => {
  154. if (file.substr(-length, length) === script) {
  155. matched = {
  156. result: [file],
  157. total: 1,
  158. };
  159. return true;
  160. }
  161. });
  162. }
  163. }
  164. utils.log.detail(
  165. 'changes after filters (before/after): ' +
  166. [files.length, matched.result.length].join('/')
  167. );
  168. // reset the last check so we're only looking at recently modified files
  169. config.lastStarted = Date.now();
  170. if (matched.result.length) {
  171. if (config.options.delay > 0) {
  172. utils.log.detail('delaying restart for ' + config.options.delay + 'ms');
  173. if (debouncedBus === undefined) {
  174. debouncedBus = debounce(restartBus, config.options.delay);
  175. }
  176. debouncedBus(matched);
  177. } else {
  178. return restartBus(matched);
  179. }
  180. }
  181. }
  182. }
  183. function restartBus(matched) {
  184. utils.log.status('restarting due to changes...');
  185. matched.result.map(file => {
  186. utils.log.detail(path.relative(process.cwd(), file));
  187. });
  188. if (config.options.verbose) {
  189. utils.log._log('');
  190. }
  191. bus.emit('restart', matched.result);
  192. }
  193. function debounce(fn, delay) {
  194. var timer = null;
  195. return function () {
  196. const context = this;
  197. const args = arguments;
  198. clearTimeout(timer);
  199. timer = setTimeout(() =>fn.apply(context, args), delay);
  200. };
  201. }