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