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