You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
335 lines
8.3 KiB
335 lines
8.3 KiB
/* |
|
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<Watcher, string>} */ |
|
const pendingWatchers = new Map(); |
|
|
|
/** @type {Map<string, RecursiveWatcher>} */ |
|
const recursiveWatchers = new Map(); |
|
|
|
/** @type {Map<string, DirectWatcher>} */ |
|
const directWatchers = new Map(); |
|
|
|
/** @type {Map<Watcher, RecursiveWatcher | DirectWatcher>} */ |
|
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<Watcher, string>} */ |
|
this.mapWatcherToPath = new Map(); |
|
/** @type {Map<string, Set<Watcher>>} */ |
|
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<string, Watcher[] | Watcher>} */ |
|
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; |
|
};
|
|
|