Spaces:
Sleeping
Sleeping
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); | |
}; | |
} | |