• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts {
3    /**
4     * Partial interface of the System thats needed to support the caching of directory structure
5     */
6    export interface DirectoryStructureHost {
7        fileExists(path: string): boolean;
8        readFile(path: string, encoding?: string): string | undefined;
9
10        // TODO: GH#18217 Optional methods are frequently used as non-optional
11        directoryExists?(path: string): boolean;
12        getDirectories?(path: string): string[];
13        readDirectory?(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[];
14        realpath?(path: string): string;
15
16        createDirectory?(path: string): void;
17        writeFile?(path: string, data: string, writeByteOrderMark?: boolean): void;
18    }
19
20    interface FileAndDirectoryExistence {
21        fileExists: boolean;
22        directoryExists: boolean;
23    }
24
25    export interface CachedDirectoryStructureHost extends DirectoryStructureHost {
26        useCaseSensitiveFileNames: boolean;
27
28        getDirectories(path: string): string[];
29        readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[];
30
31        /** Returns the queried result for the file exists and directory exists if at all it was done */
32        addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path): FileAndDirectoryExistence | undefined;
33        addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind): void;
34        clearCache(): void;
35    }
36
37    interface MutableFileSystemEntries {
38        readonly files: string[];
39        readonly directories: string[];
40    }
41
42    export function createCachedDirectoryStructureHost(host: DirectoryStructureHost, currentDirectory: string, useCaseSensitiveFileNames: boolean): CachedDirectoryStructureHost | undefined {
43        if (!host.getDirectories || !host.readDirectory) {
44            return undefined;
45        }
46
47        const cachedReadDirectoryResult = new Map<string, MutableFileSystemEntries>();
48        const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
49        return {
50            useCaseSensitiveFileNames,
51            fileExists,
52            readFile: (path, encoding) => host.readFile(path, encoding),
53            directoryExists: host.directoryExists && directoryExists,
54            getDirectories,
55            readDirectory,
56            createDirectory: host.createDirectory && createDirectory,
57            writeFile: host.writeFile && writeFile,
58            addOrDeleteFileOrDirectory,
59            addOrDeleteFile,
60            clearCache,
61            realpath: host.realpath && realpath
62        };
63
64        function toPath(fileName: string) {
65            return ts.toPath(fileName, currentDirectory, getCanonicalFileName);
66        }
67
68        function getCachedFileSystemEntries(rootDirPath: Path): MutableFileSystemEntries | undefined {
69            return cachedReadDirectoryResult.get(ensureTrailingDirectorySeparator(rootDirPath));
70        }
71
72        function getCachedFileSystemEntriesForBaseDir(path: Path): MutableFileSystemEntries | undefined {
73            return getCachedFileSystemEntries(getDirectoryPath(path));
74        }
75
76        function getBaseNameOfFileName(fileName: string) {
77            return getBaseFileName(normalizePath(fileName));
78        }
79
80        function createCachedFileSystemEntries(rootDir: string, rootDirPath: Path) {
81            const resultFromHost: MutableFileSystemEntries = {
82                files: map(host.readDirectory!(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]), getBaseNameOfFileName) || [],
83                directories: host.getDirectories!(rootDir) || []
84            };
85
86            cachedReadDirectoryResult.set(ensureTrailingDirectorySeparator(rootDirPath), resultFromHost);
87            return resultFromHost;
88        }
89
90        /**
91         * If the readDirectory result was already cached, it returns that
92         * Otherwise gets result from host and caches it.
93         * The host request is done under try catch block to avoid caching incorrect result
94         */
95        function tryReadDirectory(rootDir: string, rootDirPath: Path): MutableFileSystemEntries | undefined {
96            rootDirPath = ensureTrailingDirectorySeparator(rootDirPath);
97            const cachedResult = getCachedFileSystemEntries(rootDirPath);
98            if (cachedResult) {
99                return cachedResult;
100            }
101
102            try {
103                return createCachedFileSystemEntries(rootDir, rootDirPath);
104            }
105            catch (_e) {
106                // If there is exception to read directories, dont cache the result and direct the calls to host
107                Debug.assert(!cachedReadDirectoryResult.has(ensureTrailingDirectorySeparator(rootDirPath)));
108                return undefined;
109            }
110        }
111
112        function fileNameEqual(name1: string, name2: string) {
113            return getCanonicalFileName(name1) === getCanonicalFileName(name2);
114        }
115
116        function hasEntry(entries: readonly string[], name: string) {
117            return some(entries, file => fileNameEqual(file, name));
118        }
119
120        function updateFileSystemEntry(entries: string[], baseName: string, isValid: boolean) {
121            if (hasEntry(entries, baseName)) {
122                if (!isValid) {
123                    return filterMutate(entries, entry => !fileNameEqual(entry, baseName));
124                }
125            }
126            else if (isValid) {
127                return entries.push(baseName);
128            }
129        }
130
131        function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void {
132            const path = toPath(fileName);
133            const result = getCachedFileSystemEntriesForBaseDir(path);
134            if (result) {
135                updateFilesOfFileSystemEntry(result, getBaseNameOfFileName(fileName), /*fileExists*/ true);
136            }
137            return host.writeFile!(fileName, data, writeByteOrderMark);
138        }
139
140        function fileExists(fileName: string): boolean {
141            const path = toPath(fileName);
142            const result = getCachedFileSystemEntriesForBaseDir(path);
143            return result && hasEntry(result.files, getBaseNameOfFileName(fileName)) ||
144                host.fileExists(fileName);
145        }
146
147        function directoryExists(dirPath: string): boolean {
148            const path = toPath(dirPath);
149            return cachedReadDirectoryResult.has(ensureTrailingDirectorySeparator(path)) || host.directoryExists!(dirPath);
150        }
151
152        function createDirectory(dirPath: string) {
153            const path = toPath(dirPath);
154            const result = getCachedFileSystemEntriesForBaseDir(path);
155            const baseFileName = getBaseNameOfFileName(dirPath);
156            if (result) {
157                updateFileSystemEntry(result.directories, baseFileName, /*isValid*/ true);
158            }
159            host.createDirectory!(dirPath);
160        }
161
162        function getDirectories(rootDir: string): string[] {
163            const rootDirPath = toPath(rootDir);
164            const result = tryReadDirectory(rootDir, rootDirPath);
165            if (result) {
166                return result.directories.slice();
167            }
168            return host.getDirectories!(rootDir);
169        }
170
171        function readDirectory(rootDir: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] {
172            const rootDirPath = toPath(rootDir);
173            const result = tryReadDirectory(rootDir, rootDirPath);
174            if (result) {
175                return matchFiles(rootDir, extensions, excludes, includes, useCaseSensitiveFileNames, currentDirectory, depth, getFileSystemEntries, realpath);
176            }
177            return host.readDirectory!(rootDir, extensions, excludes, includes, depth);
178
179            function getFileSystemEntries(dir: string): FileSystemEntries {
180                const path = toPath(dir);
181                if (path === rootDirPath) {
182                    return result!;
183                }
184                return tryReadDirectory(dir, path) || emptyFileSystemEntries;
185            }
186        }
187
188        function realpath(s: string) {
189            return host.realpath ? host.realpath(s) : s;
190        }
191
192        function addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path) {
193            const existingResult = getCachedFileSystemEntries(fileOrDirectoryPath);
194            if (existingResult) {
195                // Just clear the cache for now
196                // For now just clear the cache, since this could mean that multiple level entries might need to be re-evaluated
197                clearCache();
198                return undefined;
199            }
200
201            const parentResult = getCachedFileSystemEntriesForBaseDir(fileOrDirectoryPath);
202            if (!parentResult) {
203                return undefined;
204            }
205
206            // This was earlier a file (hence not in cached directory contents)
207            // or we never cached the directory containing it
208
209            if (!host.directoryExists) {
210                // Since host doesnt support directory exists, clear the cache as otherwise it might not be same
211                clearCache();
212                return undefined;
213            }
214
215            const baseName = getBaseNameOfFileName(fileOrDirectory);
216            const fsQueryResult: FileAndDirectoryExistence = {
217                fileExists: host.fileExists(fileOrDirectoryPath),
218                directoryExists: host.directoryExists(fileOrDirectoryPath)
219            };
220            if (fsQueryResult.directoryExists || hasEntry(parentResult.directories, baseName)) {
221                // Folder added or removed, clear the cache instead of updating the folder and its structure
222                clearCache();
223            }
224            else {
225                // No need to update the directory structure, just files
226                updateFilesOfFileSystemEntry(parentResult, baseName, fsQueryResult.fileExists);
227            }
228            return fsQueryResult;
229
230        }
231
232        function addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind) {
233            if (eventKind === FileWatcherEventKind.Changed) {
234                return;
235            }
236
237            const parentResult = getCachedFileSystemEntriesForBaseDir(filePath);
238            if (parentResult) {
239                updateFilesOfFileSystemEntry(parentResult, getBaseNameOfFileName(fileName), eventKind === FileWatcherEventKind.Created);
240            }
241        }
242
243        function updateFilesOfFileSystemEntry(parentResult: MutableFileSystemEntries, baseName: string, fileExists: boolean) {
244            updateFileSystemEntry(parentResult.files, baseName, fileExists);
245        }
246
247        function clearCache() {
248            cachedReadDirectoryResult.clear();
249        }
250    }
251
252    export enum ConfigFileProgramReloadLevel {
253        None,
254        /** Update the file name list from the disk */
255        Partial,
256        /** Reload completely by re-reading contents of config file from disk and updating program */
257        Full
258    }
259
260    export interface SharedExtendedConfigFileWatcher<T> extends FileWatcher {
261        fileWatcher: FileWatcher;
262        projects: Set<T>;
263    }
264
265    /**
266     * Updates the map of shared extended config file watches with a new set of extended config files from a base config file of the project
267     */
268    export function updateSharedExtendedConfigFileWatcher<T>(
269        projectPath: T,
270        parsed: ParsedCommandLine | undefined,
271        extendedConfigFilesMap: ESMap<Path, SharedExtendedConfigFileWatcher<T>>,
272        createExtendedConfigFileWatch: (extendedConfigPath: string, extendedConfigFilePath: Path) => FileWatcher,
273        toPath: (fileName: string) => Path,
274    ) {
275        const extendedConfigs = arrayToMap(parsed?.options.configFile?.extendedSourceFiles || emptyArray, toPath);
276        // remove project from all unrelated watchers
277        extendedConfigFilesMap.forEach((watcher, extendedConfigFilePath) => {
278            if (!extendedConfigs.has(extendedConfigFilePath)) {
279                watcher.projects.delete(projectPath);
280                watcher.close();
281            }
282        });
283        // Update the extended config files watcher
284        extendedConfigs.forEach((extendedConfigFileName, extendedConfigFilePath) => {
285            const existing = extendedConfigFilesMap.get(extendedConfigFilePath);
286            if (existing) {
287                existing.projects.add(projectPath);
288            }
289            else {
290                // start watching previously unseen extended config
291                extendedConfigFilesMap.set(extendedConfigFilePath, {
292                    projects: new Set([projectPath]),
293                    fileWatcher: createExtendedConfigFileWatch(extendedConfigFileName, extendedConfigFilePath),
294                    close: () => {
295                        const existing = extendedConfigFilesMap.get(extendedConfigFilePath);
296                        if (!existing || existing.projects.size !== 0) return;
297                        existing.fileWatcher.close();
298                        extendedConfigFilesMap.delete(extendedConfigFilePath);
299                    },
300                });
301            }
302        });
303    }
304
305    /**
306     * Updates the existing missing file watches with the new set of missing files after new program is created
307     */
308    export function updateMissingFilePathsWatch(
309        program: Program,
310        missingFileWatches: ESMap<Path, FileWatcher>,
311        createMissingFileWatch: (missingFilePath: Path) => FileWatcher,
312    ) {
313        const missingFilePaths = program.getMissingFilePaths();
314        // TODO(rbuckton): Should be a `Set` but that requires changing the below code that uses `mutateMap`
315        const newMissingFilePathMap = arrayToMap(missingFilePaths, identity, returnTrue);
316        // Update the missing file paths watcher
317        mutateMap(
318            missingFileWatches,
319            newMissingFilePathMap,
320            {
321                // Watch the missing files
322                createNewValue: createMissingFileWatch,
323                // Files that are no longer missing (e.g. because they are no longer required)
324                // should no longer be watched.
325                onDeleteValue: closeFileWatcher
326            }
327        );
328    }
329
330    export interface WildcardDirectoryWatcher {
331        watcher: FileWatcher;
332        flags: WatchDirectoryFlags;
333    }
334
335    /**
336     * Updates the existing wild card directory watches with the new set of wild card directories from the config file
337     * after new program is created because the config file was reloaded or program was created first time from the config file
338     * Note that there is no need to call this function when the program is updated with additional files without reloading config files,
339     * as wildcard directories wont change unless reloading config file
340     */
341    export function updateWatchingWildcardDirectories(
342        existingWatchedForWildcards: ESMap<string, WildcardDirectoryWatcher>,
343        wildcardDirectories: ESMap<string, WatchDirectoryFlags>,
344        watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher
345    ) {
346        mutateMap(
347            existingWatchedForWildcards,
348            wildcardDirectories,
349            {
350                // Create new watch and recursive info
351                createNewValue: createWildcardDirectoryWatcher,
352                // Close existing watch thats not needed any more
353                onDeleteValue: closeFileWatcherOf,
354                // Close existing watch that doesnt match in the flags
355                onExistingValue: updateWildcardDirectoryWatcher
356            }
357        );
358
359        function createWildcardDirectoryWatcher(directory: string, flags: WatchDirectoryFlags): WildcardDirectoryWatcher {
360            // Create new watch and recursive info
361            return {
362                watcher: watchDirectory(directory, flags),
363                flags
364            };
365        }
366
367        function updateWildcardDirectoryWatcher(existingWatcher: WildcardDirectoryWatcher, flags: WatchDirectoryFlags, directory: string) {
368            // Watcher needs to be updated if the recursive flags dont match
369            if (existingWatcher.flags === flags) {
370                return;
371            }
372
373            existingWatcher.watcher.close();
374            existingWatchedForWildcards.set(directory, createWildcardDirectoryWatcher(directory, flags));
375        }
376    }
377
378    export interface IsIgnoredFileFromWildCardWatchingInput {
379        watchedDirPath: Path;
380        fileOrDirectory: string;
381        fileOrDirectoryPath: Path;
382        configFileName: string;
383        options: CompilerOptions;
384        program: BuilderProgram | Program | undefined;
385        extraFileExtensions?: readonly FileExtensionInfo[];
386        currentDirectory: string;
387        useCaseSensitiveFileNames: boolean;
388        writeLog: (s: string) => void;
389    }
390    /* @internal */
391    export function isIgnoredFileFromWildCardWatching({
392        watchedDirPath, fileOrDirectory, fileOrDirectoryPath,
393        configFileName, options, program, extraFileExtensions,
394        currentDirectory, useCaseSensitiveFileNames,
395        writeLog,
396    }: IsIgnoredFileFromWildCardWatchingInput): boolean {
397        const newPath = removeIgnoredPath(fileOrDirectoryPath);
398        if (!newPath) {
399            writeLog(`Project: ${configFileName} Detected ignored path: ${fileOrDirectory}`);
400            return true;
401        }
402
403        fileOrDirectoryPath = newPath;
404        if (fileOrDirectoryPath === watchedDirPath) return false;
405
406        // If the the added or created file or directory is not supported file name, ignore the file
407        // But when watched directory is added/removed, we need to reload the file list
408        if (hasExtension(fileOrDirectoryPath) && !isSupportedSourceFileName(fileOrDirectory, options, extraFileExtensions)) {
409            writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrDirectory}`);
410            return true;
411        }
412
413        if (isExcludedFile(fileOrDirectory, options.configFile!.configFileSpecs!, getNormalizedAbsolutePath(getDirectoryPath(configFileName), currentDirectory), useCaseSensitiveFileNames, currentDirectory)) {
414            writeLog(`Project: ${configFileName} Detected excluded file: ${fileOrDirectory}`);
415            return true;
416        }
417
418        if (!program) return false;
419
420        // We want to ignore emit file check if file is not going to be emitted next to source file
421        // In that case we follow config file inclusion rules
422        if (options.outFile || options.outDir) return false;
423
424        // File if emitted next to input needs to be ignored
425        if (isDeclarationFileName(fileOrDirectoryPath)) {
426            // If its declaration directory: its not ignored if not excluded by config
427            if (options.declarationDir) return false;
428        }
429        else if (!fileExtensionIsOneOf(fileOrDirectoryPath, supportedJSExtensions)) {
430            return false;
431        }
432
433        // just check if sourceFile with the name exists
434        const filePathWithoutExtension = removeFileExtension(fileOrDirectoryPath);
435        const realProgram = isBuilderProgram(program) ? program.getProgramOrUndefined() : program;
436        if (hasSourceFile((filePathWithoutExtension + Extension.Ts) as Path) ||
437            hasSourceFile((filePathWithoutExtension + Extension.Tsx) as Path) ||
438            hasSourceFile((filePathWithoutExtension + Extension.Ets) as Path)) {
439            writeLog(`Project: ${configFileName} Detected output file: ${fileOrDirectory}`);
440            return true;
441        }
442        return false;
443
444        function hasSourceFile(file: Path) {
445            return realProgram ?
446                !!realProgram.getSourceFileByPath(file) :
447                (program as BuilderProgram).getState().fileInfos.has(file);
448        }
449    }
450
451    function isBuilderProgram<T extends BuilderProgram>(program: Program | T): program is T {
452        return !!(program as T).getState;
453    }
454
455    export function isEmittedFileOfProgram(program: Program | undefined, file: string) {
456        if (!program) {
457            return false;
458        }
459
460        return program.isEmittedFile(file);
461    }
462
463    export enum WatchLogLevel {
464        None,
465        TriggerOnly,
466        Verbose
467    }
468
469    export interface WatchFactoryHost {
470        watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: WatchOptions): FileWatcher;
471        watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher;
472        getCurrentDirectory?(): string;
473        useCaseSensitiveFileNames: boolean | (() => boolean);
474    }
475
476    export interface WatchFactory<X, Y = undefined> {
477        watchFile: (file: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined, detailInfo1: X, detailInfo2?: Y) => FileWatcher;
478        watchDirectory: (directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags, options: WatchOptions | undefined, detailInfo1: X, detailInfo2?: Y) => FileWatcher;
479    }
480
481    export type GetDetailWatchInfo<X, Y> = (detailInfo1: X, detailInfo2: Y | undefined) => string;
482    export function getWatchFactory<X, Y = undefined>(host: WatchFactoryHost, watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo<X, Y>): WatchFactory<X, Y> {
483        setSysLog(watchLogLevel === WatchLogLevel.Verbose ? log : noop);
484        const plainInvokeFactory: WatchFactory<X, Y> = {
485            watchFile: (file, callback, pollingInterval, options) => host.watchFile(file, callback, pollingInterval, options),
486            watchDirectory: (directory, callback, flags, options) => host.watchDirectory(directory, callback, (flags & WatchDirectoryFlags.Recursive) !== 0, options),
487        };
488        const triggerInvokingFactory: WatchFactory<X, Y> | undefined = watchLogLevel !== WatchLogLevel.None ?
489            {
490                watchFile: createTriggerLoggingAddWatch("watchFile"),
491                watchDirectory: createTriggerLoggingAddWatch("watchDirectory")
492            } :
493            undefined;
494        const factory = watchLogLevel === WatchLogLevel.Verbose ?
495            {
496                watchFile: createFileWatcherWithLogging,
497                watchDirectory: createDirectoryWatcherWithLogging
498            } :
499            triggerInvokingFactory || plainInvokeFactory;
500        const excludeWatcherFactory = watchLogLevel === WatchLogLevel.Verbose ?
501            createExcludeWatcherWithLogging :
502            returnNoopFileWatcher;
503
504        return {
505            watchFile: createExcludeHandlingAddWatch("watchFile"),
506            watchDirectory: createExcludeHandlingAddWatch("watchDirectory")
507        };
508
509        function createExcludeHandlingAddWatch<T extends keyof WatchFactory<X, Y>>(key: T): WatchFactory<X, Y>[T] {
510            return (
511                file: string,
512                cb: FileWatcherCallback | DirectoryWatcherCallback,
513                flags: PollingInterval | WatchDirectoryFlags,
514                options: WatchOptions | undefined,
515                detailInfo1: X,
516                detailInfo2?: Y
517            ) => !matchesExclude(file, key === "watchFile" ? options?.excludeFiles : options?.excludeDirectories, useCaseSensitiveFileNames(), host.getCurrentDirectory?.() || "") ?
518                    factory[key].call(/*thisArgs*/ undefined, file, cb, flags, options, detailInfo1, detailInfo2) :
519                    excludeWatcherFactory(file, flags, options, detailInfo1, detailInfo2);
520        }
521
522        function useCaseSensitiveFileNames() {
523            return typeof host.useCaseSensitiveFileNames === "boolean" ?
524                host.useCaseSensitiveFileNames :
525                host.useCaseSensitiveFileNames();
526        }
527
528        function createExcludeWatcherWithLogging(
529            file: string,
530            flags: PollingInterval | WatchDirectoryFlags,
531            options: WatchOptions | undefined,
532            detailInfo1: X,
533            detailInfo2?: Y
534        ) {
535            log(`ExcludeWatcher:: Added:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`);
536            return {
537                close: () => log(`ExcludeWatcher:: Close:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`)
538            };
539        }
540
541        function createFileWatcherWithLogging(
542            file: string,
543            cb: FileWatcherCallback,
544            flags: PollingInterval,
545            options: WatchOptions | undefined,
546            detailInfo1: X,
547            detailInfo2?: Y
548        ): FileWatcher {
549            log(`FileWatcher:: Added:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`);
550            const watcher = triggerInvokingFactory!.watchFile(file, cb, flags, options, detailInfo1, detailInfo2);
551            return {
552                close: () => {
553                    log(`FileWatcher:: Close:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`);
554                    watcher.close();
555                }
556            };
557        }
558
559        function createDirectoryWatcherWithLogging(
560            file: string,
561            cb: DirectoryWatcherCallback,
562            flags: WatchDirectoryFlags,
563            options: WatchOptions | undefined,
564            detailInfo1: X,
565            detailInfo2?: Y
566        ): FileWatcher {
567            const watchInfo = `DirectoryWatcher:: Added:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`;
568            log(watchInfo);
569            const start = timestamp();
570            const watcher = triggerInvokingFactory!.watchDirectory(file, cb, flags, options, detailInfo1, detailInfo2);
571            const elapsed = timestamp() - start;
572            log(`Elapsed:: ${elapsed}ms ${watchInfo}`);
573            return {
574                close: () => {
575                    const watchInfo = `DirectoryWatcher:: Close:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`;
576                    log(watchInfo);
577                    const start = timestamp();
578                    watcher.close();
579                    const elapsed = timestamp() - start;
580                    log(`Elapsed:: ${elapsed}ms ${watchInfo}`);
581                }
582            };
583        }
584
585        function createTriggerLoggingAddWatch<T extends keyof WatchFactory<X, Y>>(key: T): WatchFactory<X, Y>[T] {
586            return (
587                file: string,
588                cb: FileWatcherCallback | DirectoryWatcherCallback,
589                flags: PollingInterval | WatchDirectoryFlags,
590                options: WatchOptions | undefined,
591                detailInfo1: X,
592                detailInfo2?: Y
593            ) => plainInvokeFactory[key].call(/*thisArgs*/ undefined, file, (...args: any[]) => {
594                const triggerredInfo = `${key === "watchFile" ? "FileWatcher" : "DirectoryWatcher"}:: Triggered with ${args[0]} ${args[1] !== undefined ? args[1] : ""}:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`;
595                log(triggerredInfo);
596                const start = timestamp();
597                cb.call(/*thisArg*/ undefined, ...args);
598                const elapsed = timestamp() - start;
599                log(`Elapsed:: ${elapsed}ms ${triggerredInfo}`);
600            }, flags, options, detailInfo1, detailInfo2);
601        }
602
603        function getWatchInfo<T>(file: string, flags: T, options: WatchOptions | undefined, detailInfo1: X, detailInfo2: Y | undefined, getDetailWatchInfo: GetDetailWatchInfo<X, Y> | undefined) {
604            return `WatchInfo: ${file} ${flags} ${JSON.stringify(options)} ${getDetailWatchInfo ? getDetailWatchInfo(detailInfo1, detailInfo2) : detailInfo2 === undefined ? detailInfo1 : `${detailInfo1} ${detailInfo2}`}`;
605        }
606    }
607
608    export function getFallbackOptions(options: WatchOptions | undefined): WatchOptions {
609        const fallbackPolling = options?.fallbackPolling;
610        return {
611            watchFile: fallbackPolling !== undefined ?
612                fallbackPolling as unknown as WatchFileKind :
613                WatchFileKind.PriorityPollingInterval
614        };
615    }
616
617    export function closeFileWatcherOf<T extends { watcher: FileWatcher; }>(objWithWatcher: T) {
618        objWithWatcher.watcher.close();
619    }
620}
621