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