source: frontend/node_modules/chokidar/lib/fsevents-handler.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: 16.0 KB
Line 
1'use strict';
2
3const fs = require('fs');
4const sysPath = require('path');
5const { promisify } = require('util');
6
7let fsevents;
8try {
9 fsevents = require('fsevents');
10} catch (error) {
11 if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error);
12}
13
14if (fsevents) {
15 // TODO: real check
16 const mtch = process.version.match(/v(\d+)\.(\d+)/);
17 if (mtch && mtch[1] && mtch[2]) {
18 const maj = Number.parseInt(mtch[1], 10);
19 const min = Number.parseInt(mtch[2], 10);
20 if (maj === 8 && min < 16) {
21 fsevents = undefined;
22 }
23 }
24}
25
26const {
27 EV_ADD,
28 EV_CHANGE,
29 EV_ADD_DIR,
30 EV_UNLINK,
31 EV_ERROR,
32 STR_DATA,
33 STR_END,
34 FSEVENT_CREATED,
35 FSEVENT_MODIFIED,
36 FSEVENT_DELETED,
37 FSEVENT_MOVED,
38 // FSEVENT_CLONED,
39 FSEVENT_UNKNOWN,
40 FSEVENT_FLAG_MUST_SCAN_SUBDIRS,
41 FSEVENT_TYPE_FILE,
42 FSEVENT_TYPE_DIRECTORY,
43 FSEVENT_TYPE_SYMLINK,
44
45 ROOT_GLOBSTAR,
46 DIR_SUFFIX,
47 DOT_SLASH,
48 FUNCTION_TYPE,
49 EMPTY_FN,
50 IDENTITY_FN
51} = require('./constants');
52
53const Depth = (value) => isNaN(value) ? {} : {depth: value};
54
55const stat = promisify(fs.stat);
56const lstat = promisify(fs.lstat);
57const realpath = promisify(fs.realpath);
58
59const statMethods = { stat, lstat };
60
61/**
62 * @typedef {String} Path
63 */
64
65/**
66 * @typedef {Object} FsEventsWatchContainer
67 * @property {Set<Function>} listeners
68 * @property {Function} rawEmitter
69 * @property {{stop: Function}} watcher
70 */
71
72// fsevents instance helper functions
73/**
74 * Object to hold per-process fsevents instances (may be shared across chokidar FSWatcher instances)
75 * @type {Map<Path,FsEventsWatchContainer>}
76 */
77const FSEventsWatchers = new Map();
78
79// Threshold of duplicate path prefixes at which to start
80// consolidating going forward
81const consolidateThreshhold = 10;
82
83const wrongEventFlags = new Set([
84 69888, 70400, 71424, 72704, 73472, 131328, 131840, 262912
85]);
86
87/**
88 * Instantiates the fsevents interface
89 * @param {Path} path path to be watched
90 * @param {Function} callback called when fsevents is bound and ready
91 * @returns {{stop: Function}} new fsevents instance
92 */
93const createFSEventsInstance = (path, callback) => {
94 const stop = fsevents.watch(path, callback);
95 return {stop};
96};
97
98/**
99 * Instantiates the fsevents interface or binds listeners to an existing one covering
100 * the same file tree.
101 * @param {Path} path - to be watched
102 * @param {Path} realPath - real path for symlinks
103 * @param {Function} listener - called when fsevents emits events
104 * @param {Function} rawEmitter - passes data to listeners of the 'raw' event
105 * @returns {Function} closer
106 */
107function setFSEventsListener(path, realPath, listener, rawEmitter) {
108 let watchPath = sysPath.extname(realPath) ? sysPath.dirname(realPath) : realPath;
109
110 const parentPath = sysPath.dirname(watchPath);
111 let cont = FSEventsWatchers.get(watchPath);
112
113 // If we've accumulated a substantial number of paths that
114 // could have been consolidated by watching one directory
115 // above the current one, create a watcher on the parent
116 // path instead, so that we do consolidate going forward.
117 if (couldConsolidate(parentPath)) {
118 watchPath = parentPath;
119 }
120
121 const resolvedPath = sysPath.resolve(path);
122 const hasSymlink = resolvedPath !== realPath;
123
124 const filteredListener = (fullPath, flags, info) => {
125 if (hasSymlink) fullPath = fullPath.replace(realPath, resolvedPath);
126 if (
127 fullPath === resolvedPath ||
128 !fullPath.indexOf(resolvedPath + sysPath.sep)
129 ) listener(fullPath, flags, info);
130 };
131
132 // check if there is already a watcher on a parent path
133 // modifies `watchPath` to the parent path when it finds a match
134 let watchedParent = false;
135 for (const watchedPath of FSEventsWatchers.keys()) {
136 if (realPath.indexOf(sysPath.resolve(watchedPath) + sysPath.sep) === 0) {
137 watchPath = watchedPath;
138 cont = FSEventsWatchers.get(watchPath);
139 watchedParent = true;
140 break;
141 }
142 }
143
144 if (cont || watchedParent) {
145 cont.listeners.add(filteredListener);
146 } else {
147 cont = {
148 listeners: new Set([filteredListener]),
149 rawEmitter,
150 watcher: createFSEventsInstance(watchPath, (fullPath, flags) => {
151 if (!cont.listeners.size) return;
152 if (flags & FSEVENT_FLAG_MUST_SCAN_SUBDIRS) return;
153 const info = fsevents.getInfo(fullPath, flags);
154 cont.listeners.forEach(list => {
155 list(fullPath, flags, info);
156 });
157
158 cont.rawEmitter(info.event, fullPath, info);
159 })
160 };
161 FSEventsWatchers.set(watchPath, cont);
162 }
163
164 // removes this instance's listeners and closes the underlying fsevents
165 // instance if there are no more listeners left
166 return () => {
167 const lst = cont.listeners;
168
169 lst.delete(filteredListener);
170 if (!lst.size) {
171 FSEventsWatchers.delete(watchPath);
172 if (cont.watcher) return cont.watcher.stop().then(() => {
173 cont.rawEmitter = cont.watcher = undefined;
174 Object.freeze(cont);
175 });
176 }
177 };
178}
179
180// Decide whether or not we should start a new higher-level
181// parent watcher
182const couldConsolidate = (path) => {
183 let count = 0;
184 for (const watchPath of FSEventsWatchers.keys()) {
185 if (watchPath.indexOf(path) === 0) {
186 count++;
187 if (count >= consolidateThreshhold) {
188 return true;
189 }
190 }
191 }
192
193 return false;
194};
195
196// returns boolean indicating whether fsevents can be used
197const canUse = () => fsevents && FSEventsWatchers.size < 128;
198
199// determines subdirectory traversal levels from root to path
200const calcDepth = (path, root) => {
201 let i = 0;
202 while (!path.indexOf(root) && (path = sysPath.dirname(path)) !== root) i++;
203 return i;
204};
205
206// returns boolean indicating whether the fsevents' event info has the same type
207// as the one returned by fs.stat
208const sameTypes = (info, stats) => (
209 info.type === FSEVENT_TYPE_DIRECTORY && stats.isDirectory() ||
210 info.type === FSEVENT_TYPE_SYMLINK && stats.isSymbolicLink() ||
211 info.type === FSEVENT_TYPE_FILE && stats.isFile()
212)
213
214/**
215 * @mixin
216 */
217class FsEventsHandler {
218
219/**
220 * @param {import('../index').FSWatcher} fsw
221 */
222constructor(fsw) {
223 this.fsw = fsw;
224}
225checkIgnored(path, stats) {
226 const ipaths = this.fsw._ignoredPaths;
227 if (this.fsw._isIgnored(path, stats)) {
228 ipaths.add(path);
229 if (stats && stats.isDirectory()) {
230 ipaths.add(path + ROOT_GLOBSTAR);
231 }
232 return true;
233 }
234
235 ipaths.delete(path);
236 ipaths.delete(path + ROOT_GLOBSTAR);
237}
238
239addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts) {
240 const event = watchedDir.has(item) ? EV_CHANGE : EV_ADD;
241 this.handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts);
242}
243
244async checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts) {
245 try {
246 const stats = await stat(path)
247 if (this.fsw.closed) return;
248 if (sameTypes(info, stats)) {
249 this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
250 } else {
251 this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
252 }
253 } catch (error) {
254 if (error.code === 'EACCES') {
255 this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
256 } else {
257 this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
258 }
259 }
260}
261
262handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts) {
263 if (this.fsw.closed || this.checkIgnored(path)) return;
264
265 if (event === EV_UNLINK) {
266 const isDirectory = info.type === FSEVENT_TYPE_DIRECTORY
267 // suppress unlink events on never before seen files
268 if (isDirectory || watchedDir.has(item)) {
269 this.fsw._remove(parent, item, isDirectory);
270 }
271 } else {
272 if (event === EV_ADD) {
273 // track new directories
274 if (info.type === FSEVENT_TYPE_DIRECTORY) this.fsw._getWatchedDir(path);
275
276 if (info.type === FSEVENT_TYPE_SYMLINK && opts.followSymlinks) {
277 // push symlinks back to the top of the stack to get handled
278 const curDepth = opts.depth === undefined ?
279 undefined : calcDepth(fullPath, realPath) + 1;
280 return this._addToFsEvents(path, false, true, curDepth);
281 }
282
283 // track new paths
284 // (other than symlinks being followed, which will be tracked soon)
285 this.fsw._getWatchedDir(parent).add(item);
286 }
287 /**
288 * @type {'add'|'addDir'|'unlink'|'unlinkDir'}
289 */
290 const eventName = info.type === FSEVENT_TYPE_DIRECTORY ? event + DIR_SUFFIX : event;
291 this.fsw._emit(eventName, path);
292 if (eventName === EV_ADD_DIR) this._addToFsEvents(path, false, true);
293 }
294}
295
296/**
297 * Handle symlinks encountered during directory scan
298 * @param {String} watchPath - file/dir path to be watched with fsevents
299 * @param {String} realPath - real path (in case of symlinks)
300 * @param {Function} transform - path transformer
301 * @param {Function} globFilter - path filter in case a glob pattern was provided
302 * @returns {Function} closer for the watcher instance
303*/
304_watchWithFsEvents(watchPath, realPath, transform, globFilter) {
305 if (this.fsw.closed || this.fsw._isIgnored(watchPath)) return;
306 const opts = this.fsw.options;
307 const watchCallback = async (fullPath, flags, info) => {
308 if (this.fsw.closed) return;
309 if (
310 opts.depth !== undefined &&
311 calcDepth(fullPath, realPath) > opts.depth
312 ) return;
313 const path = transform(sysPath.join(
314 watchPath, sysPath.relative(watchPath, fullPath)
315 ));
316 if (globFilter && !globFilter(path)) return;
317 // ensure directories are tracked
318 const parent = sysPath.dirname(path);
319 const item = sysPath.basename(path);
320 const watchedDir = this.fsw._getWatchedDir(
321 info.type === FSEVENT_TYPE_DIRECTORY ? path : parent
322 );
323
324 // correct for wrong events emitted
325 if (wrongEventFlags.has(flags) || info.event === FSEVENT_UNKNOWN) {
326 if (typeof opts.ignored === FUNCTION_TYPE) {
327 let stats;
328 try {
329 stats = await stat(path);
330 } catch (error) {}
331 if (this.fsw.closed) return;
332 if (this.checkIgnored(path, stats)) return;
333 if (sameTypes(info, stats)) {
334 this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
335 } else {
336 this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
337 }
338 } else {
339 this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts);
340 }
341 } else {
342 switch (info.event) {
343 case FSEVENT_CREATED:
344 case FSEVENT_MODIFIED:
345 return this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
346 case FSEVENT_DELETED:
347 case FSEVENT_MOVED:
348 return this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts);
349 }
350 }
351 };
352
353 const closer = setFSEventsListener(
354 watchPath,
355 realPath,
356 watchCallback,
357 this.fsw._emitRaw
358 );
359
360 this.fsw._emitReady();
361 return closer;
362}
363
364/**
365 * Handle symlinks encountered during directory scan
366 * @param {String} linkPath path to symlink
367 * @param {String} fullPath absolute path to the symlink
368 * @param {Function} transform pre-existing path transformer
369 * @param {Number} curDepth level of subdirectories traversed to where symlink is
370 * @returns {Promise<void>}
371 */
372async _handleFsEventsSymlink(linkPath, fullPath, transform, curDepth) {
373 // don't follow the same symlink more than once
374 if (this.fsw.closed || this.fsw._symlinkPaths.has(fullPath)) return;
375
376 this.fsw._symlinkPaths.set(fullPath, true);
377 this.fsw._incrReadyCount();
378
379 try {
380 const linkTarget = await realpath(linkPath);
381 if (this.fsw.closed) return;
382 if (this.fsw._isIgnored(linkTarget)) {
383 return this.fsw._emitReady();
384 }
385
386 this.fsw._incrReadyCount();
387
388 // add the linkTarget for watching with a wrapper for transform
389 // that causes emitted paths to incorporate the link's path
390 this._addToFsEvents(linkTarget || linkPath, (path) => {
391 let aliasedPath = linkPath;
392 if (linkTarget && linkTarget !== DOT_SLASH) {
393 aliasedPath = path.replace(linkTarget, linkPath);
394 } else if (path !== DOT_SLASH) {
395 aliasedPath = sysPath.join(linkPath, path);
396 }
397 return transform(aliasedPath);
398 }, false, curDepth);
399 } catch(error) {
400 if (this.fsw._handleError(error)) {
401 return this.fsw._emitReady();
402 }
403 }
404}
405
406/**
407 *
408 * @param {Path} newPath
409 * @param {fs.Stats} stats
410 */
411emitAdd(newPath, stats, processPath, opts, forceAdd) {
412 const pp = processPath(newPath);
413 const isDir = stats.isDirectory();
414 const dirObj = this.fsw._getWatchedDir(sysPath.dirname(pp));
415 const base = sysPath.basename(pp);
416
417 // ensure empty dirs get tracked
418 if (isDir) this.fsw._getWatchedDir(pp);
419 if (dirObj.has(base)) return;
420 dirObj.add(base);
421
422 if (!opts.ignoreInitial || forceAdd === true) {
423 this.fsw._emit(isDir ? EV_ADD_DIR : EV_ADD, pp, stats);
424 }
425}
426
427initWatch(realPath, path, wh, processPath) {
428 if (this.fsw.closed) return;
429 const closer = this._watchWithFsEvents(
430 wh.watchPath,
431 sysPath.resolve(realPath || wh.watchPath),
432 processPath,
433 wh.globFilter
434 );
435 this.fsw._addPathCloser(path, closer);
436}
437
438/**
439 * Handle added path with fsevents
440 * @param {String} path file/dir path or glob pattern
441 * @param {Function|Boolean=} transform converts working path to what the user expects
442 * @param {Boolean=} forceAdd ensure add is emitted
443 * @param {Number=} priorDepth Level of subdirectories already traversed.
444 * @returns {Promise<void>}
445 */
446async _addToFsEvents(path, transform, forceAdd, priorDepth) {
447 if (this.fsw.closed) {
448 return;
449 }
450 const opts = this.fsw.options;
451 const processPath = typeof transform === FUNCTION_TYPE ? transform : IDENTITY_FN;
452
453 const wh = this.fsw._getWatchHelpers(path);
454
455 // evaluate what is at the path we're being asked to watch
456 try {
457 const stats = await statMethods[wh.statMethod](wh.watchPath);
458 if (this.fsw.closed) return;
459 if (this.fsw._isIgnored(wh.watchPath, stats)) {
460 throw null;
461 }
462 if (stats.isDirectory()) {
463 // emit addDir unless this is a glob parent
464 if (!wh.globFilter) this.emitAdd(processPath(path), stats, processPath, opts, forceAdd);
465
466 // don't recurse further if it would exceed depth setting
467 if (priorDepth && priorDepth > opts.depth) return;
468
469 // scan the contents of the dir
470 this.fsw._readdirp(wh.watchPath, {
471 fileFilter: entry => wh.filterPath(entry),
472 directoryFilter: entry => wh.filterDir(entry),
473 ...Depth(opts.depth - (priorDepth || 0))
474 }).on(STR_DATA, (entry) => {
475 // need to check filterPath on dirs b/c filterDir is less restrictive
476 if (this.fsw.closed) {
477 return;
478 }
479 if (entry.stats.isDirectory() && !wh.filterPath(entry)) return;
480
481 const joinedPath = sysPath.join(wh.watchPath, entry.path);
482 const {fullPath} = entry;
483
484 if (wh.followSymlinks && entry.stats.isSymbolicLink()) {
485 // preserve the current depth here since it can't be derived from
486 // real paths past the symlink
487 const curDepth = opts.depth === undefined ?
488 undefined : calcDepth(joinedPath, sysPath.resolve(wh.watchPath)) + 1;
489
490 this._handleFsEventsSymlink(joinedPath, fullPath, processPath, curDepth);
491 } else {
492 this.emitAdd(joinedPath, entry.stats, processPath, opts, forceAdd);
493 }
494 }).on(EV_ERROR, EMPTY_FN).on(STR_END, () => {
495 this.fsw._emitReady();
496 });
497 } else {
498 this.emitAdd(wh.watchPath, stats, processPath, opts, forceAdd);
499 this.fsw._emitReady();
500 }
501 } catch (error) {
502 if (!error || this.fsw._handleError(error)) {
503 // TODO: Strange thing: "should not choke on an ignored watch path" will be failed without 2 ready calls -__-
504 this.fsw._emitReady();
505 this.fsw._emitReady();
506 }
507 }
508
509 if (opts.persistent && forceAdd !== true) {
510 if (typeof transform === FUNCTION_TYPE) {
511 // realpath has already been resolved
512 this.initWatch(undefined, path, wh, processPath);
513 } else {
514 let realPath;
515 try {
516 realPath = await realpath(wh.watchPath);
517 } catch (e) {}
518 this.initWatch(realPath, path, wh, processPath);
519 }
520 }
521}
522
523}
524
525module.exports = FsEventsHandler;
526module.exports.canUse = canUse;
Note: See TracBrowser for help on using the repository browser.