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