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