| | |
| | | /* |
| | | MIT License http://www.opensource.org/licenses/mit-license.php |
| | | Author Tobias Koppers @sokra |
| | | Author Alexander Akait @akexander-akait |
| | | */ |
| | | "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 => { |
| | | if (ignored.length === 0) { |
| | | return; |
| | | } |
| | | const source = globToRegExp(ignored, { globstar: true, extended: true }) |
| | | .source; |
| | | return source.slice(0, source.length - 1) + "(?:$|\\/)"; |
| | | }; |
| | | |
| | | const ignoredToFunction = ignored => { |
| | | if (Array.isArray(ignored)) { |
| | | const stringRegexps = ignored.map(i => stringToRegexp(i)).filter(Boolean); |
| | | if (stringRegexps.length === 0) { |
| | | return () => false; |
| | | } |
| | | const regexp = new RegExp(stringRegexps.join("|")); |
| | | return x => regexp.test(x.replace(/\\/g, "/")); |
| | | } else if (typeof ignored === "string") { |
| | | const stringRegexp = stringToRegexp(ignored); |
| | | if (!stringRegexp) { |
| | | return () => false; |
| | | } |
| | | const regexp = new RegExp(stringRegexp); |
| | | 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; |
| | | // TODO remove this file in the next major release |
| | | module.exports = require("./index"); |