| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | Author Tobias Koppers @sokra
|
|---|
| 4 | */
|
|---|
| 5 | "use strict";
|
|---|
| 6 |
|
|---|
| 7 | const { EventEmitter } = require("events");
|
|---|
| 8 | const fs = require("fs");
|
|---|
| 9 | const path = require("path");
|
|---|
| 10 | const reducePlan = require("./reducePlan");
|
|---|
| 11 |
|
|---|
| 12 | /** @typedef {import("fs").FSWatcher} FSWatcher */
|
|---|
| 13 | /** @typedef {import("./index").EventType} EventType */
|
|---|
| 14 |
|
|---|
| 15 | const IS_OSX = require("os").platform() === "darwin";
|
|---|
| 16 | const IS_WIN = require("os").platform() === "win32";
|
|---|
| 17 |
|
|---|
| 18 | const SUPPORTS_RECURSIVE_WATCHING = IS_OSX || IS_WIN;
|
|---|
| 19 |
|
|---|
| 20 | // Use 20 for OSX to make `FSWatcher.close` faster
|
|---|
| 21 | // https://github.com/nodejs/node/issues/29949
|
|---|
| 22 | const watcherLimit =
|
|---|
| 23 | // @ts-expect-error avoid additional checks
|
|---|
| 24 | +process.env.WATCHPACK_WATCHER_LIMIT || (IS_OSX ? 20 : 10000);
|
|---|
| 25 |
|
|---|
| 26 | const recursiveWatcherLogging = Boolean(
|
|---|
| 27 | process.env.WATCHPACK_RECURSIVE_WATCHER_LOGGING,
|
|---|
| 28 | );
|
|---|
| 29 |
|
|---|
| 30 | let isBatch = false;
|
|---|
| 31 | let watcherCount = 0;
|
|---|
| 32 |
|
|---|
| 33 | /** @type {Map<Watcher, string>} */
|
|---|
| 34 | const pendingWatchers = new Map();
|
|---|
| 35 |
|
|---|
| 36 | /** @type {Map<string, RecursiveWatcher>} */
|
|---|
| 37 | const recursiveWatchers = new Map();
|
|---|
| 38 |
|
|---|
| 39 | /** @type {Map<string, DirectWatcher>} */
|
|---|
| 40 | const directWatchers = new Map();
|
|---|
| 41 |
|
|---|
| 42 | /** @type {Map<Watcher, RecursiveWatcher | DirectWatcher>} */
|
|---|
| 43 | const underlyingWatcher = new Map();
|
|---|
| 44 |
|
|---|
| 45 | /**
|
|---|
| 46 | * @param {string} filePath file path
|
|---|
| 47 | * @returns {NodeJS.ErrnoException} new error with file path in the message
|
|---|
| 48 | */
|
|---|
| 49 | function createEPERMError(filePath) {
|
|---|
| 50 | const error =
|
|---|
| 51 | /** @type {NodeJS.ErrnoException} */
|
|---|
| 52 | (new Error(`Operation not permitted: ${filePath}`));
|
|---|
| 53 | error.code = "EPERM";
|
|---|
| 54 | return error;
|
|---|
| 55 | }
|
|---|
| 56 |
|
|---|
| 57 | /**
|
|---|
| 58 | * @param {FSWatcher} watcher watcher
|
|---|
| 59 | * @param {string} filePath a file path
|
|---|
| 60 | * @param {(type: "rename" | "change", filename: string) => void} handleChangeEvent function to handle change
|
|---|
| 61 | * @returns {(type: "rename" | "change", filename: string) => void} handler of change event
|
|---|
| 62 | */
|
|---|
| 63 | function createHandleChangeEvent(watcher, filePath, handleChangeEvent) {
|
|---|
| 64 | return (type, filename) => {
|
|---|
| 65 | // TODO: After Node.js v22, fs.watch(dir) and deleting a dir will trigger the rename change event.
|
|---|
| 66 | // Here we just ignore it and keep the same behavior as before v22
|
|---|
| 67 | // https://github.com/libuv/libuv/pull/4376
|
|---|
| 68 | if (
|
|---|
| 69 | type === "rename" &&
|
|---|
| 70 | path.isAbsolute(filename) &&
|
|---|
| 71 | path.basename(filename) === path.basename(filePath)
|
|---|
| 72 | ) {
|
|---|
| 73 | if (!IS_OSX) {
|
|---|
| 74 | // Before v22, windows will throw EPERM error
|
|---|
| 75 | watcher.emit("error", createEPERMError(filename));
|
|---|
| 76 | }
|
|---|
| 77 | // Before v22, macos nothing to do
|
|---|
| 78 | return;
|
|---|
| 79 | }
|
|---|
| 80 | handleChangeEvent(type, filename);
|
|---|
| 81 | };
|
|---|
| 82 | }
|
|---|
| 83 |
|
|---|
| 84 | class DirectWatcher {
|
|---|
| 85 | /**
|
|---|
| 86 | * @param {string} filePath file path
|
|---|
| 87 | */
|
|---|
| 88 | constructor(filePath) {
|
|---|
| 89 | this.filePath = filePath;
|
|---|
| 90 | this.watchers = new Set();
|
|---|
| 91 | /** @type {FSWatcher | undefined} */
|
|---|
| 92 | this.watcher = undefined;
|
|---|
| 93 | try {
|
|---|
| 94 | const watcher = fs.watch(filePath);
|
|---|
| 95 |
|
|---|
| 96 | this.watcher = watcher;
|
|---|
| 97 | const handleChangeEvent = createHandleChangeEvent(
|
|---|
| 98 | watcher,
|
|---|
| 99 | filePath,
|
|---|
| 100 | (type, filename) => {
|
|---|
| 101 | for (const w of this.watchers) {
|
|---|
| 102 | w.emit("change", type, filename);
|
|---|
| 103 | }
|
|---|
| 104 | },
|
|---|
| 105 | );
|
|---|
| 106 | watcher.on("change", handleChangeEvent);
|
|---|
| 107 | watcher.on("error", (error) => {
|
|---|
| 108 | for (const w of this.watchers) {
|
|---|
| 109 | w.emit("error", error);
|
|---|
| 110 | }
|
|---|
| 111 | });
|
|---|
| 112 | } catch (err) {
|
|---|
| 113 | process.nextTick(() => {
|
|---|
| 114 | for (const w of this.watchers) {
|
|---|
| 115 | w.emit("error", err);
|
|---|
| 116 | }
|
|---|
| 117 | });
|
|---|
| 118 | }
|
|---|
| 119 | watcherCount++;
|
|---|
| 120 | }
|
|---|
| 121 |
|
|---|
| 122 | /**
|
|---|
| 123 | * @param {Watcher} watcher a watcher
|
|---|
| 124 | */
|
|---|
| 125 | add(watcher) {
|
|---|
| 126 | underlyingWatcher.set(watcher, this);
|
|---|
| 127 | this.watchers.add(watcher);
|
|---|
| 128 | }
|
|---|
| 129 |
|
|---|
| 130 | /**
|
|---|
| 131 | * @param {Watcher} watcher a watcher
|
|---|
| 132 | */
|
|---|
| 133 | remove(watcher) {
|
|---|
| 134 | this.watchers.delete(watcher);
|
|---|
| 135 | if (this.watchers.size === 0) {
|
|---|
| 136 | directWatchers.delete(this.filePath);
|
|---|
| 137 | watcherCount--;
|
|---|
| 138 | if (this.watcher) this.watcher.close();
|
|---|
| 139 | }
|
|---|
| 140 | }
|
|---|
| 141 |
|
|---|
| 142 | getWatchers() {
|
|---|
| 143 | return this.watchers;
|
|---|
| 144 | }
|
|---|
| 145 | }
|
|---|
| 146 |
|
|---|
| 147 | /** @typedef {Set<Watcher>} WatcherSet */
|
|---|
| 148 |
|
|---|
| 149 | class RecursiveWatcher {
|
|---|
| 150 | /**
|
|---|
| 151 | * @param {string} rootPath a root path
|
|---|
| 152 | */
|
|---|
| 153 | constructor(rootPath) {
|
|---|
| 154 | this.rootPath = rootPath;
|
|---|
| 155 | /** @type {Map<Watcher, string>} */
|
|---|
| 156 | this.mapWatcherToPath = new Map();
|
|---|
| 157 | /** @type {Map<string, WatcherSet>} */
|
|---|
| 158 | this.mapPathToWatchers = new Map();
|
|---|
| 159 | this.watcher = undefined;
|
|---|
| 160 | try {
|
|---|
| 161 | const watcher = fs.watch(rootPath, {
|
|---|
| 162 | recursive: true,
|
|---|
| 163 | });
|
|---|
| 164 | this.watcher = watcher;
|
|---|
| 165 | watcher.on("change", (type, filename) => {
|
|---|
| 166 | if (!filename) {
|
|---|
| 167 | if (recursiveWatcherLogging) {
|
|---|
| 168 | process.stderr.write(
|
|---|
| 169 | `[watchpack] dispatch ${type} event in recursive watcher (${this.rootPath}) to all watchers\n`,
|
|---|
| 170 | );
|
|---|
| 171 | }
|
|---|
| 172 | for (const w of this.mapWatcherToPath.keys()) {
|
|---|
| 173 | w.emit("change", /** @type {EventType} */ (type));
|
|---|
| 174 | }
|
|---|
| 175 | } else {
|
|---|
| 176 | const dir = path.dirname(/** @type {string} */ (filename));
|
|---|
| 177 | const watchers = this.mapPathToWatchers.get(dir);
|
|---|
| 178 | if (recursiveWatcherLogging) {
|
|---|
| 179 | process.stderr.write(
|
|---|
| 180 | `[watchpack] dispatch ${type} event in recursive watcher (${
|
|---|
| 181 | this.rootPath
|
|---|
| 182 | }) for '${filename}' to ${
|
|---|
| 183 | watchers ? watchers.size : 0
|
|---|
| 184 | } watchers\n`,
|
|---|
| 185 | );
|
|---|
| 186 | }
|
|---|
| 187 | if (watchers === undefined) return;
|
|---|
| 188 | for (const w of watchers) {
|
|---|
| 189 | w.emit(
|
|---|
| 190 | "change",
|
|---|
| 191 | /** @type {EventType} */ (type),
|
|---|
| 192 | path.basename(/** @type {string} */ (filename)),
|
|---|
| 193 | );
|
|---|
| 194 | }
|
|---|
| 195 | }
|
|---|
| 196 | });
|
|---|
| 197 | watcher.on("error", (error) => {
|
|---|
| 198 | for (const w of this.mapWatcherToPath.keys()) {
|
|---|
| 199 | w.emit("error", error);
|
|---|
| 200 | }
|
|---|
| 201 | });
|
|---|
| 202 | } catch (err) {
|
|---|
| 203 | process.nextTick(() => {
|
|---|
| 204 | for (const w of this.mapWatcherToPath.keys()) {
|
|---|
| 205 | w.emit("error", err);
|
|---|
| 206 | }
|
|---|
| 207 | });
|
|---|
| 208 | }
|
|---|
| 209 | watcherCount++;
|
|---|
| 210 | if (recursiveWatcherLogging) {
|
|---|
| 211 | process.stderr.write(
|
|---|
| 212 | `[watchpack] created recursive watcher at ${rootPath}\n`,
|
|---|
| 213 | );
|
|---|
| 214 | }
|
|---|
| 215 | }
|
|---|
| 216 |
|
|---|
| 217 | /**
|
|---|
| 218 | * @param {string} filePath a file path
|
|---|
| 219 | * @param {Watcher} watcher a watcher
|
|---|
| 220 | */
|
|---|
| 221 | add(filePath, watcher) {
|
|---|
| 222 | underlyingWatcher.set(watcher, this);
|
|---|
| 223 | const subpath = filePath.slice(this.rootPath.length + 1) || ".";
|
|---|
| 224 | this.mapWatcherToPath.set(watcher, subpath);
|
|---|
| 225 | const set = this.mapPathToWatchers.get(subpath);
|
|---|
| 226 | if (set === undefined) {
|
|---|
| 227 | const newSet = new Set();
|
|---|
| 228 | newSet.add(watcher);
|
|---|
| 229 | this.mapPathToWatchers.set(subpath, newSet);
|
|---|
| 230 | } else {
|
|---|
| 231 | set.add(watcher);
|
|---|
| 232 | }
|
|---|
| 233 | }
|
|---|
| 234 |
|
|---|
| 235 | /**
|
|---|
| 236 | * @param {Watcher} watcher a watcher
|
|---|
| 237 | */
|
|---|
| 238 | remove(watcher) {
|
|---|
| 239 | const subpath = this.mapWatcherToPath.get(watcher);
|
|---|
| 240 | if (!subpath) return;
|
|---|
| 241 | this.mapWatcherToPath.delete(watcher);
|
|---|
| 242 | const set = /** @type {WatcherSet} */ (this.mapPathToWatchers.get(subpath));
|
|---|
| 243 | set.delete(watcher);
|
|---|
| 244 | if (set.size === 0) {
|
|---|
| 245 | this.mapPathToWatchers.delete(subpath);
|
|---|
| 246 | }
|
|---|
| 247 | if (this.mapWatcherToPath.size === 0) {
|
|---|
| 248 | recursiveWatchers.delete(this.rootPath);
|
|---|
| 249 | watcherCount--;
|
|---|
| 250 | if (this.watcher) this.watcher.close();
|
|---|
| 251 | if (recursiveWatcherLogging) {
|
|---|
| 252 | process.stderr.write(
|
|---|
| 253 | `[watchpack] closed recursive watcher at ${this.rootPath}\n`,
|
|---|
| 254 | );
|
|---|
| 255 | }
|
|---|
| 256 | }
|
|---|
| 257 | }
|
|---|
| 258 |
|
|---|
| 259 | getWatchers() {
|
|---|
| 260 | return this.mapWatcherToPath;
|
|---|
| 261 | }
|
|---|
| 262 | }
|
|---|
| 263 |
|
|---|
| 264 | /**
|
|---|
| 265 | * @typedef {object} WatcherEvents
|
|---|
| 266 | * @property {(eventType: EventType, filename?: string) => void} change change event
|
|---|
| 267 | * @property {(err: unknown) => void} error error event
|
|---|
| 268 | */
|
|---|
| 269 |
|
|---|
| 270 | /**
|
|---|
| 271 | * @extends {EventEmitter<{ [K in keyof WatcherEvents]: Parameters<WatcherEvents[K]> }>}
|
|---|
| 272 | */
|
|---|
| 273 | class Watcher extends EventEmitter {
|
|---|
| 274 | constructor() {
|
|---|
| 275 | super();
|
|---|
| 276 | }
|
|---|
| 277 |
|
|---|
| 278 | close() {
|
|---|
| 279 | if (pendingWatchers.has(this)) {
|
|---|
| 280 | pendingWatchers.delete(this);
|
|---|
| 281 | return;
|
|---|
| 282 | }
|
|---|
| 283 | const watcher = underlyingWatcher.get(this);
|
|---|
| 284 | /** @type {RecursiveWatcher | DirectWatcher} */
|
|---|
| 285 | (watcher).remove(this);
|
|---|
| 286 | underlyingWatcher.delete(this);
|
|---|
| 287 | }
|
|---|
| 288 | }
|
|---|
| 289 |
|
|---|
| 290 | /**
|
|---|
| 291 | * @param {string} filePath a file path
|
|---|
| 292 | * @returns {DirectWatcher} a directory watcher
|
|---|
| 293 | */
|
|---|
| 294 | const createDirectWatcher = (filePath) => {
|
|---|
| 295 | const existing = directWatchers.get(filePath);
|
|---|
| 296 | if (existing !== undefined) return existing;
|
|---|
| 297 | const w = new DirectWatcher(filePath);
|
|---|
| 298 | directWatchers.set(filePath, w);
|
|---|
| 299 | return w;
|
|---|
| 300 | };
|
|---|
| 301 |
|
|---|
| 302 | /**
|
|---|
| 303 | * @param {string} rootPath a root path
|
|---|
| 304 | * @returns {RecursiveWatcher} a recursive watcher
|
|---|
| 305 | */
|
|---|
| 306 | const createRecursiveWatcher = (rootPath) => {
|
|---|
| 307 | const existing = recursiveWatchers.get(rootPath);
|
|---|
| 308 | if (existing !== undefined) return existing;
|
|---|
| 309 | const w = new RecursiveWatcher(rootPath);
|
|---|
| 310 | recursiveWatchers.set(rootPath, w);
|
|---|
| 311 | return w;
|
|---|
| 312 | };
|
|---|
| 313 |
|
|---|
| 314 | const execute = () => {
|
|---|
| 315 | /** @type {Map<string, Watcher[] | Watcher>} */
|
|---|
| 316 | const map = new Map();
|
|---|
| 317 | /**
|
|---|
| 318 | * @param {Watcher} watcher a watcher
|
|---|
| 319 | * @param {string} filePath a file path
|
|---|
| 320 | */
|
|---|
| 321 | const addWatcher = (watcher, filePath) => {
|
|---|
| 322 | const entry = map.get(filePath);
|
|---|
| 323 | if (entry === undefined) {
|
|---|
| 324 | map.set(filePath, watcher);
|
|---|
| 325 | } else if (Array.isArray(entry)) {
|
|---|
| 326 | entry.push(watcher);
|
|---|
| 327 | } else {
|
|---|
| 328 | map.set(filePath, [entry, watcher]);
|
|---|
| 329 | }
|
|---|
| 330 | };
|
|---|
| 331 | for (const [watcher, filePath] of pendingWatchers) {
|
|---|
| 332 | addWatcher(watcher, filePath);
|
|---|
| 333 | }
|
|---|
| 334 | pendingWatchers.clear();
|
|---|
| 335 |
|
|---|
| 336 | // Fast case when we are not reaching the limit
|
|---|
| 337 | if (!SUPPORTS_RECURSIVE_WATCHING || watcherLimit - watcherCount >= map.size) {
|
|---|
| 338 | // Create watchers for all entries in the map
|
|---|
| 339 | for (const [filePath, entry] of map) {
|
|---|
| 340 | const w = createDirectWatcher(filePath);
|
|---|
| 341 | if (Array.isArray(entry)) {
|
|---|
| 342 | for (const item of entry) w.add(item);
|
|---|
| 343 | } else {
|
|---|
| 344 | w.add(entry);
|
|---|
| 345 | }
|
|---|
| 346 | }
|
|---|
| 347 | return;
|
|---|
| 348 | }
|
|---|
| 349 |
|
|---|
| 350 | // Reconsider existing watchers to improving watch plan
|
|---|
| 351 | for (const watcher of recursiveWatchers.values()) {
|
|---|
| 352 | for (const [w, subpath] of watcher.getWatchers()) {
|
|---|
| 353 | addWatcher(w, path.join(watcher.rootPath, subpath));
|
|---|
| 354 | }
|
|---|
| 355 | }
|
|---|
| 356 | for (const watcher of directWatchers.values()) {
|
|---|
| 357 | for (const w of watcher.getWatchers()) {
|
|---|
| 358 | addWatcher(w, watcher.filePath);
|
|---|
| 359 | }
|
|---|
| 360 | }
|
|---|
| 361 |
|
|---|
| 362 | // Merge map entries to keep watcher limit
|
|---|
| 363 | // Create a 10% buffer to be able to enter fast case more often
|
|---|
| 364 | const plan = reducePlan(map, watcherLimit * 0.9);
|
|---|
| 365 |
|
|---|
| 366 | // Update watchers for all entries in the map
|
|---|
| 367 | for (const [filePath, entry] of plan) {
|
|---|
| 368 | if (entry.size === 1) {
|
|---|
| 369 | for (const [watcher, filePath] of entry) {
|
|---|
| 370 | const w = createDirectWatcher(filePath);
|
|---|
| 371 | const old = underlyingWatcher.get(watcher);
|
|---|
| 372 | if (old === w) continue;
|
|---|
| 373 | w.add(watcher);
|
|---|
| 374 | if (old !== undefined) old.remove(watcher);
|
|---|
| 375 | }
|
|---|
| 376 | } else {
|
|---|
| 377 | const filePaths = new Set(entry.values());
|
|---|
| 378 | if (filePaths.size > 1) {
|
|---|
| 379 | const w = createRecursiveWatcher(filePath);
|
|---|
| 380 | for (const [watcher, watcherPath] of entry) {
|
|---|
| 381 | const old = underlyingWatcher.get(watcher);
|
|---|
| 382 | if (old === w) continue;
|
|---|
| 383 | w.add(watcherPath, watcher);
|
|---|
| 384 | if (old !== undefined) old.remove(watcher);
|
|---|
| 385 | }
|
|---|
| 386 | } else {
|
|---|
| 387 | for (const filePath of filePaths) {
|
|---|
| 388 | const w = createDirectWatcher(filePath);
|
|---|
| 389 | for (const watcher of entry.keys()) {
|
|---|
| 390 | const old = underlyingWatcher.get(watcher);
|
|---|
| 391 | if (old === w) continue;
|
|---|
| 392 | w.add(watcher);
|
|---|
| 393 | if (old !== undefined) old.remove(watcher);
|
|---|
| 394 | }
|
|---|
| 395 | }
|
|---|
| 396 | }
|
|---|
| 397 | }
|
|---|
| 398 | }
|
|---|
| 399 | };
|
|---|
| 400 |
|
|---|
| 401 | module.exports.Watcher = Watcher;
|
|---|
| 402 |
|
|---|
| 403 | /**
|
|---|
| 404 | * @param {() => void} fn a function
|
|---|
| 405 | */
|
|---|
| 406 | module.exports.batch = (fn) => {
|
|---|
| 407 | isBatch = true;
|
|---|
| 408 | try {
|
|---|
| 409 | fn();
|
|---|
| 410 | } finally {
|
|---|
| 411 | isBatch = false;
|
|---|
| 412 | execute();
|
|---|
| 413 | }
|
|---|
| 414 | };
|
|---|
| 415 |
|
|---|
| 416 | module.exports.createHandleChangeEvent = createHandleChangeEvent;
|
|---|
| 417 |
|
|---|
| 418 | module.exports.getNumberOfWatchers = () => watcherCount;
|
|---|
| 419 |
|
|---|
| 420 | /**
|
|---|
| 421 | * @param {string} filePath a file path
|
|---|
| 422 | * @returns {Watcher} watcher
|
|---|
| 423 | */
|
|---|
| 424 | module.exports.watch = (filePath) => {
|
|---|
| 425 | const watcher = new Watcher();
|
|---|
| 426 | // Find an existing watcher
|
|---|
| 427 | const directWatcher = directWatchers.get(filePath);
|
|---|
| 428 | if (directWatcher !== undefined) {
|
|---|
| 429 | directWatcher.add(watcher);
|
|---|
| 430 | return watcher;
|
|---|
| 431 | }
|
|---|
| 432 | let current = filePath;
|
|---|
| 433 | for (;;) {
|
|---|
| 434 | const recursiveWatcher = recursiveWatchers.get(current);
|
|---|
| 435 | if (recursiveWatcher !== undefined) {
|
|---|
| 436 | recursiveWatcher.add(filePath, watcher);
|
|---|
| 437 | return watcher;
|
|---|
| 438 | }
|
|---|
| 439 | const parent = path.dirname(current);
|
|---|
| 440 | if (parent === current) break;
|
|---|
| 441 | current = parent;
|
|---|
| 442 | }
|
|---|
| 443 | // Queue up watcher for creation
|
|---|
| 444 | pendingWatchers.set(watcher, filePath);
|
|---|
| 445 | if (!isBatch) execute();
|
|---|
| 446 | return watcher;
|
|---|
| 447 | };
|
|---|
| 448 |
|
|---|
| 449 | module.exports.watcherLimit = watcherLimit;
|
|---|