240 lines
6.3 KiB
JavaScript
240 lines
6.3 KiB
JavaScript
|
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);
|
||
|
};
|
||
|
}
|