source: frontend/node_modules/watchpack/lib/watchEventSource.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 11.4 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5"use strict";
6
7const { EventEmitter } = require("events");
8const fs = require("fs");
9const path = require("path");
10const reducePlan = require("./reducePlan");
11
12/** @typedef {import("fs").FSWatcher} FSWatcher */
13/** @typedef {import("./index").EventType} EventType */
14
15const IS_OSX = require("os").platform() === "darwin";
16const IS_WIN = require("os").platform() === "win32";
17
18const 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
22const watcherLimit =
23 // @ts-expect-error avoid additional checks
24 +process.env.WATCHPACK_WATCHER_LIMIT || (IS_OSX ? 20 : 10000);
25
26const recursiveWatcherLogging = Boolean(
27 process.env.WATCHPACK_RECURSIVE_WATCHER_LOGGING,
28);
29
30let isBatch = false;
31let watcherCount = 0;
32
33/** @type {Map<Watcher, string>} */
34const pendingWatchers = new Map();
35
36/** @type {Map<string, RecursiveWatcher>} */
37const recursiveWatchers = new Map();
38
39/** @type {Map<string, DirectWatcher>} */
40const directWatchers = new Map();
41
42/** @type {Map<Watcher, RecursiveWatcher | DirectWatcher>} */
43const underlyingWatcher = new Map();
44
45/**
46 * @param {string} filePath file path
47 * @returns {NodeJS.ErrnoException} new error with file path in the message
48 */
49function 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 */
63function 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
84class 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
149class 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 */
273class 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 */
294const 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 */
306const 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
314const 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
401module.exports.Watcher = Watcher;
402
403/**
404 * @param {() => void} fn a function
405 */
406module.exports.batch = (fn) => {
407 isBatch = true;
408 try {
409 fn();
410 } finally {
411 isBatch = false;
412 execute();
413 }
414};
415
416module.exports.createHandleChangeEvent = createHandleChangeEvent;
417
418module.exports.getNumberOfWatchers = () => watcherCount;
419
420/**
421 * @param {string} filePath a file path
422 * @returns {Watcher} watcher
423 */
424module.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
449module.exports.watcherLimit = watcherLimit;
Note: See TracBrowser for help on using the repository browser.