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

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

Fix frontend appearance

  • Property mode set to 100644
File size: 15.9 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 globToRegExp = require("glob-to-regexp");
9const LinkResolver = require("./LinkResolver");
10const getWatcherManager = require("./getWatcherManager");
11const watchEventSource = require("./watchEventSource");
12
13/** @typedef {import("./getWatcherManager").WatcherManager} WatcherManager */
14/** @typedef {import("./DirectoryWatcher")} DirectoryWatcher */
15/** @typedef {import("./DirectoryWatcher").DirectoryWatcherEvents} DirectoryWatcherEvents */
16/** @typedef {import("./DirectoryWatcher").FileWatcherEvents} FileWatcherEvents */
17
18// eslint-disable-next-line jsdoc/reject-any-type
19/** @typedef {Record<string, (...args: any[]) => any>} EventMap */
20
21/**
22 * @template {EventMap} T
23 * @typedef {import("./DirectoryWatcher").Watcher<T>} Watcher
24 */
25
26/** @typedef {(item: string) => boolean} IgnoredFunction */
27/** @typedef {string[] | RegExp | string | IgnoredFunction} Ignored */
28
29/**
30 * @typedef {object} WatcherOptions
31 * @property {boolean=} followSymlinks true when need to resolve symlinks and watch symlink and real file, otherwise false
32 * @property {Ignored=} ignored ignore some files from watching (glob pattern or regexp)
33 * @property {number | boolean=} poll true when need to enable polling mode for watching, otherwise false
34 */
35
36/** @typedef {WatcherOptions & { aggregateTimeout?: number }} WatchOptions */
37
38/**
39 * @typedef {object} NormalizedWatchOptions
40 * @property {boolean} followSymlinks true when need to resolve symlinks and watch symlink and real file, otherwise false
41 * @property {IgnoredFunction} ignored ignore some files from watching (glob pattern or regexp)
42 * @property {number | boolean=} poll true when need to enable polling mode for watching, otherwise false
43 */
44
45/** @typedef {`scan (${string})` | "change" | "rename" | `watch ${string}` | `directory-removed ${string}`} EventType */
46/** @typedef {{ safeTime: number, timestamp: number, accuracy: number }} Entry */
47/** @typedef {{ safeTime: number }} OnlySafeTimeEntry */
48// eslint-disable-next-line jsdoc/ts-no-empty-object-type
49/** @typedef {{}} ExistenceOnlyTimeEntry */
50/** @typedef {Map<string, Entry | OnlySafeTimeEntry | ExistenceOnlyTimeEntry | null>} TimeInfoEntries */
51/** @typedef {Set<string>} Changes */
52/** @typedef {Set<string>} Removals */
53/** @typedef {{ changes: Changes, removals: Removals }} Aggregated */
54/** @typedef {{ files?: Iterable<string>, directories?: Iterable<string>, missing?: Iterable<string>, startTime?: number }} WatchMethodOptions */
55/** @typedef {Record<string, number>} Times */
56
57/**
58 * @param {MapIterator<WatchpackFileWatcher> | MapIterator<WatchpackDirectoryWatcher>} watchers watchers
59 * @param {Set<DirectoryWatcher>} set set
60 */
61function addWatchersToSet(watchers, set) {
62 for (const ww of watchers) {
63 const w = ww.watcher;
64 if (!set.has(w.directoryWatcher)) {
65 set.add(w.directoryWatcher);
66 }
67 }
68}
69
70/**
71 * @param {string} ignored ignored
72 * @returns {string | undefined} resolved global to regexp
73 */
74const stringToRegexp = (ignored) => {
75 if (ignored.length === 0) {
76 return;
77 }
78 const { source } = globToRegExp(ignored, { globstar: true, extended: true });
79 return `${source.slice(0, -1)}(?:$|\\/)`;
80};
81
82/**
83 * @param {Ignored=} ignored ignored
84 * @returns {(item: string) => boolean} ignored to function
85 */
86const ignoredToFunction = (ignored) => {
87 if (Array.isArray(ignored)) {
88 const stringRegexps = ignored.map((i) => stringToRegexp(i)).filter(Boolean);
89 if (stringRegexps.length === 0) {
90 return () => false;
91 }
92 const regexp = new RegExp(stringRegexps.join("|"));
93 return (item) => regexp.test(item.replace(/\\/g, "/"));
94 } else if (typeof ignored === "string") {
95 const stringRegexp = stringToRegexp(ignored);
96 if (!stringRegexp) {
97 return () => false;
98 }
99 const regexp = new RegExp(stringRegexp);
100 return (item) => regexp.test(item.replace(/\\/g, "/"));
101 } else if (ignored instanceof RegExp) {
102 return (item) => ignored.test(item.replace(/\\/g, "/"));
103 } else if (typeof ignored === "function") {
104 return ignored;
105 } else if (ignored) {
106 throw new Error(`Invalid option for 'ignored': ${ignored}`);
107 } else {
108 return () => false;
109 }
110};
111
112/**
113 * @param {WatchOptions} options options
114 * @returns {NormalizedWatchOptions} normalized options
115 */
116const normalizeOptions = (options) => ({
117 followSymlinks: Boolean(options.followSymlinks),
118 ignored: ignoredToFunction(options.ignored),
119 poll: options.poll,
120});
121
122const normalizeCache = new WeakMap();
123/**
124 * @param {WatchOptions} options options
125 * @returns {NormalizedWatchOptions} normalized options
126 */
127const cachedNormalizeOptions = (options) => {
128 const cacheEntry = normalizeCache.get(options);
129 if (cacheEntry !== undefined) return cacheEntry;
130 const normalized = normalizeOptions(options);
131 normalizeCache.set(options, normalized);
132 return normalized;
133};
134
135class WatchpackFileWatcher {
136 /**
137 * @param {Watchpack} watchpack watchpack
138 * @param {Watcher<FileWatcherEvents>} watcher watcher
139 * @param {string | string[]} files files
140 */
141 constructor(watchpack, watcher, files) {
142 /** @type {string[]} */
143 this.files = Array.isArray(files) ? files : [files];
144 this.watcher = watcher;
145 watcher.on("initial-missing", (type) => {
146 for (const file of this.files) {
147 if (!watchpack._missing.has(file)) {
148 watchpack._onRemove(file, file, type);
149 }
150 }
151 });
152 watcher.on("change", (mtime, type, _initial) => {
153 for (const file of this.files) {
154 watchpack._onChange(file, mtime, file, type);
155 }
156 });
157 watcher.on("remove", (type) => {
158 for (const file of this.files) {
159 watchpack._onRemove(file, file, type);
160 }
161 });
162 }
163
164 /**
165 * @param {string | string[]} files files
166 */
167 update(files) {
168 if (!Array.isArray(files)) {
169 if (this.files.length !== 1) {
170 this.files = [files];
171 } else if (this.files[0] !== files) {
172 this.files[0] = files;
173 }
174 } else {
175 this.files = files;
176 }
177 }
178
179 close() {
180 this.watcher.close();
181 }
182}
183
184class WatchpackDirectoryWatcher {
185 /**
186 * @param {Watchpack} watchpack watchpack
187 * @param {Watcher<DirectoryWatcherEvents>} watcher watcher
188 * @param {string} directories directories
189 */
190 constructor(watchpack, watcher, directories) {
191 /** @type {string[]} */
192 this.directories = Array.isArray(directories) ? directories : [directories];
193 this.watcher = watcher;
194 watcher.on("initial-missing", (type) => {
195 for (const item of this.directories) {
196 watchpack._onRemove(item, item, type);
197 }
198 });
199 watcher.on("change", (file, mtime, type, _initial) => {
200 for (const item of this.directories) {
201 watchpack._onChange(item, mtime, file, type);
202 }
203 });
204 watcher.on("remove", (type) => {
205 for (const item of this.directories) {
206 watchpack._onRemove(item, item, type);
207 }
208 });
209 }
210
211 /**
212 * @param {string | string[]} directories directories
213 */
214 update(directories) {
215 if (!Array.isArray(directories)) {
216 if (this.directories.length !== 1) {
217 this.directories = [directories];
218 } else if (this.directories[0] !== directories) {
219 this.directories[0] = directories;
220 }
221 } else {
222 this.directories = directories;
223 }
224 }
225
226 close() {
227 this.watcher.close();
228 }
229}
230
231/**
232 * @typedef {object} WatchpackEvents
233 * @property {(file: string, mtime: number, type: EventType) => void} change change event
234 * @property {(file: string, type: EventType) => void} remove remove event
235 * @property {(changes: Changes, removals: Removals) => void} aggregated aggregated event
236 */
237
238/**
239 * @extends {EventEmitter<{ [K in keyof WatchpackEvents]: Parameters<WatchpackEvents[K]> }>}
240 */
241class Watchpack extends EventEmitter {
242 /**
243 * @param {WatchOptions=} options options
244 */
245 constructor(options = {}) {
246 super();
247 if (!options) options = {};
248 /** @type {WatchOptions} */
249 this.options = options;
250 this.aggregateTimeout =
251 typeof options.aggregateTimeout === "number"
252 ? options.aggregateTimeout
253 : 200;
254 /** @type {NormalizedWatchOptions} */
255 this.watcherOptions = cachedNormalizeOptions(options);
256 /** @type {WatcherManager} */
257 this.watcherManager = getWatcherManager(this.watcherOptions);
258 /** @type {Map<string, WatchpackFileWatcher>} */
259 this.fileWatchers = new Map();
260 /** @type {Map<string, WatchpackDirectoryWatcher>} */
261 this.directoryWatchers = new Map();
262 /** @type {Set<string>} */
263 this._missing = new Set();
264 this.startTime = undefined;
265 this.paused = false;
266 /** @type {Changes} */
267 this.aggregatedChanges = new Set();
268 /** @type {Removals} */
269 this.aggregatedRemovals = new Set();
270 /** @type {undefined | NodeJS.Timeout} */
271 this.aggregateTimer = undefined;
272 this._onTimeout = this._onTimeout.bind(this);
273 }
274
275 /**
276 * @overload
277 * @param {Iterable<string>} arg1 files
278 * @param {Iterable<string>} arg2 directories
279 * @param {number=} arg3 startTime
280 * @returns {void}
281 */
282 /**
283 * @overload
284 * @param {WatchMethodOptions} arg1 watch options
285 * @returns {void}
286 */
287 /**
288 * @param {Iterable<string> | WatchMethodOptions} arg1 files
289 * @param {Iterable<string>=} arg2 directories
290 * @param {number=} arg3 startTime
291 * @returns {void}
292 */
293 watch(arg1, arg2, arg3) {
294 /** @type {Iterable<string> | undefined} */
295 let files;
296 /** @type {Iterable<string> | undefined} */
297 let directories;
298 /** @type {Iterable<string> | undefined} */
299 let missing;
300 /** @type {number | undefined} */
301 let startTime;
302 if (!arg2) {
303 ({
304 files = [],
305 directories = [],
306 missing = [],
307 startTime,
308 } = /** @type {WatchMethodOptions} */ (arg1));
309 } else {
310 files = /** @type {Iterable<string>} */ (arg1);
311 directories = /** @type {Iterable<string>} */ (arg2);
312 missing = [];
313 startTime = /** @type {number} */ (arg3);
314 }
315 this.paused = false;
316 const { fileWatchers, directoryWatchers } = this;
317 const { ignored } = this.watcherOptions;
318 /**
319 * @param {string} path path
320 * @returns {boolean} true when need to filter, otherwise false
321 */
322 const filter = (path) => !ignored(path);
323 /**
324 * @template K, V
325 * @param {Map<K, V | V[]>} map map
326 * @param {K} key key
327 * @param {V} item item
328 */
329 const addToMap = (map, key, item) => {
330 const list = map.get(key);
331 if (list === undefined) {
332 map.set(key, item);
333 } else if (Array.isArray(list)) {
334 list.push(item);
335 } else {
336 map.set(key, [list, item]);
337 }
338 };
339 const fileWatchersNeeded = new Map();
340 const directoryWatchersNeeded = new Map();
341 /** @type {Set<string>} */
342 const missingFiles = new Set();
343 if (this.watcherOptions.followSymlinks) {
344 const resolver = new LinkResolver();
345 for (const file of files) {
346 if (filter(file)) {
347 for (const innerFile of resolver.resolve(file)) {
348 if (file === innerFile || filter(innerFile)) {
349 addToMap(fileWatchersNeeded, innerFile, file);
350 }
351 }
352 }
353 }
354 for (const file of missing) {
355 if (filter(file)) {
356 for (const innerFile of resolver.resolve(file)) {
357 if (file === innerFile || filter(innerFile)) {
358 missingFiles.add(file);
359 addToMap(fileWatchersNeeded, innerFile, file);
360 }
361 }
362 }
363 }
364 for (const dir of directories) {
365 if (filter(dir)) {
366 let first = true;
367 for (const innerItem of resolver.resolve(dir)) {
368 if (filter(innerItem)) {
369 addToMap(
370 first ? directoryWatchersNeeded : fileWatchersNeeded,
371 innerItem,
372 dir,
373 );
374 }
375 first = false;
376 }
377 }
378 }
379 } else {
380 for (const file of files) {
381 if (filter(file)) {
382 addToMap(fileWatchersNeeded, file, file);
383 }
384 }
385 for (const file of missing) {
386 if (filter(file)) {
387 missingFiles.add(file);
388 addToMap(fileWatchersNeeded, file, file);
389 }
390 }
391 for (const dir of directories) {
392 if (filter(dir)) {
393 addToMap(directoryWatchersNeeded, dir, dir);
394 }
395 }
396 }
397 // Close unneeded old watchers
398 // and update existing watchers
399 for (const [key, w] of fileWatchers) {
400 const needed = fileWatchersNeeded.get(key);
401 if (needed === undefined) {
402 w.close();
403 fileWatchers.delete(key);
404 } else {
405 w.update(needed);
406 fileWatchersNeeded.delete(key);
407 }
408 }
409 for (const [key, w] of directoryWatchers) {
410 const needed = directoryWatchersNeeded.get(key);
411 if (needed === undefined) {
412 w.close();
413 directoryWatchers.delete(key);
414 } else {
415 w.update(needed);
416 directoryWatchersNeeded.delete(key);
417 }
418 }
419 // Create new watchers and install handlers on these watchers
420 watchEventSource.batch(() => {
421 for (const [key, files] of fileWatchersNeeded) {
422 const watcher = this.watcherManager.watchFile(key, startTime);
423 if (watcher) {
424 fileWatchers.set(key, new WatchpackFileWatcher(this, watcher, files));
425 }
426 }
427 for (const [key, directories] of directoryWatchersNeeded) {
428 const watcher = this.watcherManager.watchDirectory(key, startTime);
429 if (watcher) {
430 directoryWatchers.set(
431 key,
432 new WatchpackDirectoryWatcher(this, watcher, directories),
433 );
434 }
435 }
436 });
437 this._missing = missingFiles;
438 this.startTime = startTime;
439 }
440
441 close() {
442 this.paused = true;
443 if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
444 for (const w of this.fileWatchers.values()) w.close();
445 for (const w of this.directoryWatchers.values()) w.close();
446 this.fileWatchers.clear();
447 this.directoryWatchers.clear();
448 }
449
450 pause() {
451 this.paused = true;
452 if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
453 }
454
455 /**
456 * @returns {Record<string, number>} times
457 */
458 getTimes() {
459 /** @type {Set<DirectoryWatcher>} */
460 const directoryWatchers = new Set();
461 addWatchersToSet(this.fileWatchers.values(), directoryWatchers);
462 addWatchersToSet(this.directoryWatchers.values(), directoryWatchers);
463 /** @type {Record<string, number>} */
464 const obj = Object.create(null);
465 for (const w of directoryWatchers) {
466 const times = w.getTimes();
467 for (const file of Object.keys(times)) obj[file] = times[file];
468 }
469 return obj;
470 }
471
472 /**
473 * @returns {TimeInfoEntries} time info entries
474 */
475 getTimeInfoEntries() {
476 /** @type {TimeInfoEntries} */
477 const map = new Map();
478 this.collectTimeInfoEntries(map, map);
479 return map;
480 }
481
482 /**
483 * @param {TimeInfoEntries} fileTimestamps file timestamps
484 * @param {TimeInfoEntries} directoryTimestamps directory timestamps
485 */
486 collectTimeInfoEntries(fileTimestamps, directoryTimestamps) {
487 /** @type {Set<DirectoryWatcher>} */
488 const allWatchers = new Set();
489 addWatchersToSet(this.fileWatchers.values(), allWatchers);
490 addWatchersToSet(this.directoryWatchers.values(), allWatchers);
491 for (const w of allWatchers) {
492 w.collectTimeInfoEntries(fileTimestamps, directoryTimestamps);
493 }
494 }
495
496 /**
497 * @returns {Aggregated} aggregated info
498 */
499 getAggregated() {
500 if (this.aggregateTimer) {
501 clearTimeout(this.aggregateTimer);
502 this.aggregateTimer = undefined;
503 }
504 const changes = this.aggregatedChanges;
505 const removals = this.aggregatedRemovals;
506 this.aggregatedChanges = new Set();
507 this.aggregatedRemovals = new Set();
508 return { changes, removals };
509 }
510
511 /**
512 * @param {string} item item
513 * @param {number} mtime mtime
514 * @param {string} file file
515 * @param {EventType} type type
516 */
517 _onChange(item, mtime, file, type) {
518 file = file || item;
519 if (!this.paused) {
520 this.emit("change", file, mtime, type);
521 if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
522 this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout);
523 }
524 this.aggregatedRemovals.delete(item);
525 this.aggregatedChanges.add(item);
526 }
527
528 /**
529 * @param {string} item item
530 * @param {string} file file
531 * @param {EventType} type type
532 */
533 _onRemove(item, file, type) {
534 file = file || item;
535 if (!this.paused) {
536 this.emit("remove", file, type);
537 if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
538 this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout);
539 }
540 this.aggregatedChanges.delete(item);
541 this.aggregatedRemovals.add(item);
542 }
543
544 _onTimeout() {
545 this.aggregateTimer = undefined;
546 const changes = this.aggregatedChanges;
547 const removals = this.aggregatedRemovals;
548 this.aggregatedChanges = new Set();
549 this.aggregatedRemovals = new Set();
550 this.emit("aggregated", changes, removals);
551 }
552}
553
554module.exports = Watchpack;
Note: See TracBrowser for help on using the repository browser.