You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			384 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			384 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
| /*
 | |
| 	MIT License http://www.opensource.org/licenses/mit-license.php
 | |
| 	Author Tobias Koppers @sokra
 | |
| */
 | |
| "use strict";
 | |
| 
 | |
| const getWatcherManager = require("./getWatcherManager");
 | |
| const LinkResolver = require("./LinkResolver");
 | |
| const EventEmitter = require("events").EventEmitter;
 | |
| const globToRegExp = require("glob-to-regexp");
 | |
| const watchEventSource = require("./watchEventSource");
 | |
| 
 | |
| const EMPTY_ARRAY = [];
 | |
| const EMPTY_OPTIONS = {};
 | |
| 
 | |
| function addWatchersToSet(watchers, set) {
 | |
| 	for (const ww of watchers) {
 | |
| 		const w = ww.watcher;
 | |
| 		if (!set.has(w.directoryWatcher)) {
 | |
| 			set.add(w.directoryWatcher);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| const stringToRegexp = ignored => {
 | |
| 	const source = globToRegExp(ignored, { globstar: true, extended: true })
 | |
| 		.source;
 | |
| 	const matchingStart = source.slice(0, source.length - 1) + "(?:$|\\/)";
 | |
| 	return matchingStart;
 | |
| };
 | |
| 
 | |
| const ignoredToFunction = ignored => {
 | |
| 	if (Array.isArray(ignored)) {
 | |
| 		const regexp = new RegExp(ignored.map(i => stringToRegexp(i)).join("|"));
 | |
| 		return x => regexp.test(x.replace(/\\/g, "/"));
 | |
| 	} else if (typeof ignored === "string") {
 | |
| 		const regexp = new RegExp(stringToRegexp(ignored));
 | |
| 		return x => regexp.test(x.replace(/\\/g, "/"));
 | |
| 	} else if (ignored instanceof RegExp) {
 | |
| 		return x => ignored.test(x.replace(/\\/g, "/"));
 | |
| 	} else if (ignored instanceof Function) {
 | |
| 		return ignored;
 | |
| 	} else if (ignored) {
 | |
| 		throw new Error(`Invalid option for 'ignored': ${ignored}`);
 | |
| 	} else {
 | |
| 		return () => false;
 | |
| 	}
 | |
| };
 | |
| 
 | |
| const normalizeOptions = options => {
 | |
| 	return {
 | |
| 		followSymlinks: !!options.followSymlinks,
 | |
| 		ignored: ignoredToFunction(options.ignored),
 | |
| 		poll: options.poll
 | |
| 	};
 | |
| };
 | |
| 
 | |
| const normalizeCache = new WeakMap();
 | |
| const cachedNormalizeOptions = options => {
 | |
| 	const cacheEntry = normalizeCache.get(options);
 | |
| 	if (cacheEntry !== undefined) return cacheEntry;
 | |
| 	const normalized = normalizeOptions(options);
 | |
| 	normalizeCache.set(options, normalized);
 | |
| 	return normalized;
 | |
| };
 | |
| 
 | |
| class WatchpackFileWatcher {
 | |
| 	constructor(watchpack, watcher, files) {
 | |
| 		this.files = Array.isArray(files) ? files : [files];
 | |
| 		this.watcher = watcher;
 | |
| 		watcher.on("initial-missing", type => {
 | |
| 			for (const file of this.files) {
 | |
| 				if (!watchpack._missing.has(file))
 | |
| 					watchpack._onRemove(file, file, type);
 | |
| 			}
 | |
| 		});
 | |
| 		watcher.on("change", (mtime, type) => {
 | |
| 			for (const file of this.files) {
 | |
| 				watchpack._onChange(file, mtime, file, type);
 | |
| 			}
 | |
| 		});
 | |
| 		watcher.on("remove", type => {
 | |
| 			for (const file of this.files) {
 | |
| 				watchpack._onRemove(file, file, type);
 | |
| 			}
 | |
| 		});
 | |
| 	}
 | |
| 
 | |
| 	update(files) {
 | |
| 		if (!Array.isArray(files)) {
 | |
| 			if (this.files.length !== 1) {
 | |
| 				this.files = [files];
 | |
| 			} else if (this.files[0] !== files) {
 | |
| 				this.files[0] = files;
 | |
| 			}
 | |
| 		} else {
 | |
| 			this.files = files;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	close() {
 | |
| 		this.watcher.close();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| class WatchpackDirectoryWatcher {
 | |
| 	constructor(watchpack, watcher, directories) {
 | |
| 		this.directories = Array.isArray(directories) ? directories : [directories];
 | |
| 		this.watcher = watcher;
 | |
| 		watcher.on("initial-missing", type => {
 | |
| 			for (const item of this.directories) {
 | |
| 				watchpack._onRemove(item, item, type);
 | |
| 			}
 | |
| 		});
 | |
| 		watcher.on("change", (file, mtime, type) => {
 | |
| 			for (const item of this.directories) {
 | |
| 				watchpack._onChange(item, mtime, file, type);
 | |
| 			}
 | |
| 		});
 | |
| 		watcher.on("remove", type => {
 | |
| 			for (const item of this.directories) {
 | |
| 				watchpack._onRemove(item, item, type);
 | |
| 			}
 | |
| 		});
 | |
| 	}
 | |
| 
 | |
| 	update(directories) {
 | |
| 		if (!Array.isArray(directories)) {
 | |
| 			if (this.directories.length !== 1) {
 | |
| 				this.directories = [directories];
 | |
| 			} else if (this.directories[0] !== directories) {
 | |
| 				this.directories[0] = directories;
 | |
| 			}
 | |
| 		} else {
 | |
| 			this.directories = directories;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	close() {
 | |
| 		this.watcher.close();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| class Watchpack extends EventEmitter {
 | |
| 	constructor(options) {
 | |
| 		super();
 | |
| 		if (!options) options = EMPTY_OPTIONS;
 | |
| 		this.options = options;
 | |
| 		this.aggregateTimeout =
 | |
| 			typeof options.aggregateTimeout === "number"
 | |
| 				? options.aggregateTimeout
 | |
| 				: 200;
 | |
| 		this.watcherOptions = cachedNormalizeOptions(options);
 | |
| 		this.watcherManager = getWatcherManager(this.watcherOptions);
 | |
| 		this.fileWatchers = new Map();
 | |
| 		this.directoryWatchers = new Map();
 | |
| 		this._missing = new Set();
 | |
| 		this.startTime = undefined;
 | |
| 		this.paused = false;
 | |
| 		this.aggregatedChanges = new Set();
 | |
| 		this.aggregatedRemovals = new Set();
 | |
| 		this.aggregateTimer = undefined;
 | |
| 		this._onTimeout = this._onTimeout.bind(this);
 | |
| 	}
 | |
| 
 | |
| 	watch(arg1, arg2, arg3) {
 | |
| 		let files, directories, missing, startTime;
 | |
| 		if (!arg2) {
 | |
| 			({
 | |
| 				files = EMPTY_ARRAY,
 | |
| 				directories = EMPTY_ARRAY,
 | |
| 				missing = EMPTY_ARRAY,
 | |
| 				startTime
 | |
| 			} = arg1);
 | |
| 		} else {
 | |
| 			files = arg1;
 | |
| 			directories = arg2;
 | |
| 			missing = EMPTY_ARRAY;
 | |
| 			startTime = arg3;
 | |
| 		}
 | |
| 		this.paused = false;
 | |
| 		const fileWatchers = this.fileWatchers;
 | |
| 		const directoryWatchers = this.directoryWatchers;
 | |
| 		const ignored = this.watcherOptions.ignored;
 | |
| 		const filter = path => !ignored(path);
 | |
| 		const addToMap = (map, key, item) => {
 | |
| 			const list = map.get(key);
 | |
| 			if (list === undefined) {
 | |
| 				map.set(key, item);
 | |
| 			} else if (Array.isArray(list)) {
 | |
| 				list.push(item);
 | |
| 			} else {
 | |
| 				map.set(key, [list, item]);
 | |
| 			}
 | |
| 		};
 | |
| 		const fileWatchersNeeded = new Map();
 | |
| 		const directoryWatchersNeeded = new Map();
 | |
| 		const missingFiles = new Set();
 | |
| 		if (this.watcherOptions.followSymlinks) {
 | |
| 			const resolver = new LinkResolver();
 | |
| 			for (const file of files) {
 | |
| 				if (filter(file)) {
 | |
| 					for (const innerFile of resolver.resolve(file)) {
 | |
| 						if (file === innerFile || filter(innerFile)) {
 | |
| 							addToMap(fileWatchersNeeded, innerFile, file);
 | |
| 						}
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 			for (const file of missing) {
 | |
| 				if (filter(file)) {
 | |
| 					for (const innerFile of resolver.resolve(file)) {
 | |
| 						if (file === innerFile || filter(innerFile)) {
 | |
| 							missingFiles.add(file);
 | |
| 							addToMap(fileWatchersNeeded, innerFile, file);
 | |
| 						}
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 			for (const dir of directories) {
 | |
| 				if (filter(dir)) {
 | |
| 					let first = true;
 | |
| 					for (const innerItem of resolver.resolve(dir)) {
 | |
| 						if (filter(innerItem)) {
 | |
| 							addToMap(
 | |
| 								first ? directoryWatchersNeeded : fileWatchersNeeded,
 | |
| 								innerItem,
 | |
| 								dir
 | |
| 							);
 | |
| 						}
 | |
| 						first = false;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		} else {
 | |
| 			for (const file of files) {
 | |
| 				if (filter(file)) {
 | |
| 					addToMap(fileWatchersNeeded, file, file);
 | |
| 				}
 | |
| 			}
 | |
| 			for (const file of missing) {
 | |
| 				if (filter(file)) {
 | |
| 					missingFiles.add(file);
 | |
| 					addToMap(fileWatchersNeeded, file, file);
 | |
| 				}
 | |
| 			}
 | |
| 			for (const dir of directories) {
 | |
| 				if (filter(dir)) {
 | |
| 					addToMap(directoryWatchersNeeded, dir, dir);
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		// Close unneeded old watchers
 | |
| 		// and update existing watchers
 | |
| 		for (const [key, w] of fileWatchers) {
 | |
| 			const needed = fileWatchersNeeded.get(key);
 | |
| 			if (needed === undefined) {
 | |
| 				w.close();
 | |
| 				fileWatchers.delete(key);
 | |
| 			} else {
 | |
| 				w.update(needed);
 | |
| 				fileWatchersNeeded.delete(key);
 | |
| 			}
 | |
| 		}
 | |
| 		for (const [key, w] of directoryWatchers) {
 | |
| 			const needed = directoryWatchersNeeded.get(key);
 | |
| 			if (needed === undefined) {
 | |
| 				w.close();
 | |
| 				directoryWatchers.delete(key);
 | |
| 			} else {
 | |
| 				w.update(needed);
 | |
| 				directoryWatchersNeeded.delete(key);
 | |
| 			}
 | |
| 		}
 | |
| 		// Create new watchers and install handlers on these watchers
 | |
| 		watchEventSource.batch(() => {
 | |
| 			for (const [key, files] of fileWatchersNeeded) {
 | |
| 				const watcher = this.watcherManager.watchFile(key, startTime);
 | |
| 				if (watcher) {
 | |
| 					fileWatchers.set(key, new WatchpackFileWatcher(this, watcher, files));
 | |
| 				}
 | |
| 			}
 | |
| 			for (const [key, directories] of directoryWatchersNeeded) {
 | |
| 				const watcher = this.watcherManager.watchDirectory(key, startTime);
 | |
| 				if (watcher) {
 | |
| 					directoryWatchers.set(
 | |
| 						key,
 | |
| 						new WatchpackDirectoryWatcher(this, watcher, directories)
 | |
| 					);
 | |
| 				}
 | |
| 			}
 | |
| 		});
 | |
| 		this._missing = missingFiles;
 | |
| 		this.startTime = startTime;
 | |
| 	}
 | |
| 
 | |
| 	close() {
 | |
| 		this.paused = true;
 | |
| 		if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
 | |
| 		for (const w of this.fileWatchers.values()) w.close();
 | |
| 		for (const w of this.directoryWatchers.values()) w.close();
 | |
| 		this.fileWatchers.clear();
 | |
| 		this.directoryWatchers.clear();
 | |
| 	}
 | |
| 
 | |
| 	pause() {
 | |
| 		this.paused = true;
 | |
| 		if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
 | |
| 	}
 | |
| 
 | |
| 	getTimes() {
 | |
| 		const directoryWatchers = new Set();
 | |
| 		addWatchersToSet(this.fileWatchers.values(), directoryWatchers);
 | |
| 		addWatchersToSet(this.directoryWatchers.values(), directoryWatchers);
 | |
| 		const obj = Object.create(null);
 | |
| 		for (const w of directoryWatchers) {
 | |
| 			const times = w.getTimes();
 | |
| 			for (const file of Object.keys(times)) obj[file] = times[file];
 | |
| 		}
 | |
| 		return obj;
 | |
| 	}
 | |
| 
 | |
| 	getTimeInfoEntries() {
 | |
| 		const map = new Map();
 | |
| 		this.collectTimeInfoEntries(map, map);
 | |
| 		return map;
 | |
| 	}
 | |
| 
 | |
| 	collectTimeInfoEntries(fileTimestamps, directoryTimestamps) {
 | |
| 		const allWatchers = new Set();
 | |
| 		addWatchersToSet(this.fileWatchers.values(), allWatchers);
 | |
| 		addWatchersToSet(this.directoryWatchers.values(), allWatchers);
 | |
| 		const safeTime = { value: 0 };
 | |
| 		for (const w of allWatchers) {
 | |
| 			w.collectTimeInfoEntries(fileTimestamps, directoryTimestamps, safeTime);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	getAggregated() {
 | |
| 		if (this.aggregateTimer) {
 | |
| 			clearTimeout(this.aggregateTimer);
 | |
| 			this.aggregateTimer = undefined;
 | |
| 		}
 | |
| 		const changes = this.aggregatedChanges;
 | |
| 		const removals = this.aggregatedRemovals;
 | |
| 		this.aggregatedChanges = new Set();
 | |
| 		this.aggregatedRemovals = new Set();
 | |
| 		return { changes, removals };
 | |
| 	}
 | |
| 
 | |
| 	_onChange(item, mtime, file, type) {
 | |
| 		file = file || item;
 | |
| 		if (!this.paused) {
 | |
| 			this.emit("change", file, mtime, type);
 | |
| 			if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
 | |
| 			this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout);
 | |
| 		}
 | |
| 		this.aggregatedRemovals.delete(item);
 | |
| 		this.aggregatedChanges.add(item);
 | |
| 	}
 | |
| 
 | |
| 	_onRemove(item, file, type) {
 | |
| 		file = file || item;
 | |
| 		if (!this.paused) {
 | |
| 			this.emit("remove", file, type);
 | |
| 			if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
 | |
| 			this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout);
 | |
| 		}
 | |
| 		this.aggregatedChanges.delete(item);
 | |
| 		this.aggregatedRemovals.add(item);
 | |
| 	}
 | |
| 
 | |
| 	_onTimeout() {
 | |
| 		this.aggregateTimer = undefined;
 | |
| 		const changes = this.aggregatedChanges;
 | |
| 		const removals = this.aggregatedRemovals;
 | |
| 		this.aggregatedChanges = new Set();
 | |
| 		this.aggregatedRemovals = new Set();
 | |
| 		this.emit("aggregated", changes, removals);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| module.exports = Watchpack;
 |