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 or oh_modules changed. 85 if (endsWith(path, "/node_modules/.staging") || endsWith(path, "/oh_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 isOHModulesAtTypesDirectory(dirPath: Path) { 477 return endsWith(dirPath, "/oh_modules/@types"); 478 } 479 480 function getDirectoryToWatchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path): DirectoryOfFailedLookupWatch | undefined { 481 if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { 482 // Ensure failed look up is normalized path 483 failedLookupLocation = isRootedDiskPath(failedLookupLocation) ? normalizePath(failedLookupLocation) : getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory()); 484 const failedLookupPathSplit = failedLookupLocationPath.split(directorySeparator); 485 const failedLookupSplit = failedLookupLocation.split(directorySeparator); 486 Debug.assert(failedLookupSplit.length === failedLookupPathSplit.length, `FailedLookup: ${failedLookupLocation} failedLookupLocationPath: ${failedLookupLocationPath}`); 487 if (failedLookupPathSplit.length > rootSplitLength + 1) { 488 // Instead of watching root, watch directory in root to avoid watching excluded directories not needed for module resolution 489 return { 490 dir: failedLookupSplit.slice(0, rootSplitLength + 1).join(directorySeparator), 491 dirPath: failedLookupPathSplit.slice(0, rootSplitLength + 1).join(directorySeparator) as Path 492 }; 493 } 494 else { 495 // Always watch root directory non recursively 496 return { 497 dir: rootDir!, 498 dirPath: rootPath, 499 nonRecursive: false 500 }; 501 } 502 } 503 504 return getDirectoryToWatchFromFailedLookupLocationDirectory( 505 getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())), 506 getDirectoryPath(failedLookupLocationPath) 507 ); 508 } 509 510 function getDirectoryToWatchFromFailedLookupLocationDirectory(dir: string, dirPath: Path): DirectoryOfFailedLookupWatch | undefined { 511 // If directory path contains node module, get the most parent node_modules or oh_modules directory for watching 512 const isOHModules: boolean = isOhpm(resolutionHost.getCompilationSettings().packageManagerType); 513 while (isOHModules ? pathContainsOHModules(dirPath) : pathContainsNodeModules(dirPath)) { 514 dir = getDirectoryPath(dir); 515 dirPath = getDirectoryPath(dirPath); 516 } 517 518 // If the directory is node_modules or oh_modules use it to watch, always watch it recursively 519 if (isOHModules ? isOHModulesDirectory(dirPath) : isNodeModulesDirectory(dirPath)) { 520 return canWatchDirectory(getDirectoryPath(dirPath)) ? { dir, dirPath } : undefined; 521 } 522 523 let nonRecursive = true; 524 // Use some ancestor of the root directory 525 let subDirectoryPath: Path | undefined, subDirectory: string | undefined; 526 if (rootPath !== undefined) { 527 while (!isInDirectoryPath(dirPath, rootPath)) { 528 const parentPath = getDirectoryPath(dirPath); 529 if (parentPath === dirPath) { 530 break; 531 } 532 nonRecursive = false; 533 subDirectoryPath = dirPath; 534 subDirectory = dir; 535 dirPath = parentPath; 536 dir = getDirectoryPath(dir); 537 } 538 } 539 540 return canWatchDirectory(dirPath) ? { dir: subDirectory || dir, dirPath: subDirectoryPath || dirPath, nonRecursive } : undefined; 541 } 542 543 function isPathWithDefaultFailedLookupExtension(path: Path) { 544 return fileExtensionIsOneOf(path, failedLookupDefaultExtensions); 545 } 546 547 function watchFailedLookupLocationsOfExternalModuleResolutions<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>( 548 name: string, 549 resolution: T, 550 filePath: Path, 551 getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>, 552 ) { 553 if (resolution.refCount) { 554 resolution.refCount++; 555 Debug.assertDefined(resolution.files); 556 } 557 else { 558 resolution.refCount = 1; 559 Debug.assert(length(resolution.files) === 0); // This resolution shouldnt be referenced by any file yet 560 if (isExternalModuleNameRelative(name)) { 561 watchFailedLookupLocationOfResolution(resolution); 562 } 563 else { 564 nonRelativeExternalModuleResolutions.add(name, resolution); 565 } 566 const resolved = getResolutionWithResolvedFileName(resolution); 567 if (resolved && resolved.resolvedFileName) { 568 resolvedFileToResolution.add(resolutionHost.toPath(resolved.resolvedFileName), resolution); 569 } 570 } 571 (resolution.files || (resolution.files = [])).push(filePath); 572 } 573 574 function watchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) { 575 Debug.assert(!!resolution.refCount); 576 577 const { failedLookupLocations } = resolution; 578 if (!failedLookupLocations.length) return; 579 resolutionsWithFailedLookups.push(resolution); 580 581 let setAtRoot = false; 582 for (const failedLookupLocation of failedLookupLocations) { 583 const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); 584 const toWatch = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); 585 if (toWatch) { 586 const { dir, dirPath, nonRecursive } = toWatch; 587 // If the failed lookup location path is not one of the supported extensions, 588 // store it in the custom path 589 if (!isPathWithDefaultFailedLookupExtension(failedLookupLocationPath)) { 590 const refCount = customFailedLookupPaths.get(failedLookupLocationPath) || 0; 591 customFailedLookupPaths.set(failedLookupLocationPath, refCount + 1); 592 } 593 if (dirPath === rootPath) { 594 Debug.assert(!nonRecursive); 595 setAtRoot = true; 596 } 597 else { 598 setDirectoryWatcher(dir, dirPath, nonRecursive); 599 } 600 } 601 } 602 603 if (setAtRoot) { 604 // This is always non recursive 605 setDirectoryWatcher(rootDir!, rootPath, /*nonRecursive*/ true); // TODO: GH#18217 606 } 607 } 608 609 function watchFailedLookupLocationOfNonRelativeModuleResolutions(resolutions: ResolutionWithFailedLookupLocations[], name: string) { 610 const program = resolutionHost.getCurrentProgram(); 611 if (!program || !program.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(name)) { 612 resolutions.forEach(watchFailedLookupLocationOfResolution); 613 } 614 } 615 616 function setDirectoryWatcher(dir: string, dirPath: Path, nonRecursive?: boolean) { 617 const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); 618 if (dirWatcher) { 619 Debug.assert(!!nonRecursive === !!dirWatcher.nonRecursive); 620 dirWatcher.refCount++; 621 } 622 else { 623 directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath, nonRecursive), refCount: 1, nonRecursive }); 624 } 625 } 626 627 function stopWatchFailedLookupLocationOfResolution<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>( 628 resolution: T, 629 filePath: Path, 630 getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>, 631 ) { 632 unorderedRemoveItem(Debug.assertDefined(resolution.files), filePath); 633 resolution.refCount!--; 634 if (resolution.refCount) { 635 return; 636 } 637 const resolved = getResolutionWithResolvedFileName(resolution); 638 if (resolved && resolved.resolvedFileName) { 639 resolvedFileToResolution.remove(resolutionHost.toPath(resolved.resolvedFileName), resolution); 640 } 641 642 if (!unorderedRemoveItem(resolutionsWithFailedLookups, resolution)) { 643 // If not watching failed lookups, it wont be there in resolutionsWithFailedLookups 644 return; 645 } 646 647 const { failedLookupLocations } = resolution; 648 let removeAtRoot = false; 649 for (const failedLookupLocation of failedLookupLocations) { 650 const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); 651 const toWatch = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); 652 if (toWatch) { 653 const { dirPath } = toWatch; 654 const refCount = customFailedLookupPaths.get(failedLookupLocationPath); 655 if (refCount) { 656 if (refCount === 1) { 657 customFailedLookupPaths.delete(failedLookupLocationPath); 658 } 659 else { 660 Debug.assert(refCount > 1); 661 customFailedLookupPaths.set(failedLookupLocationPath, refCount - 1); 662 } 663 } 664 665 if (dirPath === rootPath) { 666 removeAtRoot = true; 667 } 668 else { 669 removeDirectoryWatcher(dirPath); 670 } 671 } 672 } 673 if (removeAtRoot) { 674 removeDirectoryWatcher(rootPath); 675 } 676 } 677 678 function removeDirectoryWatcher(dirPath: string) { 679 const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath)!; 680 // Do not close the watcher yet since it might be needed by other failed lookup locations. 681 dirWatcher.refCount--; 682 } 683 684 function createDirectoryWatcher(directory: string, dirPath: Path, nonRecursive: boolean | undefined) { 685 return resolutionHost.watchDirectoryOfFailedLookupLocation(directory, fileOrDirectory => { 686 const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); 687 if (cachedDirectoryStructureHost) { 688 // Since the file existence changed, update the sourceFiles cache 689 cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); 690 } 691 692 scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath); 693 }, nonRecursive ? WatchDirectoryFlags.None : WatchDirectoryFlags.Recursive); 694 } 695 696 function removeResolutionsOfFileFromCache<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>( 697 cache: ESMap<string, ESMap<string, T>>, 698 filePath: Path, 699 getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>, 700 ) { 701 // Deleted file, stop watching failed lookups for all the resolutions in the file 702 const resolutions = cache.get(filePath); 703 if (resolutions) { 704 resolutions.forEach(resolution => stopWatchFailedLookupLocationOfResolution(resolution, filePath, getResolutionWithResolvedFileName)); 705 cache.delete(filePath); 706 } 707 } 708 709 function removeResolutionsFromProjectReferenceRedirects(filePath: Path) { 710 if (!fileExtensionIs(filePath, Extension.Json)) { return; } 711 712 const program = resolutionHost.getCurrentProgram(); 713 if (!program) { return; } 714 715 // If this file is input file for the referenced project, get it 716 const resolvedProjectReference = program.getResolvedProjectReferenceByPath(filePath); 717 if (!resolvedProjectReference) { return; } 718 719 // filePath is for the projectReference and the containing file is from this project reference, invalidate the resolution 720 resolvedProjectReference.commandLine.fileNames.forEach(f => removeResolutionsOfFile(resolutionHost.toPath(f))); 721 } 722 723 function removeResolutionsOfFile(filePath: Path) { 724 removeResolutionsOfFileFromCache(resolvedModuleNames, filePath, getResolvedModule); 725 removeResolutionsOfFileFromCache(resolvedTypeReferenceDirectives, filePath, getResolvedTypeReferenceDirective); 726 } 727 728 function invalidateResolutions(resolutions: ResolutionWithFailedLookupLocations[] | undefined, canInvalidate: (resolution: ResolutionWithFailedLookupLocations) => boolean) { 729 if (!resolutions) return false; 730 let invalidated = false; 731 for (const resolution of resolutions) { 732 if (resolution.isInvalidated || !canInvalidate(resolution)) continue; 733 resolution.isInvalidated = invalidated = true; 734 for (const containingFilePath of Debug.assertDefined(resolution.files)) { 735 (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = new Set())).add(containingFilePath); 736 // When its a file with inferred types resolution, invalidate type reference directive resolution 737 hasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames || endsWith(containingFilePath, inferredTypesContainingFile); 738 } 739 } 740 return invalidated; 741 } 742 743 function invalidateResolutionOfFile(filePath: Path) { 744 removeResolutionsOfFile(filePath); 745 // Resolution is invalidated if the resulting file name is same as the deleted file path 746 const prevHasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames; 747 if (invalidateResolutions(resolvedFileToResolution.get(filePath), returnTrue) && 748 hasChangedAutomaticTypeDirectiveNames && 749 !prevHasChangedAutomaticTypeDirectiveNames) { 750 resolutionHost.onChangedAutomaticTypeDirectiveNames(); 751 } 752 } 753 754 function setFilesWithInvalidatedNonRelativeUnresolvedImports(filesMap: ReadonlyESMap<Path, readonly string[]>) { 755 Debug.assert(filesWithInvalidatedNonRelativeUnresolvedImports === filesMap || filesWithInvalidatedNonRelativeUnresolvedImports === undefined); 756 filesWithInvalidatedNonRelativeUnresolvedImports = filesMap; 757 } 758 759 function scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath: Path, isCreatingWatchedDirectory: boolean) { 760 if (isCreatingWatchedDirectory) { 761 // Watching directory is created 762 // Invalidate any resolution has failed lookup in this directory 763 isInDirectoryChecks.push(fileOrDirectoryPath); 764 } 765 else { 766 // If something to do with folder/file starting with "." in node_modules folder, skip it 767 const updatedPath = removeIgnoredPath(fileOrDirectoryPath); 768 if (!updatedPath) return false; 769 fileOrDirectoryPath = updatedPath; 770 771 // prevent saving an open file from over-eagerly triggering invalidation 772 if (resolutionHost.fileIsOpen(fileOrDirectoryPath)) { 773 return false; 774 } 775 776 // Some file or directory in the watching directory is created 777 // Return early if it does not have any of the watching extension or not the custom failed lookup path 778 const dirOfFileOrDirectory = getDirectoryPath(fileOrDirectoryPath); 779 const isOHModules = isOhpm(resolutionHost.getCompilationSettings().packageManagerType); 780 if (isNodeModulesAtTypesDirectory(fileOrDirectoryPath) || (isOHModules && isOHModulesAtTypesDirectory(fileOrDirectoryPath)) || 781 isNodeModulesDirectory(fileOrDirectoryPath) || isNodeModulesAtTypesDirectory(dirOfFileOrDirectory) || (isOHModules && 782 isOHModulesAtTypesDirectory(dirOfFileOrDirectory)) || isNodeModulesDirectory(dirOfFileOrDirectory)) { 783 // Invalidate any resolution from this directory 784 failedLookupChecks.push(fileOrDirectoryPath); 785 startsWithPathChecks.push(fileOrDirectoryPath); 786 } 787 else { 788 if (!isPathWithDefaultFailedLookupExtension(fileOrDirectoryPath) && !customFailedLookupPaths.has(fileOrDirectoryPath)) { 789 return false; 790 } 791 // Ignore emits from the program 792 if (isEmittedFileOfProgram(resolutionHost.getCurrentProgram(), fileOrDirectoryPath)) { 793 return false; 794 } 795 // Resolution need to be invalidated if failed lookup location is same as the file or directory getting created 796 failedLookupChecks.push(fileOrDirectoryPath); 797 } 798 } 799 resolutionHost.scheduleInvalidateResolutionsOfFailedLookupLocations(); 800 } 801 802 function invalidateResolutionsOfFailedLookupLocations() { 803 if (!failedLookupChecks.length && !startsWithPathChecks.length && !isInDirectoryChecks.length) { 804 return false; 805 } 806 807 const invalidated = invalidateResolutions(resolutionsWithFailedLookups, canInvalidateFailedLookupResolution); 808 failedLookupChecks.length = 0; 809 startsWithPathChecks.length = 0; 810 isInDirectoryChecks.length = 0; 811 return invalidated; 812 } 813 814 function canInvalidateFailedLookupResolution(resolution: ResolutionWithFailedLookupLocations) { 815 return resolution.failedLookupLocations.some(location => { 816 const locationPath = resolutionHost.toPath(location); 817 return contains(failedLookupChecks, locationPath) || 818 startsWithPathChecks.some(fileOrDirectoryPath => startsWith(locationPath, fileOrDirectoryPath)) || 819 isInDirectoryChecks.some(fileOrDirectoryPath => isInDirectoryPath(fileOrDirectoryPath, locationPath)); 820 }); 821 } 822 823 function closeTypeRootsWatch() { 824 clearMap(typeRootsWatches, closeFileWatcher); 825 } 826 827 function getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot: string, typeRootPath: Path): Path | undefined { 828 if (isInDirectoryPath(rootPath, typeRootPath)) { 829 return rootPath; 830 } 831 const toWatch = getDirectoryToWatchFromFailedLookupLocationDirectory(typeRoot, typeRootPath); 832 return toWatch && directoryWatchesOfFailedLookups.has(toWatch.dirPath) ? toWatch.dirPath : undefined; 833 } 834 835 function createTypeRootsWatch(typeRootPath: Path, typeRoot: string): FileWatcher { 836 // Create new watch and recursive info 837 return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrDirectory => { 838 const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); 839 if (cachedDirectoryStructureHost) { 840 // Since the file existence changed, update the sourceFiles cache 841 cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); 842 } 843 844 // For now just recompile 845 // We could potentially store more data here about whether it was/would be really be used or not 846 // and with that determine to trigger compilation but for now this is enough 847 hasChangedAutomaticTypeDirectiveNames = true; 848 resolutionHost.onChangedAutomaticTypeDirectiveNames(); 849 850 // Since directory watchers invoked are flaky, the failed lookup location events might not be triggered 851 // So handle to failed lookup locations here as well to ensure we are invalidating resolutions 852 const dirPath = getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot, typeRootPath); 853 if (dirPath) { 854 scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath); 855 } 856 }, WatchDirectoryFlags.Recursive); 857 } 858 859 /** 860 * Watches the types that would get added as part of getAutomaticTypeDirectiveNames 861 * To be called when compiler options change 862 */ 863 function updateTypeRootsWatch() { 864 const options = resolutionHost.getCompilationSettings(); 865 if (options.types) { 866 // No need to do any watch since resolution cache is going to handle the failed lookups 867 // for the types added by this 868 closeTypeRootsWatch(); 869 return; 870 } 871 872 // we need to assume the directories exist to ensure that we can get all the type root directories that get included 873 // But filter directories that are at root level to say directory doesnt exist, so that we arent watching them 874 const typeRoots = getEffectiveTypeRoots(options, { directoryExists: directoryExistsForTypeRootWatch, getCurrentDirectory }); 875 if (typeRoots) { 876 mutateMap( 877 typeRootsWatches, 878 arrayToMap(typeRoots, tr => resolutionHost.toPath(tr)), 879 { 880 createNewValue: createTypeRootsWatch, 881 onDeleteValue: closeFileWatcher 882 } 883 ); 884 } 885 else { 886 closeTypeRootsWatch(); 887 } 888 } 889 890 /** 891 * Use this function to return if directory exists to get type roots to watch 892 * If we return directory exists then only the paths will be added to type roots 893 * Hence return true for all directories except root directories which are filtered from watching 894 */ 895 function directoryExistsForTypeRootWatch(nodeTypesDirectory: string) { 896 const dir = getDirectoryPath(getDirectoryPath(nodeTypesDirectory)); 897 const dirPath = resolutionHost.toPath(dir); 898 return dirPath === rootPath || canWatchDirectory(dirPath); 899 } 900 } 901} 902