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.
244 lines
6.4 KiB
244 lines
6.4 KiB
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 (utils.isIBMi) { |
|
watchOptions.usePolling = 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('unlink', 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); |
|
}; |
|
}
|
|
|