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.
		
		
		
		
		
			
		
			
				
					240 lines
				
				6.3 KiB
			
		
		
			
		
	
	
					240 lines
				
				6.3 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								module.exports.watch = watch;
							 | 
						||
| 
								 | 
							
								module.exports.resetWatchers = resetWatchers;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								var debug = require('debug')('nodemon:watch');
							 | 
						||
| 
								 | 
							
								var debugRoot = require('debug')('nodemon');
							 | 
						||
| 
								 | 
							
								var chokidar = require('chokidar');
							 | 
						||
| 
								 | 
							
								var undefsafe = require('undefsafe');
							 | 
						||
| 
								 | 
							
								var config = require('../config');
							 | 
						||
| 
								 | 
							
								var path = require('path');
							 | 
						||
| 
								 | 
							
								var utils = require('../utils');
							 | 
						||
| 
								 | 
							
								var bus = utils.bus;
							 | 
						||
| 
								 | 
							
								var match = require('./match');
							 | 
						||
| 
								 | 
							
								var watchers = [];
							 | 
						||
| 
								 | 
							
								var debouncedBus;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								bus.on('reset', resetWatchers);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function resetWatchers() {
							 | 
						||
| 
								 | 
							
								  debugRoot('resetting watchers');
							 | 
						||
| 
								 | 
							
								  watchers.forEach(function (watcher) {
							 | 
						||
| 
								 | 
							
								    watcher.close();
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								  watchers = [];
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function watch() {
							 | 
						||
| 
								 | 
							
								  if (watchers.length) {
							 | 
						||
| 
								 | 
							
								    debug('early exit on watch, still watching (%s)', watchers.length);
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  var dirs = [].slice.call(config.dirs);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  debugRoot('start watch on: %s', dirs.join(', '));
							 | 
						||
| 
								 | 
							
								  const rootIgnored = config.options.ignore;
							 | 
						||
| 
								 | 
							
								  debugRoot('ignored', rootIgnored);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  var watchedFiles = [];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const promise = new Promise(function (resolve) {
							 | 
						||
| 
								 | 
							
								    const dotFilePattern = /[/\\]\./;
							 | 
						||
| 
								 | 
							
								    var ignored = match.rulesToMonitor(
							 | 
						||
| 
								 | 
							
								      [], // not needed
							 | 
						||
| 
								 | 
							
								      Array.from(rootIgnored),
							 | 
						||
| 
								 | 
							
								      config
							 | 
						||
| 
								 | 
							
								    ).map(pattern => pattern.slice(1));
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const addDotFile = dirs.filter(dir => dir.match(dotFilePattern));
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // don't ignore dotfiles if explicitly watched.
							 | 
						||
| 
								 | 
							
								    if (addDotFile.length === 0) {
							 | 
						||
| 
								 | 
							
								      ignored.push(dotFilePattern);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    var watchOptions = {
							 | 
						||
| 
								 | 
							
								      ignorePermissionErrors: true,
							 | 
						||
| 
								 | 
							
								      ignored: ignored,
							 | 
						||
| 
								 | 
							
								      persistent: true,
							 | 
						||
| 
								 | 
							
								      usePolling: config.options.legacyWatch || false,
							 | 
						||
| 
								 | 
							
								      interval: config.options.pollingInterval,
							 | 
						||
| 
								 | 
							
								      // note to future developer: I've gone back and forth on adding `cwd`
							 | 
						||
| 
								 | 
							
								      // to the props and in some cases it fixes bugs but typically it causes
							 | 
						||
| 
								 | 
							
								      // bugs elsewhere (since nodemon is used is so many ways). the final
							 | 
						||
| 
								 | 
							
								      // decision is to *not* use it at all and work around it
							 | 
						||
| 
								 | 
							
								      // cwd: ...
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (utils.isWindows) {
							 | 
						||
| 
								 | 
							
								      watchOptions.disableGlobbing = true;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (process.env.TEST) {
							 | 
						||
| 
								 | 
							
								      watchOptions.useFsEvents = false;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    var watcher = chokidar.watch(
							 | 
						||
| 
								 | 
							
								      dirs,
							 | 
						||
| 
								 | 
							
								      Object.assign({}, watchOptions, config.options.watchOptions || {})
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    watcher.ready = false;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    var total = 0;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    watcher.on('change', filterAndRestart);
							 | 
						||
| 
								 | 
							
								    watcher.on('add', function (file) {
							 | 
						||
| 
								 | 
							
								      if (watcher.ready) {
							 | 
						||
| 
								 | 
							
								        return filterAndRestart(file);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      watchedFiles.push(file);
							 | 
						||
| 
								 | 
							
								      bus.emit('watching', file);
							 | 
						||
| 
								 | 
							
								      debug('chokidar watching: %s', file);
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								    watcher.on('ready', function () {
							 | 
						||
| 
								 | 
							
								      watchedFiles = Array.from(new Set(watchedFiles)); // ensure no dupes
							 | 
						||
| 
								 | 
							
								      total = watchedFiles.length;
							 | 
						||
| 
								 | 
							
								      watcher.ready = true;
							 | 
						||
| 
								 | 
							
								      resolve(total);
							 | 
						||
| 
								 | 
							
								      debugRoot('watch is complete');
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    watcher.on('error', function (error) {
							 | 
						||
| 
								 | 
							
								      if (error.code === 'EINVAL') {
							 | 
						||
| 
								 | 
							
								        utils.log.error(
							 | 
						||
| 
								 | 
							
								          'Internal watch failed. Likely cause: too many ' +
							 | 
						||
| 
								 | 
							
								          'files being watched (perhaps from the root of a drive?\n' +
							 | 
						||
| 
								 | 
							
								          'See https://github.com/paulmillr/chokidar/issues/229 for details'
							 | 
						||
| 
								 | 
							
								        );
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        utils.log.error('Internal watch failed: ' + error.message);
							 | 
						||
| 
								 | 
							
								        process.exit(1);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    watchers.push(watcher);
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return promise.catch(e => {
							 | 
						||
| 
								 | 
							
								    // this is a core error and it should break nodemon - so I have to break
							 | 
						||
| 
								 | 
							
								    // out of a promise using the setTimeout
							 | 
						||
| 
								 | 
							
								    setTimeout(() => {
							 | 
						||
| 
								 | 
							
								      throw e;
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								  }).then(function () {
							 | 
						||
| 
								 | 
							
								    utils.log.detail(`watching ${watchedFiles.length} file${
							 | 
						||
| 
								 | 
							
								      watchedFiles.length === 1 ? '' : 's'}`);
							 | 
						||
| 
								 | 
							
								    return watchedFiles;
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function filterAndRestart(files) {
							 | 
						||
| 
								 | 
							
								  if (!Array.isArray(files)) {
							 | 
						||
| 
								 | 
							
								    files = [files];
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (files.length) {
							 | 
						||
| 
								 | 
							
								    var cwd = process.cwd();
							 | 
						||
| 
								 | 
							
								    if (this.options && this.options.cwd) {
							 | 
						||
| 
								 | 
							
								      cwd = this.options.cwd;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    utils.log.detail(
							 | 
						||
| 
								 | 
							
								      'files triggering change check: ' +
							 | 
						||
| 
								 | 
							
								      files
							 | 
						||
| 
								 | 
							
								        .map(file => {
							 | 
						||
| 
								 | 
							
								          const res = path.relative(cwd, file);
							 | 
						||
| 
								 | 
							
								          return res;
							 | 
						||
| 
								 | 
							
								        })
							 | 
						||
| 
								 | 
							
								        .join(', ')
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // make sure the path is right and drop an empty
							 | 
						||
| 
								 | 
							
								    // filenames (sometimes on windows)
							 | 
						||
| 
								 | 
							
								    files = files.filter(Boolean).map(file => {
							 | 
						||
| 
								 | 
							
								      return path.relative(process.cwd(), path.relative(cwd, file));
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (utils.isWindows) {
							 | 
						||
| 
								 | 
							
								      // ensure the drive letter is in uppercase (c:\foo -> C:\foo)
							 | 
						||
| 
								 | 
							
								      files = files.map(f => {
							 | 
						||
| 
								 | 
							
								        if (f.indexOf(':') === -1) { return f; }
							 | 
						||
| 
								 | 
							
								        return f[0].toUpperCase() + f.slice(1);
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    debug('filterAndRestart on', files);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    var matched = match(
							 | 
						||
| 
								 | 
							
								      files,
							 | 
						||
| 
								 | 
							
								      config.options.monitor,
							 | 
						||
| 
								 | 
							
								      undefsafe(config, 'options.execOptions.ext')
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    debug('matched?', JSON.stringify(matched));
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // if there's no matches, then test to see if the changed file is the
							 | 
						||
| 
								 | 
							
								    // running script, if so, let's allow a restart
							 | 
						||
| 
								 | 
							
								    if (config.options.execOptions && config.options.execOptions.script) {
							 | 
						||
| 
								 | 
							
								      const script = path.resolve(config.options.execOptions.script);
							 | 
						||
| 
								 | 
							
								      if (matched.result.length === 0 && script) {
							 | 
						||
| 
								 | 
							
								        const length = script.length;
							 | 
						||
| 
								 | 
							
								        files.find(file => {
							 | 
						||
| 
								 | 
							
								          if (file.substr(-length, length) === script) {
							 | 
						||
| 
								 | 
							
								            matched = {
							 | 
						||
| 
								 | 
							
								              result: [file],
							 | 
						||
| 
								 | 
							
								              total: 1,
							 | 
						||
| 
								 | 
							
								            };
							 | 
						||
| 
								 | 
							
								            return true;
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    utils.log.detail(
							 | 
						||
| 
								 | 
							
								      'changes after filters (before/after): ' +
							 | 
						||
| 
								 | 
							
								      [files.length, matched.result.length].join('/')
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // reset the last check so we're only looking at recently modified files
							 | 
						||
| 
								 | 
							
								    config.lastStarted = Date.now();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (matched.result.length) {
							 | 
						||
| 
								 | 
							
								      if (config.options.delay > 0) {
							 | 
						||
| 
								 | 
							
								        utils.log.detail('delaying restart for ' + config.options.delay + 'ms');
							 | 
						||
| 
								 | 
							
								        if (debouncedBus === undefined) {
							 | 
						||
| 
								 | 
							
								          debouncedBus = debounce(restartBus, config.options.delay);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        debouncedBus(matched);
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        return restartBus(matched);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function restartBus(matched) {
							 | 
						||
| 
								 | 
							
								  utils.log.status('restarting due to changes...');
							 | 
						||
| 
								 | 
							
								  matched.result.map(file => {
							 | 
						||
| 
								 | 
							
								    utils.log.detail(path.relative(process.cwd(), file));
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (config.options.verbose) {
							 | 
						||
| 
								 | 
							
								    utils.log._log('');
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  bus.emit('restart', matched.result);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function debounce(fn, delay) {
							 | 
						||
| 
								 | 
							
								  var timer = null;
							 | 
						||
| 
								 | 
							
								  return function () {
							 | 
						||
| 
								 | 
							
								    const context = this;
							 | 
						||
| 
								 | 
							
								    const args = arguments;
							 | 
						||
| 
								 | 
							
								    clearTimeout(timer);
							 | 
						||
| 
								 | 
							
								    timer = setTimeout(() =>fn.apply(context, args), delay);
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								}
							 |