/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const fs = require("fs"); const path = require("path"); const { EventEmitter } = require("events"); const reducePlan = require("./reducePlan"); const IS_OSX = require("os").platform() === "darwin"; const IS_WIN = require("os").platform() === "win32"; const SUPPORTS_RECURSIVE_WATCHING = IS_OSX || IS_WIN; const watcherLimit = +process.env.WATCHPACK_WATCHER_LIMIT || (IS_OSX ? 2000 : 10000); const recursiveWatcherLogging = !!process.env .WATCHPACK_RECURSIVE_WATCHER_LOGGING; let isBatch = false; let watcherCount = 0; /** @type {Map} */ const pendingWatchers = new Map(); /** @type {Map} */ const recursiveWatchers = new Map(); /** @type {Map} */ const directWatchers = new Map(); /** @type {Map} */ const underlyingWatcher = new Map(); class DirectWatcher { constructor(filePath) { this.filePath = filePath; this.watchers = new Set(); this.watcher = undefined; try { const watcher = fs.watch(filePath); this.watcher = watcher; watcher.on("change", (type, filename) => { for (const w of this.watchers) { w.emit("change", type, filename); } }); watcher.on("error", error => { for (const w of this.watchers) { w.emit("error", error); } }); } catch (err) { process.nextTick(() => { for (const w of this.watchers) { w.emit("error", err); } }); } watcherCount++; } add(watcher) { underlyingWatcher.set(watcher, this); this.watchers.add(watcher); } remove(watcher) { this.watchers.delete(watcher); if (this.watchers.size === 0) { directWatchers.delete(this.filePath); watcherCount--; if (this.watcher) this.watcher.close(); } } getWatchers() { return this.watchers; } } class RecursiveWatcher { constructor(rootPath) { this.rootPath = rootPath; /** @type {Map} */ this.mapWatcherToPath = new Map(); /** @type {Map>} */ this.mapPathToWatchers = new Map(); this.watcher = undefined; try { const watcher = fs.watch(rootPath, { recursive: true }); this.watcher = watcher; watcher.on("change", (type, filename) => { if (!filename) { if (recursiveWatcherLogging) { process.stderr.write( `[watchpack] dispatch ${type} event in recursive watcher (${ this.rootPath }) to all watchers\n` ); } for (const w of this.mapWatcherToPath.keys()) { w.emit("change", type); } } else { const dir = path.dirname(filename); const watchers = this.mapPathToWatchers.get(dir); if (recursiveWatcherLogging) { process.stderr.write( `[watchpack] dispatch ${type} event in recursive watcher (${ this.rootPath }) for '${filename}' to ${ watchers ? watchers.size : 0 } watchers\n` ); } if (watchers === undefined) return; for (const w of watchers) { w.emit("change", type, path.basename(filename)); } } }); watcher.on("error", error => { for (const w of this.mapWatcherToPath.keys()) { w.emit("error", error); } }); } catch (err) { process.nextTick(() => { for (const w of this.mapWatcherToPath.keys()) { w.emit("error", err); } }); } watcherCount++; if (recursiveWatcherLogging) { process.stderr.write( `[watchpack] created recursive watcher at ${rootPath}\n` ); } } add(filePath, watcher) { underlyingWatcher.set(watcher, this); const subpath = filePath.slice(this.rootPath.length + 1) || "."; this.mapWatcherToPath.set(watcher, subpath); const set = this.mapPathToWatchers.get(subpath); if (set === undefined) { const newSet = new Set(); newSet.add(watcher); this.mapPathToWatchers.set(subpath, newSet); } else { set.add(watcher); } } remove(watcher) { const subpath = this.mapWatcherToPath.get(watcher); if (!subpath) return; this.mapWatcherToPath.delete(watcher); const set = this.mapPathToWatchers.get(subpath); set.delete(watcher); if (set.size === 0) { this.mapPathToWatchers.delete(subpath); } if (this.mapWatcherToPath.size === 0) { recursiveWatchers.delete(this.rootPath); watcherCount--; if (this.watcher) this.watcher.close(); if (recursiveWatcherLogging) { process.stderr.write( `[watchpack] closed recursive watcher at ${this.rootPath}\n` ); } } } getWatchers() { return this.mapWatcherToPath; } } class Watcher extends EventEmitter { close() { if (pendingWatchers.has(this)) { pendingWatchers.delete(this); return; } const watcher = underlyingWatcher.get(this); watcher.remove(this); underlyingWatcher.delete(this); } } const createDirectWatcher = filePath => { const existing = directWatchers.get(filePath); if (existing !== undefined) return existing; const w = new DirectWatcher(filePath); directWatchers.set(filePath, w); return w; }; const createRecursiveWatcher = rootPath => { const existing = recursiveWatchers.get(rootPath); if (existing !== undefined) return existing; const w = new RecursiveWatcher(rootPath); recursiveWatchers.set(rootPath, w); return w; }; const execute = () => { /** @type {Map} */ const map = new Map(); const addWatcher = (watcher, filePath) => { const entry = map.get(filePath); if (entry === undefined) { map.set(filePath, watcher); } else if (Array.isArray(entry)) { entry.push(watcher); } else { map.set(filePath, [entry, watcher]); } }; for (const [watcher, filePath] of pendingWatchers) { addWatcher(watcher, filePath); } pendingWatchers.clear(); // Fast case when we are not reaching the limit if (!SUPPORTS_RECURSIVE_WATCHING || watcherLimit - watcherCount >= map.size) { // Create watchers for all entries in the map for (const [filePath, entry] of map) { const w = createDirectWatcher(filePath); if (Array.isArray(entry)) { for (const item of entry) w.add(item); } else { w.add(entry); } } return; } // Reconsider existing watchers to improving watch plan for (const watcher of recursiveWatchers.values()) { for (const [w, subpath] of watcher.getWatchers()) { addWatcher(w, path.join(watcher.rootPath, subpath)); } } for (const watcher of directWatchers.values()) { for (const w of watcher.getWatchers()) { addWatcher(w, watcher.filePath); } } // Merge map entries to keep watcher limit // Create a 10% buffer to be able to enter fast case more often const plan = reducePlan(map, watcherLimit * 0.9); // Update watchers for all entries in the map for (const [filePath, entry] of plan) { if (entry.size === 1) { for (const [watcher, filePath] of entry) { const w = createDirectWatcher(filePath); const old = underlyingWatcher.get(watcher); if (old === w) continue; w.add(watcher); if (old !== undefined) old.remove(watcher); } } else { const filePaths = new Set(entry.values()); if (filePaths.size > 1) { const w = createRecursiveWatcher(filePath); for (const [watcher, watcherPath] of entry) { const old = underlyingWatcher.get(watcher); if (old === w) continue; w.add(watcherPath, watcher); if (old !== undefined) old.remove(watcher); } } else { for (const filePath of filePaths) { const w = createDirectWatcher(filePath); for (const watcher of entry.keys()) { const old = underlyingWatcher.get(watcher); if (old === w) continue; w.add(watcher); if (old !== undefined) old.remove(watcher); } } } } } }; exports.watch = filePath => { const watcher = new Watcher(); // Find an existing watcher const directWatcher = directWatchers.get(filePath); if (directWatcher !== undefined) { directWatcher.add(watcher); return watcher; } let current = filePath; for (;;) { const recursiveWatcher = recursiveWatchers.get(current); if (recursiveWatcher !== undefined) { recursiveWatcher.add(filePath, watcher); return watcher; } const parent = path.dirname(current); if (parent === current) break; current = parent; } // Queue up watcher for creation pendingWatchers.set(watcher, filePath); if (!isBatch) execute(); return watcher; }; exports.batch = fn => { isBatch = true; try { fn(); } finally { isBatch = false; execute(); } }; exports.getNumberOfWatchers = () => { return watcherCount; };