watchpack.js (10390B)
1 /* 2 MIT License http://www.opensource.org/licenses/mit-license.php 3 Author Tobias Koppers @sokra 4 */ 5 "use strict"; 6 7 const getWatcherManager = require("./getWatcherManager"); 8 const LinkResolver = require("./LinkResolver"); 9 const EventEmitter = require("events").EventEmitter; 10 const globToRegExp = require("glob-to-regexp"); 11 const watchEventSource = require("./watchEventSource"); 12 13 const EMPTY_ARRAY = []; 14 const EMPTY_OPTIONS = {}; 15 16 function addWatchersToSet(watchers, set) { 17 for (const ww of watchers) { 18 const w = ww.watcher; 19 if (!set.has(w.directoryWatcher)) { 20 set.add(w.directoryWatcher); 21 } 22 } 23 } 24 25 const stringToRegexp = ignored => { 26 const source = globToRegExp(ignored, { globstar: true, extended: true }) 27 .source; 28 const matchingStart = source.slice(0, source.length - 1) + "(?:$|\\/)"; 29 return matchingStart; 30 }; 31 32 const ignoredToFunction = ignored => { 33 if (Array.isArray(ignored)) { 34 const regexp = new RegExp(ignored.map(i => stringToRegexp(i)).join("|")); 35 return x => regexp.test(x.replace(/\\/g, "/")); 36 } else if (typeof ignored === "string") { 37 const regexp = new RegExp(stringToRegexp(ignored)); 38 return x => regexp.test(x.replace(/\\/g, "/")); 39 } else if (ignored instanceof RegExp) { 40 return x => ignored.test(x.replace(/\\/g, "/")); 41 } else if (ignored instanceof Function) { 42 return ignored; 43 } else if (ignored) { 44 throw new Error(`Invalid option for 'ignored': ${ignored}`); 45 } else { 46 return () => false; 47 } 48 }; 49 50 const normalizeOptions = options => { 51 return { 52 followSymlinks: !!options.followSymlinks, 53 ignored: ignoredToFunction(options.ignored), 54 poll: options.poll 55 }; 56 }; 57 58 const normalizeCache = new WeakMap(); 59 const cachedNormalizeOptions = options => { 60 const cacheEntry = normalizeCache.get(options); 61 if (cacheEntry !== undefined) return cacheEntry; 62 const normalized = normalizeOptions(options); 63 normalizeCache.set(options, normalized); 64 return normalized; 65 }; 66 67 class WatchpackFileWatcher { 68 constructor(watchpack, watcher, files) { 69 this.files = Array.isArray(files) ? files : [files]; 70 this.watcher = watcher; 71 watcher.on("initial-missing", type => { 72 for (const file of this.files) { 73 if (!watchpack._missing.has(file)) 74 watchpack._onRemove(file, file, type); 75 } 76 }); 77 watcher.on("change", (mtime, type) => { 78 for (const file of this.files) { 79 watchpack._onChange(file, mtime, file, type); 80 } 81 }); 82 watcher.on("remove", type => { 83 for (const file of this.files) { 84 watchpack._onRemove(file, file, type); 85 } 86 }); 87 } 88 89 update(files) { 90 if (!Array.isArray(files)) { 91 if (this.files.length !== 1) { 92 this.files = [files]; 93 } else if (this.files[0] !== files) { 94 this.files[0] = files; 95 } 96 } else { 97 this.files = files; 98 } 99 } 100 101 close() { 102 this.watcher.close(); 103 } 104 } 105 106 class WatchpackDirectoryWatcher { 107 constructor(watchpack, watcher, directories) { 108 this.directories = Array.isArray(directories) ? directories : [directories]; 109 this.watcher = watcher; 110 watcher.on("initial-missing", type => { 111 for (const item of this.directories) { 112 watchpack._onRemove(item, item, type); 113 } 114 }); 115 watcher.on("change", (file, mtime, type) => { 116 for (const item of this.directories) { 117 watchpack._onChange(item, mtime, file, type); 118 } 119 }); 120 watcher.on("remove", type => { 121 for (const item of this.directories) { 122 watchpack._onRemove(item, item, type); 123 } 124 }); 125 } 126 127 update(directories) { 128 if (!Array.isArray(directories)) { 129 if (this.directories.length !== 1) { 130 this.directories = [directories]; 131 } else if (this.directories[0] !== directories) { 132 this.directories[0] = directories; 133 } 134 } else { 135 this.directories = directories; 136 } 137 } 138 139 close() { 140 this.watcher.close(); 141 } 142 } 143 144 class Watchpack extends EventEmitter { 145 constructor(options) { 146 super(); 147 if (!options) options = EMPTY_OPTIONS; 148 this.options = options; 149 this.aggregateTimeout = 150 typeof options.aggregateTimeout === "number" 151 ? options.aggregateTimeout 152 : 200; 153 this.watcherOptions = cachedNormalizeOptions(options); 154 this.watcherManager = getWatcherManager(this.watcherOptions); 155 this.fileWatchers = new Map(); 156 this.directoryWatchers = new Map(); 157 this._missing = new Set(); 158 this.startTime = undefined; 159 this.paused = false; 160 this.aggregatedChanges = new Set(); 161 this.aggregatedRemovals = new Set(); 162 this.aggregateTimer = undefined; 163 this._onTimeout = this._onTimeout.bind(this); 164 } 165 166 watch(arg1, arg2, arg3) { 167 let files, directories, missing, startTime; 168 if (!arg2) { 169 ({ 170 files = EMPTY_ARRAY, 171 directories = EMPTY_ARRAY, 172 missing = EMPTY_ARRAY, 173 startTime 174 } = arg1); 175 } else { 176 files = arg1; 177 directories = arg2; 178 missing = EMPTY_ARRAY; 179 startTime = arg3; 180 } 181 this.paused = false; 182 const fileWatchers = this.fileWatchers; 183 const directoryWatchers = this.directoryWatchers; 184 const ignored = this.watcherOptions.ignored; 185 const filter = path => !ignored(path); 186 const addToMap = (map, key, item) => { 187 const list = map.get(key); 188 if (list === undefined) { 189 map.set(key, item); 190 } else if (Array.isArray(list)) { 191 list.push(item); 192 } else { 193 map.set(key, [list, item]); 194 } 195 }; 196 const fileWatchersNeeded = new Map(); 197 const directoryWatchersNeeded = new Map(); 198 const missingFiles = new Set(); 199 if (this.watcherOptions.followSymlinks) { 200 const resolver = new LinkResolver(); 201 for (const file of files) { 202 if (filter(file)) { 203 for (const innerFile of resolver.resolve(file)) { 204 if (file === innerFile || filter(innerFile)) { 205 addToMap(fileWatchersNeeded, innerFile, file); 206 } 207 } 208 } 209 } 210 for (const file of missing) { 211 if (filter(file)) { 212 for (const innerFile of resolver.resolve(file)) { 213 if (file === innerFile || filter(innerFile)) { 214 missingFiles.add(file); 215 addToMap(fileWatchersNeeded, innerFile, file); 216 } 217 } 218 } 219 } 220 for (const dir of directories) { 221 if (filter(dir)) { 222 let first = true; 223 for (const innerItem of resolver.resolve(dir)) { 224 if (filter(innerItem)) { 225 addToMap( 226 first ? directoryWatchersNeeded : fileWatchersNeeded, 227 innerItem, 228 dir 229 ); 230 } 231 first = false; 232 } 233 } 234 } 235 } else { 236 for (const file of files) { 237 if (filter(file)) { 238 addToMap(fileWatchersNeeded, file, file); 239 } 240 } 241 for (const file of missing) { 242 if (filter(file)) { 243 missingFiles.add(file); 244 addToMap(fileWatchersNeeded, file, file); 245 } 246 } 247 for (const dir of directories) { 248 if (filter(dir)) { 249 addToMap(directoryWatchersNeeded, dir, dir); 250 } 251 } 252 } 253 // Close unneeded old watchers 254 // and update existing watchers 255 for (const [key, w] of fileWatchers) { 256 const needed = fileWatchersNeeded.get(key); 257 if (needed === undefined) { 258 w.close(); 259 fileWatchers.delete(key); 260 } else { 261 w.update(needed); 262 fileWatchersNeeded.delete(key); 263 } 264 } 265 for (const [key, w] of directoryWatchers) { 266 const needed = directoryWatchersNeeded.get(key); 267 if (needed === undefined) { 268 w.close(); 269 directoryWatchers.delete(key); 270 } else { 271 w.update(needed); 272 directoryWatchersNeeded.delete(key); 273 } 274 } 275 // Create new watchers and install handlers on these watchers 276 watchEventSource.batch(() => { 277 for (const [key, files] of fileWatchersNeeded) { 278 const watcher = this.watcherManager.watchFile(key, startTime); 279 if (watcher) { 280 fileWatchers.set(key, new WatchpackFileWatcher(this, watcher, files)); 281 } 282 } 283 for (const [key, directories] of directoryWatchersNeeded) { 284 const watcher = this.watcherManager.watchDirectory(key, startTime); 285 if (watcher) { 286 directoryWatchers.set( 287 key, 288 new WatchpackDirectoryWatcher(this, watcher, directories) 289 ); 290 } 291 } 292 }); 293 this._missing = missingFiles; 294 this.startTime = startTime; 295 } 296 297 close() { 298 this.paused = true; 299 if (this.aggregateTimer) clearTimeout(this.aggregateTimer); 300 for (const w of this.fileWatchers.values()) w.close(); 301 for (const w of this.directoryWatchers.values()) w.close(); 302 this.fileWatchers.clear(); 303 this.directoryWatchers.clear(); 304 } 305 306 pause() { 307 this.paused = true; 308 if (this.aggregateTimer) clearTimeout(this.aggregateTimer); 309 } 310 311 getTimes() { 312 const directoryWatchers = new Set(); 313 addWatchersToSet(this.fileWatchers.values(), directoryWatchers); 314 addWatchersToSet(this.directoryWatchers.values(), directoryWatchers); 315 const obj = Object.create(null); 316 for (const w of directoryWatchers) { 317 const times = w.getTimes(); 318 for (const file of Object.keys(times)) obj[file] = times[file]; 319 } 320 return obj; 321 } 322 323 getTimeInfoEntries() { 324 const map = new Map(); 325 this.collectTimeInfoEntries(map, map); 326 return map; 327 } 328 329 collectTimeInfoEntries(fileTimestamps, directoryTimestamps) { 330 const allWatchers = new Set(); 331 addWatchersToSet(this.fileWatchers.values(), allWatchers); 332 addWatchersToSet(this.directoryWatchers.values(), allWatchers); 333 const safeTime = { value: 0 }; 334 for (const w of allWatchers) { 335 w.collectTimeInfoEntries(fileTimestamps, directoryTimestamps, safeTime); 336 } 337 } 338 339 getAggregated() { 340 if (this.aggregateTimer) { 341 clearTimeout(this.aggregateTimer); 342 this.aggregateTimer = undefined; 343 } 344 const changes = this.aggregatedChanges; 345 const removals = this.aggregatedRemovals; 346 this.aggregatedChanges = new Set(); 347 this.aggregatedRemovals = new Set(); 348 return { changes, removals }; 349 } 350 351 _onChange(item, mtime, file, type) { 352 file = file || item; 353 if (!this.paused) { 354 this.emit("change", file, mtime, type); 355 if (this.aggregateTimer) clearTimeout(this.aggregateTimer); 356 this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout); 357 } 358 this.aggregatedRemovals.delete(item); 359 this.aggregatedChanges.add(item); 360 } 361 362 _onRemove(item, file, type) { 363 file = file || item; 364 if (!this.paused) { 365 this.emit("remove", file, type); 366 if (this.aggregateTimer) clearTimeout(this.aggregateTimer); 367 this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout); 368 } 369 this.aggregatedChanges.delete(item); 370 this.aggregatedRemovals.add(item); 371 } 372 373 _onTimeout() { 374 this.aggregateTimer = undefined; 375 const changes = this.aggregatedChanges; 376 const removals = this.aggregatedRemovals; 377 this.aggregatedChanges = new Set(); 378 this.aggregatedRemovals = new Set(); 379 this.emit("aggregated", changes, removals); 380 } 381 } 382 383 module.exports = Watchpack;