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
		
	
| 
											9 months ago
										 | /* | ||
|  | 	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; |