1/*@internal*/ 2namespace ts { 3 /** This is the cache of module/typedirectives resolution that can be retained across program */ 4 export interface ResolutionCache { 5 startRecordingFilesWithChangedResolutions(): void; 6 finishRecordingFilesWithChangedResolutions(): Path[] | undefined; 7 8 resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference?: ResolvedProjectReference): (ResolvedModuleFull | undefined)[]; 9 getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): CachedResolvedModuleWithFailedLookupLocations | undefined; 10 resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference): (ResolvedTypeReferenceDirective | undefined)[]; 11 12 invalidateResolutionsOfFailedLookupLocations(): boolean; 13 invalidateResolutionOfFile(filePath: Path): void; 14 removeResolutionsOfFile(filePath: Path): void; 15 removeResolutionsFromProjectReferenceRedirects(filePath: Path): void; 16 setFilesWithInvalidatedNonRelativeUnresolvedImports(filesWithUnresolvedImports: ESMap<Path, readonly string[]>): void; 17 createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): HasInvalidatedResolution; 18 hasChangedAutomaticTypeDirectiveNames(): boolean; 19 isFileWithInvalidatedNonRelativeUnresolvedImports(path: Path): boolean; 20 21 22 startCachingPerDirectoryResolution(): void; 23 finishCachingPerDirectoryResolution(): void; 24 25 updateTypeRootsWatch(): void; 26 closeTypeRootsWatch(): void; 27 28 clear(): void; 29 } 30 31 interface ResolutionWithFailedLookupLocations { 32 readonly failedLookupLocations: string[]; 33 isInvalidated?: boolean; 34 refCount?: number; 35 // Files that have this resolution using 36 files?: Path[]; 37 } 38 39 interface ResolutionWithResolvedFileName { 40 resolvedFileName: string | undefined; 41 } 42 43 interface CachedResolvedModuleWithFailedLookupLocations extends ResolvedModuleWithFailedLookupLocations, ResolutionWithFailedLookupLocations { 44 } 45 46 interface CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations extends ResolvedTypeReferenceDirectiveWithFailedLookupLocations, ResolutionWithFailedLookupLocations { 47 } 48 49 export interface ResolutionCacheHost extends ModuleResolutionHost { 50 toPath(fileName: string): Path; 51 getCanonicalFileName: GetCanonicalFileName; 52 getCompilationSettings(): CompilerOptions; 53 watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; 54 onInvalidatedResolution(): void; 55 watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; 56 onChangedAutomaticTypeDirectiveNames(): void; 57 scheduleInvalidateResolutionsOfFailedLookupLocations(): void; 58 getCachedDirectoryStructureHost(): CachedDirectoryStructureHost | undefined; 59 projectName?: string; 60 getGlobalCache?(): string | undefined; 61 globalCacheResolutionModuleName?(externalModuleName: string): string; 62 writeLog(s: string): void; 63 getCurrentProgram(): Program | undefined; 64 fileIsOpen(filePath: Path): boolean; 65 getCompilerHost?(): CompilerHost | undefined; 66 } 67 68 interface DirectoryWatchesOfFailedLookup { 69 /** watcher for the directory of failed lookup */ 70 watcher: FileWatcher; 71 /** ref count keeping this directory watch alive */ 72 refCount: number; 73 /** is the directory watched being non recursive */ 74 nonRecursive?: boolean; 75 } 76 77 interface DirectoryOfFailedLookupWatch { 78 dir: string; 79 dirPath: Path; 80 nonRecursive?: boolean; 81 } 82 83 export function removeIgnoredPath(path: Path): Path | undefined { 84 // Consider whole staging folder as if node_modules changed. 85 if (endsWith(path, "/node_modules/.staging")) { 86 return removeSuffix(path, "/.staging") as Path; 87 } 88 89 return some(ignoredPaths, searchPath => stringContains(path, searchPath)) ? 90 undefined : 91 path; 92 } 93 94 /** 95 * Filter out paths like 96 * "/", "/user", "/user/username", "/user/username/folderAtRoot", 97 * "c:/", "c:/users", "c:/users/username", "c:/users/username/folderAtRoot", "c:/folderAtRoot" 98 * @param dirPath 99 */ 100 export function canWatchDirectory(dirPath: Path) { 101 const rootLength = getRootLength(dirPath); 102 if (dirPath.length === rootLength) { 103 // Ignore "/", "c:/" 104 return false; 105 } 106 107 let nextDirectorySeparator = dirPath.indexOf(directorySeparator, rootLength); 108 if (nextDirectorySeparator === -1) { 109 // ignore "/user", "c:/users" or "c:/folderAtRoot" 110 return false; 111 } 112 113 let pathPartForUserCheck = dirPath.substring(rootLength, nextDirectorySeparator + 1); 114 const isNonDirectorySeparatorRoot = rootLength > 1 || dirPath.charCodeAt(0) !== CharacterCodes.slash; 115 if (isNonDirectorySeparatorRoot && 116 dirPath.search(/[a-zA-Z]:/) !== 0 && // Non dos style paths 117 pathPartForUserCheck.search(/[a-zA-z]\$\//) === 0) { // Dos style nextPart 118 nextDirectorySeparator = dirPath.indexOf(directorySeparator, nextDirectorySeparator + 1); 119 if (nextDirectorySeparator === -1) { 120 // ignore "//vda1cs4850/c$/folderAtRoot" 121 return false; 122 } 123 124 pathPartForUserCheck = dirPath.substring(rootLength + pathPartForUserCheck.length, nextDirectorySeparator + 1); 125 } 126 127 if (isNonDirectorySeparatorRoot && 128 pathPartForUserCheck.search(/users\//i) !== 0) { 129 // Paths like c:/folderAtRoot/subFolder are allowed 130 return true; 131 } 132 133 for (let searchIndex = nextDirectorySeparator + 1, searchLevels = 2; searchLevels > 0; searchLevels--) { 134 searchIndex = dirPath.indexOf(directorySeparator, searchIndex) + 1; 135 if (searchIndex === 0) { 136 // Folder isnt at expected minimum levels 137 return false; 138 } 139 } 140 return true; 141 } 142 143 type GetResolutionWithResolvedFileName<T extends ResolutionWithFailedLookupLocations = ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName = ResolutionWithResolvedFileName> = 144 (resolution: T) => R | undefined; 145 146 export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string | undefined, logChangesWhenResolvingModule: boolean): ResolutionCache { 147 let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; 148 let filesWithInvalidatedResolutions: Set<Path> | undefined; 149 let filesWithInvalidatedNonRelativeUnresolvedImports: ReadonlyESMap<Path, readonly string[]> | undefined; 150 const nonRelativeExternalModuleResolutions = createMultiMap<ResolutionWithFailedLookupLocations>(); 151 152 const resolutionsWithFailedLookups: ResolutionWithFailedLookupLocations[] = []; 153 const resolvedFileToResolution = createMultiMap<ResolutionWithFailedLookupLocations>(); 154 155 let hasChangedAutomaticTypeDirectiveNames = false; 156 const failedLookupChecks: Path[] = []; 157 const startsWithPathChecks: Path[] = []; 158 const isInDirectoryChecks: Path[] = []; 159 160 const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory!()); // TODO: GH#18217 161 const cachedDirectoryStructureHost = resolutionHost.getCachedDirectoryStructureHost(); 162 163 // The resolvedModuleNames and resolvedTypeReferenceDirectives are the cache of resolutions per file. 164 // The key in the map is source file's path. 165 // The values are Map of resolutions with key being name lookedup. 166 const resolvedModuleNames = new Map<Path, ESMap<string, CachedResolvedModuleWithFailedLookupLocations>>(); 167 const perDirectoryResolvedModuleNames: CacheWithRedirects<ESMap<string, CachedResolvedModuleWithFailedLookupLocations>> = createCacheWithRedirects(); 168 const nonRelativeModuleNameCache: CacheWithRedirects<PerModuleNameCache> = createCacheWithRedirects(); 169 const moduleResolutionCache = createModuleResolutionCacheWithMaps( 170 perDirectoryResolvedModuleNames, 171 nonRelativeModuleNameCache, 172 getCurrentDirectory(), 173 resolutionHost.getCanonicalFileName 174 ); 175 176 const resolvedTypeReferenceDirectives = new Map<Path, ESMap<string, CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations>>(); 177 const perDirectoryResolvedTypeReferenceDirectives: CacheWithRedirects<ESMap<string, CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations>> = createCacheWithRedirects(); 178 179 /** 180 * These are the extensions that failed lookup files will have by default, 181 * any other extension of failed lookup will be store that path in custom failed lookup path 182 * This helps in not having to comb through all resolutions when files are added/removed 183 * Note that .d.ts file also has .d.ts extension hence will be part of default extensions 184 */ 185 const failedLookupDefaultExtensions = resolutionHost.getCompilationSettings().ets 186 ? [Extension.Ets, Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx, Extension.Json] 187 : [Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx, Extension.Json, Extension.Ets]; 188 const customFailedLookupPaths = new Map<string, number>(); 189 190 const directoryWatchesOfFailedLookups = new Map<string, DirectoryWatchesOfFailedLookup>(); 191 const rootDir = rootDirForResolution && removeTrailingDirectorySeparator(getNormalizedAbsolutePath(rootDirForResolution, getCurrentDirectory())); 192 const rootPath = (rootDir && resolutionHost.toPath(rootDir)) as Path; // TODO: GH#18217 193 const rootSplitLength = rootPath !== undefined ? rootPath.split(directorySeparator).length : 0; 194 195 // TypeRoot watches for the types that get added as part of getAutomaticTypeDirectiveNames 196 const typeRootsWatches = new Map<string, FileWatcher>(); 197 198 return { 199 startRecordingFilesWithChangedResolutions, 200 finishRecordingFilesWithChangedResolutions, 201 // perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update 202 // (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution) 203 startCachingPerDirectoryResolution: clearPerDirectoryResolutions, 204 finishCachingPerDirectoryResolution, 205 resolveModuleNames, 206 getResolvedModuleWithFailedLookupLocationsFromCache, 207 resolveTypeReferenceDirectives, 208 removeResolutionsFromProjectReferenceRedirects, 209 removeResolutionsOfFile, 210 hasChangedAutomaticTypeDirectiveNames: () => hasChangedAutomaticTypeDirectiveNames, 211 invalidateResolutionOfFile, 212 invalidateResolutionsOfFailedLookupLocations, 213 setFilesWithInvalidatedNonRelativeUnresolvedImports, 214 createHasInvalidatedResolution, 215 isFileWithInvalidatedNonRelativeUnresolvedImports, 216 updateTypeRootsWatch, 217 closeTypeRootsWatch, 218 clear 219 }; 220 221 function getResolvedModule(resolution: CachedResolvedModuleWithFailedLookupLocations) { 222 return resolution.resolvedModule; 223 } 224 225 function getResolvedTypeReferenceDirective(resolution: CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations) { 226 return resolution.resolvedTypeReferenceDirective; 227 } 228 229 function isInDirectoryPath(dir: Path | undefined, file: Path) { 230 if (dir === undefined || file.length <= dir.length) { 231 return false; 232 } 233 return startsWith(file, dir) && file[dir.length] === directorySeparator; 234 } 235 236 function clear() { 237 clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf); 238 customFailedLookupPaths.clear(); 239 nonRelativeExternalModuleResolutions.clear(); 240 closeTypeRootsWatch(); 241 resolvedModuleNames.clear(); 242 resolvedTypeReferenceDirectives.clear(); 243 resolvedFileToResolution.clear(); 244 resolutionsWithFailedLookups.length = 0; 245 failedLookupChecks.length = 0; 246 startsWithPathChecks.length = 0; 247 isInDirectoryChecks.length = 0; 248 // perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update 249 // (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution) 250 clearPerDirectoryResolutions(); 251 hasChangedAutomaticTypeDirectiveNames = false; 252 } 253 254 function startRecordingFilesWithChangedResolutions() { 255 filesWithChangedSetOfUnresolvedImports = []; 256 } 257 258 function finishRecordingFilesWithChangedResolutions() { 259 const collected = filesWithChangedSetOfUnresolvedImports; 260 filesWithChangedSetOfUnresolvedImports = undefined; 261 return collected; 262 } 263 264 function isFileWithInvalidatedNonRelativeUnresolvedImports(path: Path): boolean { 265 if (!filesWithInvalidatedNonRelativeUnresolvedImports) { 266 return false; 267 } 268 269 // Invalidated if file has unresolved imports 270 const value = filesWithInvalidatedNonRelativeUnresolvedImports.get(path); 271 return !!value && !!value.length; 272 } 273 274 function createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): HasInvalidatedResolution { 275 // Ensure pending resolutions are applied 276 invalidateResolutionsOfFailedLookupLocations(); 277 if (forceAllFilesAsInvalidated) { 278 // Any file asked would have invalidated resolution 279 filesWithInvalidatedResolutions = undefined; 280 return returnTrue; 281 } 282 const collected = filesWithInvalidatedResolutions; 283 filesWithInvalidatedResolutions = undefined; 284 return path => (!!collected && collected.has(path)) || 285 isFileWithInvalidatedNonRelativeUnresolvedImports(path); 286 } 287 288 function clearPerDirectoryResolutions() { 289 perDirectoryResolvedModuleNames.clear(); 290 nonRelativeModuleNameCache.clear(); 291 perDirectoryResolvedTypeReferenceDirectives.clear(); 292 nonRelativeExternalModuleResolutions.forEach(watchFailedLookupLocationOfNonRelativeModuleResolutions); 293 nonRelativeExternalModuleResolutions.clear(); 294 } 295 296 function finishCachingPerDirectoryResolution() { 297 filesWithInvalidatedNonRelativeUnresolvedImports = undefined; 298 clearPerDirectoryResolutions(); 299 directoryWatchesOfFailedLookups.forEach((watcher, path) => { 300 if (watcher.refCount === 0) { 301 directoryWatchesOfFailedLookups.delete(path); 302 watcher.watcher.close(); 303 } 304 }); 305 hasChangedAutomaticTypeDirectiveNames = false; 306 } 307 308 function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference): CachedResolvedModuleWithFailedLookupLocations { 309 const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host, moduleResolutionCache, redirectedReference); 310 // return result immediately only if global cache support is not enabled or if it is .ts, .tsx or .d.ts 311 if (!resolutionHost.getGlobalCache) { 312 return primaryResult; 313 } 314 315 // otherwise try to load typings from @types 316 const globalCache = resolutionHost.getGlobalCache(); 317 if (globalCache !== undefined && !isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTS(primaryResult.resolvedModule.extension))) { 318 // create different collection of failed lookup locations for second pass 319 // if it will fail and we've already found something during the first pass - we don't want to pollute its results 320 const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache( 321 Debug.checkDefined(resolutionHost.globalCacheResolutionModuleName)(moduleName), 322 resolutionHost.projectName, 323 compilerOptions, 324 host, 325 globalCache); 326 if (resolvedModule) { 327 // Modify existing resolution so its saved in the directory cache as well 328 (primaryResult.resolvedModule as any) = resolvedModule; 329 primaryResult.failedLookupLocations.push(...failedLookupLocations); 330 return primaryResult; 331 } 332 } 333 334 // Default return the result from the first pass 335 return primaryResult; 336 } 337 338 interface ResolveNamesWithLocalCacheInput<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName> { 339 names: readonly string[]; 340 containingFile: string; 341 redirectedReference: ResolvedProjectReference | undefined; 342 cache: ESMap<Path, ESMap<string, T>>; 343 perDirectoryCacheWithRedirects: CacheWithRedirects<ESMap<string, T>>; 344 loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference) => T; 345 getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>; 346 shouldRetryResolution: (t: T) => boolean; 347 reusedNames?: readonly string[]; 348 logChanges?: boolean; 349 } 350 function resolveNamesWithLocalCache<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>({ 351 names, containingFile, redirectedReference, 352 cache, perDirectoryCacheWithRedirects, 353 loader, getResolutionWithResolvedFileName, 354 shouldRetryResolution, reusedNames, logChanges 355 }: ResolveNamesWithLocalCacheInput<T, R>): (R | undefined)[] { 356 const path = resolutionHost.toPath(containingFile); 357 const resolutionsInFile = cache.get(path) || cache.set(path, new Map()).get(path)!; 358 const dirPath = getDirectoryPath(path); 359 const perDirectoryCache = perDirectoryCacheWithRedirects.getOrCreateMapOfCacheRedirects(redirectedReference); 360 let perDirectoryResolution = perDirectoryCache.get(dirPath); 361 if (!perDirectoryResolution) { 362 perDirectoryResolution = new Map(); 363 perDirectoryCache.set(dirPath, perDirectoryResolution); 364 } 365 const resolvedModules: (R | undefined)[] = []; 366 const compilerOptions = resolutionHost.getCompilationSettings(); 367 const hasInvalidatedNonRelativeUnresolvedImport = logChanges && isFileWithInvalidatedNonRelativeUnresolvedImports(path); 368 369 // All the resolutions in this file are invalidated if this file wasn't resolved using same redirect 370 const program = resolutionHost.getCurrentProgram(); 371 const oldRedirect = program && program.getResolvedProjectReferenceToRedirect(containingFile); 372 const unmatchedRedirects = oldRedirect ? 373 !redirectedReference || redirectedReference.sourceFile.path !== oldRedirect.sourceFile.path : 374 !!redirectedReference; 375 376 const seenNamesInFile = new Map<string, true>(); 377 for (const name of names) { 378 let resolution = resolutionsInFile.get(name); 379 // Resolution is valid if it is present and not invalidated 380 if (!seenNamesInFile.has(name) && 381 unmatchedRedirects || !resolution || resolution.isInvalidated || 382 // If the name is unresolved import that was invalidated, recalculate 383 (hasInvalidatedNonRelativeUnresolvedImport && !isExternalModuleNameRelative(name) && shouldRetryResolution(resolution))) { 384 const existingResolution = resolution; 385 const resolutionInDirectory = perDirectoryResolution.get(name); 386 if (resolutionInDirectory) { 387 resolution = resolutionInDirectory; 388 } 389 else { 390 resolution = loader(name, containingFile, compilerOptions, resolutionHost.getCompilerHost?.() || resolutionHost, redirectedReference); 391 perDirectoryResolution.set(name, resolution); 392 } 393 resolutionsInFile.set(name, resolution); 394 watchFailedLookupLocationsOfExternalModuleResolutions(name, resolution, path, getResolutionWithResolvedFileName); 395 if (existingResolution) { 396 stopWatchFailedLookupLocationOfResolution(existingResolution, path, getResolutionWithResolvedFileName); 397 } 398 399 if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { 400 filesWithChangedSetOfUnresolvedImports.push(path); 401 // reset log changes to avoid recording the same file multiple times 402 logChanges = false; 403 } 404 } 405 Debug.assert(resolution !== undefined && !resolution.isInvalidated); 406 seenNamesInFile.set(name, true); 407 resolvedModules.push(getResolutionWithResolvedFileName(resolution)); 408 } 409 410 // Stop watching and remove the unused name 411 resolutionsInFile.forEach((resolution, name) => { 412 if (!seenNamesInFile.has(name) && !contains(reusedNames, name)) { 413 stopWatchFailedLookupLocationOfResolution(resolution, path, getResolutionWithResolvedFileName); 414 resolutionsInFile.delete(name); 415 } 416 }); 417 418 return resolvedModules; 419 420 function resolutionIsEqualTo(oldResolution: T | undefined, newResolution: T | undefined): boolean { 421 if (oldResolution === newResolution) { 422 return true; 423 } 424 if (!oldResolution || !newResolution) { 425 return false; 426 } 427 const oldResult = getResolutionWithResolvedFileName(oldResolution); 428 const newResult = getResolutionWithResolvedFileName(newResolution); 429 if (oldResult === newResult) { 430 return true; 431 } 432 if (!oldResult || !newResult) { 433 return false; 434 } 435 return oldResult.resolvedFileName === newResult.resolvedFileName; 436 } 437 } 438 439 function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference): (ResolvedTypeReferenceDirective | undefined)[] { 440 return resolveNamesWithLocalCache<CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations, ResolvedTypeReferenceDirective>({ 441 names: typeDirectiveNames, 442 containingFile, 443 redirectedReference, 444 cache: resolvedTypeReferenceDirectives, 445 perDirectoryCacheWithRedirects: perDirectoryResolvedTypeReferenceDirectives, 446 loader: resolveTypeReferenceDirective, 447 getResolutionWithResolvedFileName: getResolvedTypeReferenceDirective, 448 shouldRetryResolution: resolution => resolution.resolvedTypeReferenceDirective === undefined, 449 }); 450 } 451 452 function resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference?: ResolvedProjectReference): (ResolvedModuleFull | undefined)[] { 453 return resolveNamesWithLocalCache<CachedResolvedModuleWithFailedLookupLocations, ResolvedModuleFull>({ 454 names: moduleNames, 455 containingFile, 456 redirectedReference, 457 cache: resolvedModuleNames, 458 perDirectoryCacheWithRedirects: perDirectoryResolvedModuleNames, 459 loader: resolveModuleName, 460 getResolutionWithResolvedFileName: getResolvedModule, 461 shouldRetryResolution: resolution => !resolution.resolvedModule || !resolutionExtensionIsTSOrJson(resolution.resolvedModule.extension), 462 reusedNames, 463 logChanges: logChangesWhenResolvingModule, 464 }); 465 } 466 467 function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): CachedResolvedModuleWithFailedLookupLocations | undefined { 468 const cache = resolvedModuleNames.get(resolutionHost.toPath(containingFile)); 469 return cache && cache.get(moduleName); 470 } 471 472 function isNodeModulesAtTypesDirectory(dirPath: Path) { 473 return endsWith(dirPath, "/node_modules/@types"); 474 } 475 476 function getDirectoryToWatchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path): DirectoryOfFailedLookupWatch | undefined { 477 if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { 478 // Ensure failed look up is normalized path 479 failedLookupLocation = isRootedDiskPath(failedLookupLocation) ? normalizePath(failedLookupLocation) : getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory()); 480 const failedLookupPathSplit = failedLookupLocationPath.split(directorySeparator); 481 const failedLookupSplit = failedLookupLocation.split(directorySeparator); 482 Debug.assert(failedLookupSplit.length === failedLookupPathSplit.length, `FailedLookup: ${failedLookupLocation} failedLookupLocationPath: ${failedLookupLocationPath}`); 483 if (failedLookupPathSplit.length > rootSplitLength + 1) { 484 // Instead of watching root, watch directory in root to avoid watching excluded directories not needed for module resolution 485 return { 486 dir: failedLookupSplit.slice(0, rootSplitLength + 1).join(directorySeparator), 487 dirPath: failedLookupPathSplit.slice(0, rootSplitLength + 1).join(directorySeparator) as Path 488 }; 489 } 490 else { 491 // Always watch root directory non recursively 492 return { 493 dir: rootDir!, 494 dirPath: rootPath, 495 nonRecursive: false 496 }; 497 } 498 } 499 500 return getDirectoryToWatchFromFailedLookupLocationDirectory( 501 getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())), 502 getDirectoryPath(failedLookupLocationPath) 503 ); 504 } 505 506 function getDirectoryToWatchFromFailedLookupLocationDirectory(dir: string, dirPath: Path): DirectoryOfFailedLookupWatch | undefined { 507 // If directory path contains node module, get the most parent node_modules directory for watching 508 while (pathContainsNodeModules(dirPath)) { 509 dir = getDirectoryPath(dir); 510 dirPath = getDirectoryPath(dirPath); 511 } 512 513 // If the directory is node_modules use it to watch, always watch it recursively 514 if (isNodeModulesDirectory(dirPath)) { 515 return canWatchDirectory(getDirectoryPath(dirPath)) ? { dir, dirPath } : undefined; 516 } 517 518 let nonRecursive = true; 519 // Use some ancestor of the root directory 520 let subDirectoryPath: Path | undefined, subDirectory: string | undefined; 521 if (rootPath !== undefined) { 522 while (!isInDirectoryPath(dirPath, rootPath)) { 523 const parentPath = getDirectoryPath(dirPath); 524 if (parentPath === dirPath) { 525 break; 526 } 527 nonRecursive = false; 528 subDirectoryPath = dirPath; 529 subDirectory = dir; 530 dirPath = parentPath; 531 dir = getDirectoryPath(dir); 532 } 533 } 534 535 return canWatchDirectory(dirPath) ? { dir: subDirectory || dir, dirPath: subDirectoryPath || dirPath, nonRecursive } : undefined; 536 } 537 538 function isPathWithDefaultFailedLookupExtension(path: Path) { 539 return fileExtensionIsOneOf(path, failedLookupDefaultExtensions); 540 } 541 542 function watchFailedLookupLocationsOfExternalModuleResolutions<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>( 543 name: string, 544 resolution: T, 545 filePath: Path, 546 getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>, 547 ) { 548 if (resolution.refCount) { 549 resolution.refCount++; 550 Debug.assertDefined(resolution.files); 551 } 552 else { 553 resolution.refCount = 1; 554 Debug.assert(length(resolution.files) === 0); // This resolution shouldnt be referenced by any file yet 555 if (isExternalModuleNameRelative(name)) { 556 watchFailedLookupLocationOfResolution(resolution); 557 } 558 else { 559 nonRelativeExternalModuleResolutions.add(name, resolution); 560 } 561 const resolved = getResolutionWithResolvedFileName(resolution); 562 if (resolved && resolved.resolvedFileName) { 563 resolvedFileToResolution.add(resolutionHost.toPath(resolved.resolvedFileName), resolution); 564 } 565 } 566 (resolution.files || (resolution.files = [])).push(filePath); 567 } 568 569 function watchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) { 570 Debug.assert(!!resolution.refCount); 571 572 const { failedLookupLocations } = resolution; 573 if (!failedLookupLocations.length) return; 574 resolutionsWithFailedLookups.push(resolution); 575 576 let setAtRoot = false; 577 for (const failedLookupLocation of failedLookupLocations) { 578 const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); 579 const toWatch = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); 580 if (toWatch) { 581 const { dir, dirPath, nonRecursive } = toWatch; 582 // If the failed lookup location path is not one of the supported extensions, 583 // store it in the custom path 584 if (!isPathWithDefaultFailedLookupExtension(failedLookupLocationPath)) { 585 const refCount = customFailedLookupPaths.get(failedLookupLocationPath) || 0; 586 customFailedLookupPaths.set(failedLookupLocationPath, refCount + 1); 587 } 588 if (dirPath === rootPath) { 589 Debug.assert(!nonRecursive); 590 setAtRoot = true; 591 } 592 else { 593 setDirectoryWatcher(dir, dirPath, nonRecursive); 594 } 595 } 596 } 597 598 if (setAtRoot) { 599 // This is always non recursive 600 setDirectoryWatcher(rootDir!, rootPath, /*nonRecursive*/ true); // TODO: GH#18217 601 } 602 } 603 604 function watchFailedLookupLocationOfNonRelativeModuleResolutions(resolutions: ResolutionWithFailedLookupLocations[], name: string) { 605 const program = resolutionHost.getCurrentProgram(); 606 if (!program || !program.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(name)) { 607 resolutions.forEach(watchFailedLookupLocationOfResolution); 608 } 609 } 610 611 function setDirectoryWatcher(dir: string, dirPath: Path, nonRecursive?: boolean) { 612 const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); 613 if (dirWatcher) { 614 Debug.assert(!!nonRecursive === !!dirWatcher.nonRecursive); 615 dirWatcher.refCount++; 616 } 617 else { 618 directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath, nonRecursive), refCount: 1, nonRecursive }); 619 } 620 } 621 622 function stopWatchFailedLookupLocationOfResolution<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>( 623 resolution: T, 624 filePath: Path, 625 getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>, 626 ) { 627 unorderedRemoveItem(Debug.assertDefined(resolution.files), filePath); 628 resolution.refCount!--; 629 if (resolution.refCount) { 630 return; 631 } 632 const resolved = getResolutionWithResolvedFileName(resolution); 633 if (resolved && resolved.resolvedFileName) { 634 resolvedFileToResolution.remove(resolutionHost.toPath(resolved.resolvedFileName), resolution); 635 } 636 637 if (!unorderedRemoveItem(resolutionsWithFailedLookups, resolution)) { 638 // If not watching failed lookups, it wont be there in resolutionsWithFailedLookups 639 return; 640 } 641 642 const { failedLookupLocations } = resolution; 643 let removeAtRoot = false; 644 for (const failedLookupLocation of failedLookupLocations) { 645 const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); 646 const toWatch = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); 647 if (toWatch) { 648 const { dirPath } = toWatch; 649 const refCount = customFailedLookupPaths.get(failedLookupLocationPath); 650 if (refCount) { 651 if (refCount === 1) { 652 customFailedLookupPaths.delete(failedLookupLocationPath); 653 } 654 else { 655 Debug.assert(refCount > 1); 656 customFailedLookupPaths.set(failedLookupLocationPath, refCount - 1); 657 } 658 } 659 660 if (dirPath === rootPath) { 661 removeAtRoot = true; 662 } 663 else { 664 removeDirectoryWatcher(dirPath); 665 } 666 } 667 } 668 if (removeAtRoot) { 669 removeDirectoryWatcher(rootPath); 670 } 671 } 672 673 function removeDirectoryWatcher(dirPath: string) { 674 const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath)!; 675 // Do not close the watcher yet since it might be needed by other failed lookup locations. 676 dirWatcher.refCount--; 677 } 678 679 function createDirectoryWatcher(directory: string, dirPath: Path, nonRecursive: boolean | undefined) { 680 return resolutionHost.watchDirectoryOfFailedLookupLocation(directory, fileOrDirectory => { 681 const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); 682 if (cachedDirectoryStructureHost) { 683 // Since the file existence changed, update the sourceFiles cache 684 cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); 685 } 686 687 scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath); 688 }, nonRecursive ? WatchDirectoryFlags.None : WatchDirectoryFlags.Recursive); 689 } 690 691 function removeResolutionsOfFileFromCache<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>( 692 cache: ESMap<string, ESMap<string, T>>, 693 filePath: Path, 694 getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>, 695 ) { 696 // Deleted file, stop watching failed lookups for all the resolutions in the file 697 const resolutions = cache.get(filePath); 698 if (resolutions) { 699 resolutions.forEach(resolution => stopWatchFailedLookupLocationOfResolution(resolution, filePath, getResolutionWithResolvedFileName)); 700 cache.delete(filePath); 701 } 702 } 703 704 function removeResolutionsFromProjectReferenceRedirects(filePath: Path) { 705 if (!fileExtensionIs(filePath, Extension.Json)) { return; } 706 707 const program = resolutionHost.getCurrentProgram(); 708 if (!program) { return; } 709 710 // If this file is input file for the referenced project, get it 711 const resolvedProjectReference = program.getResolvedProjectReferenceByPath(filePath); 712 if (!resolvedProjectReference) { return; } 713 714 // filePath is for the projectReference and the containing file is from this project reference, invalidate the resolution 715 resolvedProjectReference.commandLine.fileNames.forEach(f => removeResolutionsOfFile(resolutionHost.toPath(f))); 716 } 717 718 function removeResolutionsOfFile(filePath: Path) { 719 removeResolutionsOfFileFromCache(resolvedModuleNames, filePath, getResolvedModule); 720 removeResolutionsOfFileFromCache(resolvedTypeReferenceDirectives, filePath, getResolvedTypeReferenceDirective); 721 } 722 723 function invalidateResolutions(resolutions: ResolutionWithFailedLookupLocations[] | undefined, canInvalidate: (resolution: ResolutionWithFailedLookupLocations) => boolean) { 724 if (!resolutions) return false; 725 let invalidated = false; 726 for (const resolution of resolutions) { 727 if (resolution.isInvalidated || !canInvalidate(resolution)) continue; 728 resolution.isInvalidated = invalidated = true; 729 for (const containingFilePath of Debug.assertDefined(resolution.files)) { 730 (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = new Set())).add(containingFilePath); 731 // When its a file with inferred types resolution, invalidate type reference directive resolution 732 hasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames || endsWith(containingFilePath, inferredTypesContainingFile); 733 } 734 } 735 return invalidated; 736 } 737 738 function invalidateResolutionOfFile(filePath: Path) { 739 removeResolutionsOfFile(filePath); 740 // Resolution is invalidated if the resulting file name is same as the deleted file path 741 const prevHasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames; 742 if (invalidateResolutions(resolvedFileToResolution.get(filePath), returnTrue) && 743 hasChangedAutomaticTypeDirectiveNames && 744 !prevHasChangedAutomaticTypeDirectiveNames) { 745 resolutionHost.onChangedAutomaticTypeDirectiveNames(); 746 } 747 } 748 749 function setFilesWithInvalidatedNonRelativeUnresolvedImports(filesMap: ReadonlyESMap<Path, readonly string[]>) { 750 Debug.assert(filesWithInvalidatedNonRelativeUnresolvedImports === filesMap || filesWithInvalidatedNonRelativeUnresolvedImports === undefined); 751 filesWithInvalidatedNonRelativeUnresolvedImports = filesMap; 752 } 753 754 function scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath: Path, isCreatingWatchedDirectory: boolean) { 755 if (isCreatingWatchedDirectory) { 756 // Watching directory is created 757 // Invalidate any resolution has failed lookup in this directory 758 isInDirectoryChecks.push(fileOrDirectoryPath); 759 } 760 else { 761 // If something to do with folder/file starting with "." in node_modules folder, skip it 762 const updatedPath = removeIgnoredPath(fileOrDirectoryPath); 763 if (!updatedPath) return false; 764 fileOrDirectoryPath = updatedPath; 765 766 // prevent saving an open file from over-eagerly triggering invalidation 767 if (resolutionHost.fileIsOpen(fileOrDirectoryPath)) { 768 return false; 769 } 770 771 // Some file or directory in the watching directory is created 772 // Return early if it does not have any of the watching extension or not the custom failed lookup path 773 const dirOfFileOrDirectory = getDirectoryPath(fileOrDirectoryPath); 774 if (isNodeModulesAtTypesDirectory(fileOrDirectoryPath) || isNodeModulesDirectory(fileOrDirectoryPath) || 775 isNodeModulesAtTypesDirectory(dirOfFileOrDirectory) || isNodeModulesDirectory(dirOfFileOrDirectory)) { 776 // Invalidate any resolution from this directory 777 failedLookupChecks.push(fileOrDirectoryPath); 778 startsWithPathChecks.push(fileOrDirectoryPath); 779 } 780 else { 781 if (!isPathWithDefaultFailedLookupExtension(fileOrDirectoryPath) && !customFailedLookupPaths.has(fileOrDirectoryPath)) { 782 return false; 783 } 784 // Ignore emits from the program 785 if (isEmittedFileOfProgram(resolutionHost.getCurrentProgram(), fileOrDirectoryPath)) { 786 return false; 787 } 788 // Resolution need to be invalidated if failed lookup location is same as the file or directory getting created 789 failedLookupChecks.push(fileOrDirectoryPath); 790 } 791 } 792 resolutionHost.scheduleInvalidateResolutionsOfFailedLookupLocations(); 793 } 794 795 function invalidateResolutionsOfFailedLookupLocations() { 796 if (!failedLookupChecks.length && !startsWithPathChecks.length && !isInDirectoryChecks.length) { 797 return false; 798 } 799 800 const invalidated = invalidateResolutions(resolutionsWithFailedLookups, canInvalidateFailedLookupResolution); 801 failedLookupChecks.length = 0; 802 startsWithPathChecks.length = 0; 803 isInDirectoryChecks.length = 0; 804 return invalidated; 805 } 806 807 function canInvalidateFailedLookupResolution(resolution: ResolutionWithFailedLookupLocations) { 808 return resolution.failedLookupLocations.some(location => { 809 const locationPath = resolutionHost.toPath(location); 810 return contains(failedLookupChecks, locationPath) || 811 startsWithPathChecks.some(fileOrDirectoryPath => startsWith(locationPath, fileOrDirectoryPath)) || 812 isInDirectoryChecks.some(fileOrDirectoryPath => isInDirectoryPath(fileOrDirectoryPath, locationPath)); 813 }); 814 } 815 816 function closeTypeRootsWatch() { 817 clearMap(typeRootsWatches, closeFileWatcher); 818 } 819 820 function getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot: string, typeRootPath: Path): Path | undefined { 821 if (isInDirectoryPath(rootPath, typeRootPath)) { 822 return rootPath; 823 } 824 const toWatch = getDirectoryToWatchFromFailedLookupLocationDirectory(typeRoot, typeRootPath); 825 return toWatch && directoryWatchesOfFailedLookups.has(toWatch.dirPath) ? toWatch.dirPath : undefined; 826 } 827 828 function createTypeRootsWatch(typeRootPath: Path, typeRoot: string): FileWatcher { 829 // Create new watch and recursive info 830 return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrDirectory => { 831 const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); 832 if (cachedDirectoryStructureHost) { 833 // Since the file existence changed, update the sourceFiles cache 834 cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); 835 } 836 837 // For now just recompile 838 // We could potentially store more data here about whether it was/would be really be used or not 839 // and with that determine to trigger compilation but for now this is enough 840 hasChangedAutomaticTypeDirectiveNames = true; 841 resolutionHost.onChangedAutomaticTypeDirectiveNames(); 842 843 // Since directory watchers invoked are flaky, the failed lookup location events might not be triggered 844 // So handle to failed lookup locations here as well to ensure we are invalidating resolutions 845 const dirPath = getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot, typeRootPath); 846 if (dirPath) { 847 scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath); 848 } 849 }, WatchDirectoryFlags.Recursive); 850 } 851 852 /** 853 * Watches the types that would get added as part of getAutomaticTypeDirectiveNames 854 * To be called when compiler options change 855 */ 856 function updateTypeRootsWatch() { 857 const options = resolutionHost.getCompilationSettings(); 858 if (options.types) { 859 // No need to do any watch since resolution cache is going to handle the failed lookups 860 // for the types added by this 861 closeTypeRootsWatch(); 862 return; 863 } 864 865 // we need to assume the directories exist to ensure that we can get all the type root directories that get included 866 // But filter directories that are at root level to say directory doesnt exist, so that we arent watching them 867 const typeRoots = getEffectiveTypeRoots(options, { directoryExists: directoryExistsForTypeRootWatch, getCurrentDirectory }); 868 if (typeRoots) { 869 mutateMap( 870 typeRootsWatches, 871 arrayToMap(typeRoots, tr => resolutionHost.toPath(tr)), 872 { 873 createNewValue: createTypeRootsWatch, 874 onDeleteValue: closeFileWatcher 875 } 876 ); 877 } 878 else { 879 closeTypeRootsWatch(); 880 } 881 } 882 883 /** 884 * Use this function to return if directory exists to get type roots to watch 885 * If we return directory exists then only the paths will be added to type roots 886 * Hence return true for all directories except root directories which are filtered from watching 887 */ 888 function directoryExistsForTypeRootWatch(nodeTypesDirectory: string) { 889 const dir = getDirectoryPath(getDirectoryPath(nodeTypesDirectory)); 890 const dirPath = resolutionHost.toPath(dir); 891 return dirPath === rootPath || canWatchDirectory(dirPath); 892 } 893 } 894} 895