/// /// /*@internal*/ namespace ts { /** This is the cache of module/typedirectives resolution that can be retained across program */ export interface ResolutionCache { startRecordingFilesWithChangedResolutions(): void; finishRecordingFilesWithChangedResolutions(): Path[]; resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[]; resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; invalidateResolutionOfFile(filePath: Path): void; createHasInvalidatedResolution(): HasInvalidatedResolution; startCachingPerDirectoryResolution(): void; finishCachingPerDirectoryResolution(): void; setRootDirectory(dir: string): void; clear(): void; } interface NameResolutionWithFailedLookupLocations { readonly failedLookupLocations: ReadonlyArray; isInvalidated?: boolean; } interface DirectoryWatchesOfFailedLookup { /** watcher for the directory of failed lookup */ watcher: FileWatcher; /** map with key being the failed lookup location path and value being the actual location */ mapLocations: Map; } export interface ResolutionCacheHost extends ModuleResolutionHost { toPath(fileName: string): Path; getCompilationSettings(): CompilerOptions; watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; onInvalidatedResolution(): void; getCachedPartialSystem?(): CachedPartialSystem; projectName?: string; getGlobalCache?(): string | undefined; writeLog(s: string): void; } const MAX_DIRPATHS_TO_RECURSE = 5; export function createResolutionCache(resolutionHost: ResolutionCacheHost): ResolutionCache { let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; let filesWithInvalidatedResolutions: Map | undefined; // The resolvedModuleNames and resolvedTypeReferenceDirectives are the cache of resolutions per file. // The key in the map is source file's path. // The values are Map of resolutions with key being name lookedup. const resolvedModuleNames = createMap>(); const perDirectoryResolvedModuleNames = createMap>(); const resolvedTypeReferenceDirectives = createMap>(); const perDirectoryResolvedTypeReferenceDirectives = createMap>(); const directoryWatchesOfFailedLookups = createMap(); const failedLookupLocationToDirPath = createMap(); let rootDir: string; let rootPath: Path; return { startRecordingFilesWithChangedResolutions, finishRecordingFilesWithChangedResolutions, startCachingPerDirectoryResolution, finishCachingPerDirectoryResolution, resolveModuleNames, resolveTypeReferenceDirectives, invalidateResolutionOfFile, createHasInvalidatedResolution, setRootDirectory, clear }; function setRootDirectory(dir: string) { Debug.assert(!resolvedModuleNames.size && !resolvedTypeReferenceDirectives.size && !directoryWatchesOfFailedLookups.size); rootDir = removeTrailingDirectorySeparator(getNormalizedAbsolutePath(dir, resolutionHost.getCurrentDirectory())); rootPath = resolutionHost.toPath(rootDir); } function isInDirectoryPath(dir: Path, file: Path) { if (dir === undefined || file.length <= dir.length) { return false; } return startsWith(file, dir) && file[dir.length] === directorySeparator; } function clear() { // Close all the watches for failed lookup locations, irrespective of refcounts for them since this is to clear the cache clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf); failedLookupLocationToDirPath.clear(); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); Debug.assert(perDirectoryResolvedModuleNames.size === 0 && perDirectoryResolvedTypeReferenceDirectives.size === 0); } function startRecordingFilesWithChangedResolutions() { filesWithChangedSetOfUnresolvedImports = []; } function finishRecordingFilesWithChangedResolutions() { const collected = filesWithChangedSetOfUnresolvedImports; filesWithChangedSetOfUnresolvedImports = undefined; return collected; } function createHasInvalidatedResolution(): HasInvalidatedResolution { const collected = filesWithInvalidatedResolutions; filesWithInvalidatedResolutions = undefined; return path => collected && collected.has(path); } function startCachingPerDirectoryResolution() { Debug.assert(perDirectoryResolvedModuleNames.size === 0 && perDirectoryResolvedTypeReferenceDirectives.size === 0); } function finishCachingPerDirectoryResolution() { perDirectoryResolvedModuleNames.clear(); perDirectoryResolvedTypeReferenceDirectives.clear(); } function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations { const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host); // return result immediately only if global cache support is not enabled or if it is .ts, .tsx or .d.ts if (!resolutionHost.getGlobalCache) { return primaryResult; } // otherwise try to load typings from @types const globalCache = resolutionHost.getGlobalCache(); if (globalCache !== undefined && !isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension))) { // create different collection of failed lookup locations for second pass // if it will fail and we've already found something during the first pass - we don't want to pollute its results const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, resolutionHost.projectName, compilerOptions, host, globalCache); if (resolvedModule) { return { resolvedModule, failedLookupLocations: addRange(primaryResult.failedLookupLocations as Array, failedLookupLocations) }; } } // Default return the result from the first pass return primaryResult; } function resolveNamesWithLocalCache( names: string[], containingFile: string, cache: Map>, perDirectoryCache: Map>, loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, getResult: (s: T) => R, getResultFileName: (result: R) => string | undefined, logChanges: boolean): R[] { const path = resolutionHost.toPath(containingFile); const currentResolutionsInFile = cache.get(path); const dirPath = getDirectoryPath(path); let perDirectoryResolution = perDirectoryCache.get(dirPath); if (!perDirectoryResolution) { perDirectoryResolution = createMap(); perDirectoryCache.set(dirPath, perDirectoryResolution); } const newResolutions: Map = createMap(); const resolvedModules: R[] = []; const compilerOptions = resolutionHost.getCompilationSettings(); for (const name of names) { // check if this is a duplicate entry in the list let resolution = newResolutions.get(name); if (!resolution) { const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name); if (existingResolution) { // Remove from the cache since we would update the resolution in new file ourselves currentResolutionsInFile.delete(name); } if (moduleResolutionIsValid(existingResolution)) { // ok, it is safe to use existing name resolution results resolution = existingResolution; } else { const resolutionInDirectory = perDirectoryResolution && perDirectoryResolution.get(name); if (resolutionInDirectory) { resolution = resolutionInDirectory; } else { resolution = loader(name, containingFile, compilerOptions, resolutionHost); perDirectoryResolution.set(name, resolution); } updateFailedLookupLocationWatches(resolution.failedLookupLocations, existingResolution && existingResolution.failedLookupLocations); } newResolutions.set(name, resolution); if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { filesWithChangedSetOfUnresolvedImports.push(path); // reset log changes to avoid recording the same file multiple times logChanges = false; } } Debug.assert(resolution !== undefined); resolvedModules.push(getResult(resolution)); } // Close all the file watchers for the names that arent required any more if (currentResolutionsInFile) { clearMap(currentResolutionsInFile, resolution => withFailedLookupLocations(resolution.failedLookupLocations, closeFailedLookupLocationWatcher)); } // replace old results with a new one cache.set(path, newResolutions); return resolvedModules; function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean { if (oldResolution === newResolution) { return true; } if (!oldResolution || !newResolution || oldResolution.isInvalidated) { return false; } const oldResult = getResult(oldResolution); const newResult = getResult(newResolution); if (oldResult === newResult) { return true; } if (!oldResult || !newResult) { return false; } return getResultFileName(oldResult) === getResultFileName(newResult); } function moduleResolutionIsValid(resolution: T): boolean { if (!resolution || resolution.isInvalidated) { return false; } const result = getResult(resolution); if (result) { return true; } // consider situation if we have no candidate locations as valid resolution. // after all there is no point to invalidate it if we have no idea where to look for the module. return resolution.failedLookupLocations.length === 0; } } function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { resolutionHost.writeLog(`resolveTypeReferenceDirectives: ${typeDirectiveNames} in ${containingFile}`); return resolveNamesWithLocalCache( typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, perDirectoryResolvedTypeReferenceDirectives, resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false ); } function resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[] { resolutionHost.writeLog(`resolveModuleNames: ${moduleNames} in ${containingFile}`); return resolveNamesWithLocalCache( moduleNames, containingFile, resolvedModuleNames, perDirectoryResolvedModuleNames, resolveModuleName, m => m.resolvedModule, r => r.resolvedFileName, logChanges ); } function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path) { const cachedDir = failedLookupLocationToDirPath.get(failedLookupLocationPath); if (cachedDir) { watchFailedLookupLocationInDirectory(cachedDir, failedLookupLocationPath, /*dir*/ undefined); return; } if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { // Watch in directory of rootPath watchFailedLookupLocationInDirectory(rootPath, failedLookupLocationPath, rootDir); return; } let dirPath = getDirectoryPath(failedLookupLocationPath); let dir = getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, resolutionHost.getCurrentDirectory())); for (let i = 0; i < MAX_DIRPATHS_TO_RECURSE; i++) { const parentPath = getDirectoryPath(dirPath); if (directoryWatchesOfFailedLookups.has(dirPath) || parentPath === dirPath) { watchFailedLookupLocationInDirectory(dirPath, failedLookupLocationPath, dir); return; } dirPath = parentPath; dir = getDirectoryPath(dir); } // Verify there are no watches in parent directory const ancestorDirPath = getAncestorDirectoryWithWatches(dirPath); // We wont need directory if we are using ancestor since its already cached watchFailedLookupLocationInDirectory(ancestorDirPath || dirPath, failedLookupLocationPath, dir); } function getAncestorDirectoryWithWatches(dirPath: Path) { for (let parentDirPath = getDirectoryPath(dirPath); parentDirPath !== dirPath; parentDirPath = getDirectoryPath(parentDirPath)) { if (directoryWatchesOfFailedLookups.has(parentDirPath)) { return parentDirPath; } dirPath = parentDirPath; } return undefined; } function watchFailedLookupLocationInDirectory(dirPath: Path, failedLookupLocationPath: Path, dir: string | undefined) { failedLookupLocationToDirPath.set(failedLookupLocationPath, dirPath); const watches = directoryWatchesOfFailedLookups.get(dirPath); if (watches) { const existingCount = watches.mapLocations.get(failedLookupLocationPath) || 0; watches.mapLocations.set(failedLookupLocationPath, existingCount + 1); } else { Debug.assert(dir !== undefined); const mapLocations = createMap(); mapLocations.set(failedLookupLocationPath, 1); directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath), mapLocations }); } } function createDirectoryWatcher(directory: string, dirPath: Path) { return resolutionHost.watchDirectoryOfFailedLookupLocation(directory, fileOrFolder => { const fileOrFolderPath = resolutionHost.toPath(fileOrFolder); if (resolutionHost.getCachedPartialSystem) { // Since the file existance changed, update the sourceFiles cache resolutionHost.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); } // If the location results in update to failed lookup, schedule program update if (dirPath === fileOrFolderPath) { onAddOrRemoveDirectoryOfFailedLookup(dirPath); resolutionHost.onInvalidatedResolution(); } else if (onFileAddOrRemoveInDirectoryOfFailedLookup(dirPath, fileOrFolderPath)) { resolutionHost.onInvalidatedResolution(); } }, WatchDirectoryFlags.Recursive); } function closeFailedLookupLocationWatcher(_failedLookupLocation: string, failedLookupLocationPath: Path) { const dirPath = failedLookupLocationToDirPath.get(failedLookupLocationPath); const watches = directoryWatchesOfFailedLookups.get(dirPath); const refCount = watches.mapLocations.get(failedLookupLocationPath); if (refCount === 1) { // If this was last failed lookup location being tracked by the dir watcher, // remove the failed lookup location path to dir Path entry watches.mapLocations.delete(failedLookupLocationPath); failedLookupLocationToDirPath.delete(failedLookupLocationPath); // If there are no more files that need this watcher alive, close the watcher if (watches.mapLocations.size === 0) { watches.watcher.close(); directoryWatchesOfFailedLookups.delete(dirPath); } } else { watches.mapLocations.set(failedLookupLocationPath, refCount - 1); } } type FailedLookupLocationAction = (failedLookupLocation: string, failedLookupLocationPath: Path) => void; function withFailedLookupLocations(failedLookupLocations: ReadonlyArray | undefined, fn: FailedLookupLocationAction, startIndex?: number) { if (failedLookupLocations) { for (let i = startIndex || 0; i < failedLookupLocations.length; i++) { fn(failedLookupLocations[i], resolutionHost.toPath(failedLookupLocations[i])); } } } function updateFailedLookupLocationWatches(failedLookupLocations: ReadonlyArray | undefined, existingFailedLookupLocations: ReadonlyArray | undefined) { const index = existingFailedLookupLocations && failedLookupLocations ? findDiffIndex(failedLookupLocations, existingFailedLookupLocations) : 0; // Watch all the failed lookup locations withFailedLookupLocations(failedLookupLocations, watchFailedLookupLocation, index); // Close existing watches for the failed locations withFailedLookupLocations(existingFailedLookupLocations, closeFailedLookupLocationWatcher, index); } function invalidateResolutionCacheOfDeletedFile( deletedFilePath: Path, cache: Map>, getResult: (s: T) => R, getResultFileName: (result: R) => string | undefined) { cache.forEach((value, path) => { if (path === deletedFilePath) { cache.delete(path); value.forEach(resolution => { withFailedLookupLocations(resolution.failedLookupLocations, closeFailedLookupLocationWatcher); }); } else if (value) { value.forEach(resolution => { if (resolution && !resolution.isInvalidated) { const result = getResult(resolution); if (result) { if (resolutionHost.toPath(getResultFileName(result)) === deletedFilePath) { resolution.isInvalidated = true; (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(path, true); } } } }); } }); } function invalidateResolutionCacheOfChangedFailedLookupLocation( cache: Map>, isChangedFailedLookupLocation: (location: string) => boolean ) { cache.forEach((value, containingFile) => { if (value) { value.forEach(resolution => { if (resolution && !resolution.isInvalidated && some(resolution.failedLookupLocations, isChangedFailedLookupLocation)) { // Mark the file as needing re-evaluation of module resolution instead of using it blindly. resolution.isInvalidated = true; (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFile, true); } }); } }); } function invalidateResolutionOfFile(filePath: Path) { invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName); invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); } function onFileAddOrRemoveInDirectoryOfFailedLookup(dirPath: Path, fileOrFolder: Path) { const watches = directoryWatchesOfFailedLookups.get(dirPath); const isFailedLookupFile = watches.mapLocations.has(fileOrFolder); if (isFailedLookupFile) { const isFileOrFolder: (location: string) => boolean = location => resolutionHost.toPath(location) === fileOrFolder; invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedModuleNames, isFileOrFolder); invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedTypeReferenceDirectives, isFileOrFolder); } return isFailedLookupFile; } function onAddOrRemoveDirectoryOfFailedLookup(dirPath: Path) { const isInDirPath: (location: string) => boolean = location => isInDirectoryPath(dirPath, resolutionHost.toPath(location)); invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedModuleNames, isInDirPath); invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedTypeReferenceDirectives, isInDirPath); } } }