mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-17 01:10:38 -06:00
If these are regular comments, then they won't appear in our d.ts files. But, now we are relying on an external d.ts bundler to produce our final merged, so they need to be present in the "input" d.ts files, meaning they have to be JSDoc comments. These comments only work today because all of our builds load their TS files from scratch, so they see the actual source files and their non-JSDoc comments. The comments also need to be attached to a declaration, not floating, otherwise they won't be used by api-extractor, so move them if needed.
1187 lines
63 KiB
TypeScript
1187 lines
63 KiB
TypeScript
import * as ts from "./_namespaces/ts";
|
|
import {
|
|
arrayToMap, CachedDirectoryStructureHost, CacheWithRedirects, CharacterCodes, clearMap, closeFileWatcher,
|
|
closeFileWatcherOf, CompilerOptions, contains, createCacheWithRedirects, createModeAwareCache,
|
|
createModuleResolutionCache, createMultiMap, createTypeReferenceDirectiveResolutionCache, Debug, Diagnostics,
|
|
directorySeparator, DirectoryWatcherCallback, emptyArray, emptyIterator, endsWith, ESMap, Extension, extensionIsTS,
|
|
fileExtensionIs, fileExtensionIsOneOf, FileReference, FileWatcher, FileWatcherCallback, firstDefinedIterator,
|
|
GetCanonicalFileName, getDirectoryPath, getEffectiveTypeRoots, getModeForFileReference, getModeForResolutionAtIndex,
|
|
getModeForUsageLocation, getNormalizedAbsolutePath, getResolutionName, getRootLength, HasInvalidatedResolutions,
|
|
ignoredPaths, inferredTypesContainingFile, isEmittedFileOfProgram, isExternalModuleNameRelative,
|
|
isExternalOrCommonJsModule, isNodeModulesDirectory, isRootedDiskPath, isString, isStringLiteralLike, isTraceEnabled,
|
|
length, loadModuleFromGlobalCache, Map, memoize, MinimalResolutionCacheHost, ModeAwareCache, ModuleKind,
|
|
ModuleResolutionCache, ModuleResolutionHost, ModuleResolutionInfo, mutateMap, noopFileWatcher, normalizePath,
|
|
PackageId, packageIdToString, parseNodeModuleFromPath, Path, pathContainsNodeModules, PerModuleNameCache, Program,
|
|
ReadonlyESMap, removeSuffix, removeTrailingDirectorySeparator, resolutionExtensionIsTSOrJson, ResolvedModuleFull,
|
|
ResolvedModuleWithFailedLookupLocations, ResolvedProjectReference, ResolvedTypeReferenceDirective,
|
|
ResolvedTypeReferenceDirectiveWithFailedLookupLocations, returnTrue, Set, some, SourceFile, startsWith,
|
|
stringContains, trace, unorderedRemoveItem, WatchDirectoryFlags,
|
|
} from "./_namespaces/ts";
|
|
|
|
/**
|
|
* This is the cache of module/typedirectives resolution that can be retained across program
|
|
*
|
|
* @internal
|
|
*/
|
|
export interface ResolutionCache {
|
|
startRecordingFilesWithChangedResolutions(): void;
|
|
finishRecordingFilesWithChangedResolutions(): Path[] | undefined;
|
|
|
|
resolveModuleNames(
|
|
moduleNames: string[],
|
|
containingFile: string,
|
|
reusedNames: string[] | undefined,
|
|
redirectedReference: ResolvedProjectReference | undefined,
|
|
containingSourceFile: SourceFile | undefined,
|
|
resolutionInfo: ModuleResolutionInfo | undefined
|
|
): (ResolvedModuleFull | undefined)[];
|
|
getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): CachedResolvedModuleWithFailedLookupLocations | undefined;
|
|
resolveTypeReferenceDirectives(typeDirectiveNames: string[] | readonly FileReference[], containingFile: string, redirectedReference?: ResolvedProjectReference, containingFileMode?: SourceFile["impliedNodeFormat"]): (ResolvedTypeReferenceDirective | undefined)[];
|
|
|
|
invalidateResolutionsOfFailedLookupLocations(): boolean;
|
|
invalidateResolutionOfFile(filePath: Path): void;
|
|
removeResolutionsOfFile(filePath: Path): void;
|
|
removeResolutionsFromProjectReferenceRedirects(filePath: Path): void;
|
|
setFilesWithInvalidatedNonRelativeUnresolvedImports(filesWithUnresolvedImports: ESMap<Path, readonly string[]>): void;
|
|
createHasInvalidatedResolutions(customHasInvalidatedResolutions: HasInvalidatedResolutions): HasInvalidatedResolutions;
|
|
hasChangedAutomaticTypeDirectiveNames(): boolean;
|
|
isFileWithInvalidatedNonRelativeUnresolvedImports(path: Path): boolean;
|
|
|
|
|
|
startCachingPerDirectoryResolution(): void;
|
|
finishCachingPerDirectoryResolution(newProgram: Program | undefined, oldProgram: Program | undefined): void;
|
|
|
|
updateTypeRootsWatch(): void;
|
|
closeTypeRootsWatch(): void;
|
|
|
|
getModuleResolutionCache(): ModuleResolutionCache;
|
|
|
|
clear(): void;
|
|
}
|
|
|
|
interface ResolutionWithFailedLookupLocations {
|
|
readonly failedLookupLocations: string[];
|
|
readonly affectingLocations: string[];
|
|
isInvalidated?: boolean;
|
|
refCount?: number;
|
|
// Files that have this resolution using
|
|
files?: Path[];
|
|
}
|
|
|
|
interface ResolutionWithResolvedFileName {
|
|
resolvedFileName: string | undefined;
|
|
packagetId?: PackageId;
|
|
}
|
|
|
|
interface CachedResolvedModuleWithFailedLookupLocations extends ResolvedModuleWithFailedLookupLocations, ResolutionWithFailedLookupLocations {
|
|
}
|
|
|
|
interface CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations extends ResolvedTypeReferenceDirectiveWithFailedLookupLocations, ResolutionWithFailedLookupLocations {
|
|
}
|
|
|
|
/** @internal */
|
|
export interface ResolutionCacheHost extends MinimalResolutionCacheHost {
|
|
toPath(fileName: string): Path;
|
|
getCanonicalFileName: GetCanonicalFileName;
|
|
getCompilationSettings(): CompilerOptions;
|
|
watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher;
|
|
watchAffectingFileLocation(file: string, cb: FileWatcherCallback): FileWatcher;
|
|
onInvalidatedResolution(): void;
|
|
watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher;
|
|
onChangedAutomaticTypeDirectiveNames(): void;
|
|
scheduleInvalidateResolutionsOfFailedLookupLocations(): void;
|
|
getCachedDirectoryStructureHost(): CachedDirectoryStructureHost | undefined;
|
|
projectName?: string;
|
|
getGlobalCache?(): string | undefined;
|
|
globalCacheResolutionModuleName?(externalModuleName: string): string;
|
|
writeLog(s: string): void;
|
|
getCurrentProgram(): Program | undefined;
|
|
fileIsOpen(filePath: Path): boolean;
|
|
onDiscoveredSymlink?(): void;
|
|
}
|
|
|
|
interface FileWatcherOfAffectingLocation {
|
|
/** watcher for the lookup */
|
|
watcher: FileWatcher;
|
|
resolutions: number;
|
|
files: number;
|
|
paths: Set<string>;
|
|
}
|
|
|
|
interface DirectoryWatchesOfFailedLookup {
|
|
/** watcher for the lookup */
|
|
watcher: FileWatcher;
|
|
/** ref count keeping this watch alive */
|
|
refCount: number;
|
|
/** is the directory watched being non recursive */
|
|
nonRecursive?: boolean;
|
|
}
|
|
|
|
interface DirectoryOfFailedLookupWatch {
|
|
dir: string;
|
|
dirPath: Path;
|
|
nonRecursive?: boolean;
|
|
}
|
|
|
|
/** @internal */
|
|
export function removeIgnoredPath(path: Path): Path | undefined {
|
|
// Consider whole staging folder as if node_modules changed.
|
|
if (endsWith(path, "/node_modules/.staging")) {
|
|
return removeSuffix(path, "/.staging") as Path;
|
|
}
|
|
|
|
return some(ignoredPaths, searchPath => stringContains(path, searchPath)) ?
|
|
undefined :
|
|
path;
|
|
}
|
|
|
|
/**
|
|
* Filter out paths like
|
|
* "/", "/user", "/user/username", "/user/username/folderAtRoot",
|
|
* "c:/", "c:/users", "c:/users/username", "c:/users/username/folderAtRoot", "c:/folderAtRoot"
|
|
* @param dirPath
|
|
*
|
|
* @internal
|
|
*/
|
|
export function canWatchDirectoryOrFile(dirPath: Path) {
|
|
const rootLength = getRootLength(dirPath);
|
|
if (dirPath.length === rootLength) {
|
|
// Ignore "/", "c:/"
|
|
return false;
|
|
}
|
|
|
|
let nextDirectorySeparator = dirPath.indexOf(directorySeparator, rootLength);
|
|
if (nextDirectorySeparator === -1) {
|
|
// ignore "/user", "c:/users" or "c:/folderAtRoot"
|
|
return false;
|
|
}
|
|
|
|
let pathPartForUserCheck = dirPath.substring(rootLength, nextDirectorySeparator + 1);
|
|
const isNonDirectorySeparatorRoot = rootLength > 1 || dirPath.charCodeAt(0) !== CharacterCodes.slash;
|
|
if (isNonDirectorySeparatorRoot &&
|
|
dirPath.search(/[a-zA-Z]:/) !== 0 && // Non dos style paths
|
|
pathPartForUserCheck.search(/[a-zA-Z]\$\//) === 0) { // Dos style nextPart
|
|
nextDirectorySeparator = dirPath.indexOf(directorySeparator, nextDirectorySeparator + 1);
|
|
if (nextDirectorySeparator === -1) {
|
|
// ignore "//vda1cs4850/c$/folderAtRoot"
|
|
return false;
|
|
}
|
|
|
|
pathPartForUserCheck = dirPath.substring(rootLength + pathPartForUserCheck.length, nextDirectorySeparator + 1);
|
|
}
|
|
|
|
if (isNonDirectorySeparatorRoot &&
|
|
pathPartForUserCheck.search(/users\//i) !== 0) {
|
|
// Paths like c:/folderAtRoot/subFolder are allowed
|
|
return true;
|
|
}
|
|
|
|
for (let searchIndex = nextDirectorySeparator + 1, searchLevels = 2; searchLevels > 0; searchLevels--) {
|
|
searchIndex = dirPath.indexOf(directorySeparator, searchIndex) + 1;
|
|
if (searchIndex === 0) {
|
|
// Folder isnt at expected minimum levels
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
type GetResolutionWithResolvedFileName<T extends ResolutionWithFailedLookupLocations = ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName = ResolutionWithResolvedFileName> =
|
|
(resolution: T) => R | undefined;
|
|
|
|
/** @internal */
|
|
export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string | undefined, logChangesWhenResolvingModule: boolean): ResolutionCache {
|
|
let filesWithChangedSetOfUnresolvedImports: Path[] | undefined;
|
|
let filesWithInvalidatedResolutions: Set<Path> | undefined;
|
|
let filesWithInvalidatedNonRelativeUnresolvedImports: ReadonlyESMap<Path, readonly string[]> | undefined;
|
|
const nonRelativeExternalModuleResolutions = createMultiMap<ResolutionWithFailedLookupLocations>();
|
|
|
|
const resolutionsWithFailedLookups: ResolutionWithFailedLookupLocations[] = [];
|
|
const resolutionsWithOnlyAffectingLocations: ResolutionWithFailedLookupLocations[] = [];
|
|
const resolvedFileToResolution = createMultiMap<ResolutionWithFailedLookupLocations>();
|
|
const impliedFormatPackageJsons = new Map<Path, readonly string[]>();
|
|
|
|
let hasChangedAutomaticTypeDirectiveNames = false;
|
|
let affectingPathChecksForFile: Set<string> | undefined;
|
|
let affectingPathChecks: Set<string> | undefined;
|
|
let failedLookupChecks: Set<Path> | undefined;
|
|
let startsWithPathChecks: Set<Path> | undefined;
|
|
let isInDirectoryChecks: Set<Path> | undefined;
|
|
|
|
const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory!()); // TODO: GH#18217
|
|
const cachedDirectoryStructureHost = resolutionHost.getCachedDirectoryStructureHost();
|
|
|
|
// 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 = new Map<Path, ModeAwareCache<CachedResolvedModuleWithFailedLookupLocations>>();
|
|
const perDirectoryResolvedModuleNames: CacheWithRedirects<ModeAwareCache<CachedResolvedModuleWithFailedLookupLocations>> = createCacheWithRedirects();
|
|
const nonRelativeModuleNameCache: CacheWithRedirects<PerModuleNameCache> = createCacheWithRedirects();
|
|
const moduleResolutionCache = createModuleResolutionCache(
|
|
getCurrentDirectory(),
|
|
resolutionHost.getCanonicalFileName,
|
|
/*options*/ undefined,
|
|
perDirectoryResolvedModuleNames,
|
|
nonRelativeModuleNameCache,
|
|
);
|
|
|
|
const resolvedTypeReferenceDirectives = new Map<Path, ModeAwareCache<CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
|
|
const perDirectoryResolvedTypeReferenceDirectives: CacheWithRedirects<ModeAwareCache<CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations>> = createCacheWithRedirects();
|
|
const typeReferenceDirectiveResolutionCache = createTypeReferenceDirectiveResolutionCache(
|
|
getCurrentDirectory(),
|
|
resolutionHost.getCanonicalFileName,
|
|
/*options*/ undefined,
|
|
moduleResolutionCache.getPackageJsonInfoCache(),
|
|
perDirectoryResolvedTypeReferenceDirectives
|
|
);
|
|
|
|
/**
|
|
* These are the extensions that failed lookup files will have by default,
|
|
* any other extension of failed lookup will be store that path in custom failed lookup path
|
|
* This helps in not having to comb through all resolutions when files are added/removed
|
|
* Note that .d.ts file also has .d.ts extension hence will be part of default extensions
|
|
*/
|
|
const failedLookupDefaultExtensions = [Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx, Extension.Json];
|
|
const customFailedLookupPaths = new Map<string, number>();
|
|
|
|
const directoryWatchesOfFailedLookups = new Map<string, DirectoryWatchesOfFailedLookup>();
|
|
const fileWatchesOfAffectingLocations = new Map<string, FileWatcherOfAffectingLocation>();
|
|
const rootDir = rootDirForResolution && removeTrailingDirectorySeparator(getNormalizedAbsolutePath(rootDirForResolution, getCurrentDirectory()));
|
|
const rootPath = (rootDir && resolutionHost.toPath(rootDir)) as Path; // TODO: GH#18217
|
|
const rootSplitLength = rootPath !== undefined ? rootPath.split(directorySeparator).length : 0;
|
|
|
|
// TypeRoot watches for the types that get added as part of getAutomaticTypeDirectiveNames
|
|
const typeRootsWatches = new Map<string, FileWatcher>();
|
|
|
|
return {
|
|
getModuleResolutionCache: () => moduleResolutionCache,
|
|
startRecordingFilesWithChangedResolutions,
|
|
finishRecordingFilesWithChangedResolutions,
|
|
// perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update
|
|
// (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution)
|
|
startCachingPerDirectoryResolution,
|
|
finishCachingPerDirectoryResolution,
|
|
resolveModuleNames,
|
|
getResolvedModuleWithFailedLookupLocationsFromCache,
|
|
resolveTypeReferenceDirectives,
|
|
removeResolutionsFromProjectReferenceRedirects,
|
|
removeResolutionsOfFile,
|
|
hasChangedAutomaticTypeDirectiveNames: () => hasChangedAutomaticTypeDirectiveNames,
|
|
invalidateResolutionOfFile,
|
|
invalidateResolutionsOfFailedLookupLocations,
|
|
setFilesWithInvalidatedNonRelativeUnresolvedImports,
|
|
createHasInvalidatedResolutions,
|
|
isFileWithInvalidatedNonRelativeUnresolvedImports,
|
|
updateTypeRootsWatch,
|
|
closeTypeRootsWatch,
|
|
clear
|
|
};
|
|
|
|
function getResolvedModule(resolution: CachedResolvedModuleWithFailedLookupLocations) {
|
|
return resolution.resolvedModule;
|
|
}
|
|
|
|
function getResolvedTypeReferenceDirective(resolution: CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations) {
|
|
return resolution.resolvedTypeReferenceDirective;
|
|
}
|
|
|
|
function isInDirectoryPath(dir: Path | undefined, file: Path) {
|
|
if (dir === undefined || file.length <= dir.length) {
|
|
return false;
|
|
}
|
|
return startsWith(file, dir) && file[dir.length] === directorySeparator;
|
|
}
|
|
|
|
function clear() {
|
|
clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf);
|
|
clearMap(fileWatchesOfAffectingLocations, closeFileWatcherOf);
|
|
customFailedLookupPaths.clear();
|
|
nonRelativeExternalModuleResolutions.clear();
|
|
closeTypeRootsWatch();
|
|
resolvedModuleNames.clear();
|
|
resolvedTypeReferenceDirectives.clear();
|
|
resolvedFileToResolution.clear();
|
|
resolutionsWithFailedLookups.length = 0;
|
|
resolutionsWithOnlyAffectingLocations.length = 0;
|
|
failedLookupChecks = undefined;
|
|
startsWithPathChecks = undefined;
|
|
isInDirectoryChecks = undefined;
|
|
affectingPathChecks = undefined;
|
|
affectingPathChecksForFile = undefined;
|
|
moduleResolutionCache.clear();
|
|
typeReferenceDirectiveResolutionCache.clear();
|
|
impliedFormatPackageJsons.clear();
|
|
hasChangedAutomaticTypeDirectiveNames = false;
|
|
}
|
|
|
|
function startRecordingFilesWithChangedResolutions() {
|
|
filesWithChangedSetOfUnresolvedImports = [];
|
|
}
|
|
|
|
function finishRecordingFilesWithChangedResolutions() {
|
|
const collected = filesWithChangedSetOfUnresolvedImports;
|
|
filesWithChangedSetOfUnresolvedImports = undefined;
|
|
return collected;
|
|
}
|
|
|
|
function isFileWithInvalidatedNonRelativeUnresolvedImports(path: Path): boolean {
|
|
if (!filesWithInvalidatedNonRelativeUnresolvedImports) {
|
|
return false;
|
|
}
|
|
|
|
// Invalidated if file has unresolved imports
|
|
const value = filesWithInvalidatedNonRelativeUnresolvedImports.get(path);
|
|
return !!value && !!value.length;
|
|
}
|
|
|
|
function createHasInvalidatedResolutions(customHasInvalidatedResolutions: HasInvalidatedResolutions): HasInvalidatedResolutions {
|
|
// Ensure pending resolutions are applied
|
|
invalidateResolutionsOfFailedLookupLocations();
|
|
const collected = filesWithInvalidatedResolutions;
|
|
filesWithInvalidatedResolutions = undefined;
|
|
return path => customHasInvalidatedResolutions(path) ||
|
|
!!collected?.has(path) ||
|
|
isFileWithInvalidatedNonRelativeUnresolvedImports(path);
|
|
}
|
|
|
|
function startCachingPerDirectoryResolution() {
|
|
moduleResolutionCache.clearAllExceptPackageJsonInfoCache();
|
|
typeReferenceDirectiveResolutionCache.clearAllExceptPackageJsonInfoCache();
|
|
// perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update
|
|
// (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution)
|
|
nonRelativeExternalModuleResolutions.forEach(watchFailedLookupLocationOfNonRelativeModuleResolutions);
|
|
nonRelativeExternalModuleResolutions.clear();
|
|
}
|
|
|
|
function finishCachingPerDirectoryResolution(newProgram: Program | undefined, oldProgram: Program | undefined) {
|
|
filesWithInvalidatedNonRelativeUnresolvedImports = undefined;
|
|
nonRelativeExternalModuleResolutions.forEach(watchFailedLookupLocationOfNonRelativeModuleResolutions);
|
|
nonRelativeExternalModuleResolutions.clear();
|
|
// Update file watches
|
|
if (newProgram !== oldProgram) {
|
|
newProgram?.getSourceFiles().forEach(newFile => {
|
|
const expected = isExternalOrCommonJsModule(newFile) ? newFile.packageJsonLocations?.length ?? 0 : 0;
|
|
const existing = impliedFormatPackageJsons.get(newFile.path) ?? emptyArray;
|
|
for (let i = existing.length; i < expected; i++) {
|
|
createFileWatcherOfAffectingLocation(newFile.packageJsonLocations![i], /*forResolution*/ false);
|
|
}
|
|
if (existing.length > expected) {
|
|
for (let i = expected; i < existing.length; i++) {
|
|
fileWatchesOfAffectingLocations.get(existing[i])!.files--;
|
|
}
|
|
}
|
|
if (expected) impliedFormatPackageJsons.set(newFile.path, newFile.packageJsonLocations!);
|
|
else impliedFormatPackageJsons.delete(newFile.path);
|
|
});
|
|
impliedFormatPackageJsons.forEach((existing, path) => {
|
|
if (!newProgram?.getSourceFileByPath(path)) {
|
|
existing.forEach(location => fileWatchesOfAffectingLocations.get(location)!.files--);
|
|
impliedFormatPackageJsons.delete(path);
|
|
}
|
|
});
|
|
}
|
|
directoryWatchesOfFailedLookups.forEach((watcher, path) => {
|
|
if (watcher.refCount === 0) {
|
|
directoryWatchesOfFailedLookups.delete(path);
|
|
watcher.watcher.close();
|
|
}
|
|
});
|
|
fileWatchesOfAffectingLocations.forEach((watcher, path) => {
|
|
if (watcher.files === 0 && watcher.resolutions === 0) {
|
|
fileWatchesOfAffectingLocations.delete(path);
|
|
watcher.watcher.close();
|
|
}
|
|
});
|
|
|
|
hasChangedAutomaticTypeDirectiveNames = false;
|
|
}
|
|
|
|
function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference, _containingSourceFile?: never, mode?: ModuleKind.CommonJS | ModuleKind.ESNext | undefined): CachedResolvedModuleWithFailedLookupLocations {
|
|
const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host, moduleResolutionCache, redirectedReference, mode);
|
|
// 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 && extensionIsTS(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, affectingLocations } = loadModuleFromGlobalCache(
|
|
Debug.checkDefined(resolutionHost.globalCacheResolutionModuleName)(moduleName),
|
|
resolutionHost.projectName,
|
|
compilerOptions,
|
|
host,
|
|
globalCache,
|
|
moduleResolutionCache,
|
|
);
|
|
if (resolvedModule) {
|
|
// Modify existing resolution so its saved in the directory cache as well
|
|
(primaryResult.resolvedModule as any) = resolvedModule;
|
|
primaryResult.failedLookupLocations.push(...failedLookupLocations);
|
|
primaryResult.affectingLocations.push(...affectingLocations);
|
|
return primaryResult;
|
|
}
|
|
}
|
|
|
|
// Default return the result from the first pass
|
|
return primaryResult;
|
|
}
|
|
|
|
function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference, _containingSourceFile?: SourceFile, resolutionMode?: SourceFile["impliedNodeFormat"] | undefined): CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations {
|
|
return ts.resolveTypeReferenceDirective(typeReferenceDirectiveName, containingFile, options, host, redirectedReference, typeReferenceDirectiveResolutionCache, resolutionMode);
|
|
}
|
|
|
|
interface ResolveNamesWithLocalCacheInput<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName> {
|
|
names: readonly string[] | readonly FileReference[];
|
|
containingFile: string;
|
|
redirectedReference: ResolvedProjectReference | undefined;
|
|
cache: ESMap<Path, ModeAwareCache<T>>;
|
|
perDirectoryCacheWithRedirects: CacheWithRedirects<ModeAwareCache<T>>;
|
|
loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference, containingSourceFile?: SourceFile, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext | undefined) => T;
|
|
getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>;
|
|
shouldRetryResolution: (t: T) => boolean;
|
|
reusedNames?: readonly string[];
|
|
resolutionInfo?: ModuleResolutionInfo;
|
|
logChanges?: boolean;
|
|
containingSourceFile?: SourceFile;
|
|
containingSourceFileMode?: SourceFile["impliedNodeFormat"];
|
|
}
|
|
function resolveNamesWithLocalCache<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>({
|
|
names, containingFile, redirectedReference,
|
|
cache, perDirectoryCacheWithRedirects,
|
|
loader, getResolutionWithResolvedFileName,
|
|
shouldRetryResolution, reusedNames, resolutionInfo, logChanges, containingSourceFile, containingSourceFileMode
|
|
}: ResolveNamesWithLocalCacheInput<T, R>): (R | undefined)[] {
|
|
const path = resolutionHost.toPath(containingFile);
|
|
const resolutionsInFile = cache.get(path) || cache.set(path, createModeAwareCache()).get(path)!;
|
|
const dirPath = getDirectoryPath(path);
|
|
const perDirectoryCache = perDirectoryCacheWithRedirects.getOrCreateMapOfCacheRedirects(redirectedReference);
|
|
let perDirectoryResolution = perDirectoryCache.get(dirPath);
|
|
if (!perDirectoryResolution) {
|
|
perDirectoryResolution = createModeAwareCache();
|
|
perDirectoryCache.set(dirPath, perDirectoryResolution);
|
|
}
|
|
const resolvedModules: (R | undefined)[] = [];
|
|
const compilerOptions = resolutionHost.getCompilationSettings();
|
|
const hasInvalidatedNonRelativeUnresolvedImport = logChanges && isFileWithInvalidatedNonRelativeUnresolvedImports(path);
|
|
|
|
// All the resolutions in this file are invalidated if this file wasn't resolved using same redirect
|
|
const program = resolutionHost.getCurrentProgram();
|
|
const oldRedirect = program && program.getResolvedProjectReferenceToRedirect(containingFile);
|
|
const unmatchedRedirects = oldRedirect ?
|
|
!redirectedReference || redirectedReference.sourceFile.path !== oldRedirect.sourceFile.path :
|
|
!!redirectedReference;
|
|
|
|
const seenNamesInFile = createModeAwareCache<true>();
|
|
let i = 0;
|
|
for (const entry of containingSourceFile && resolutionInfo ? resolutionInfo.names : names) {
|
|
const name = !isString(entry) ? getResolutionName(entry) : entry;
|
|
// Imports supply a `containingSourceFile` but no `containingSourceFileMode` - it would be redundant
|
|
// they require calculating the mode for a given import from it's position in the resolution table, since a given
|
|
// import's syntax may override the file's default mode.
|
|
// Type references instead supply a `containingSourceFileMode` and a non-string entry which contains
|
|
// a default file mode override if applicable.
|
|
const mode = !isString(entry) ?
|
|
isStringLiteralLike(entry) ?
|
|
getModeForUsageLocation(containingSourceFile!, entry) :
|
|
getModeForFileReference(entry, containingSourceFileMode) :
|
|
containingSourceFile ?
|
|
getModeForResolutionAtIndex(containingSourceFile, i) :
|
|
undefined;
|
|
i++;
|
|
let resolution = resolutionsInFile.get(name, mode);
|
|
// Resolution is valid if it is present and not invalidated
|
|
if (!seenNamesInFile.has(name, mode) &&
|
|
unmatchedRedirects || !resolution || resolution.isInvalidated ||
|
|
// If the name is unresolved import that was invalidated, recalculate
|
|
(hasInvalidatedNonRelativeUnresolvedImport && !isExternalModuleNameRelative(name) && shouldRetryResolution(resolution))) {
|
|
const existingResolution = resolution;
|
|
const resolutionInDirectory = perDirectoryResolution.get(name, mode);
|
|
if (resolutionInDirectory) {
|
|
resolution = resolutionInDirectory;
|
|
const host = resolutionHost.getCompilerHost?.() || resolutionHost;
|
|
if (isTraceEnabled(compilerOptions, host)) {
|
|
const resolved = getResolutionWithResolvedFileName(resolution);
|
|
trace(
|
|
host,
|
|
loader === resolveModuleName as unknown ?
|
|
resolved?.resolvedFileName ?
|
|
resolved.packagetId ?
|
|
Diagnostics.Reusing_resolution_of_module_0_from_1_found_in_cache_from_location_2_it_was_successfully_resolved_to_3_with_Package_ID_4:
|
|
Diagnostics.Reusing_resolution_of_module_0_from_1_found_in_cache_from_location_2_it_was_successfully_resolved_to_3:
|
|
Diagnostics.Reusing_resolution_of_module_0_from_1_found_in_cache_from_location_2_it_was_not_resolved :
|
|
resolved?.resolvedFileName ?
|
|
resolved.packagetId ?
|
|
Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_found_in_cache_from_location_2_it_was_successfully_resolved_to_3_with_Package_ID_4 :
|
|
Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_found_in_cache_from_location_2_it_was_successfully_resolved_to_3 :
|
|
Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_found_in_cache_from_location_2_it_was_not_resolved,
|
|
name,
|
|
containingFile,
|
|
getDirectoryPath(containingFile),
|
|
resolved?.resolvedFileName,
|
|
resolved?.packagetId && packageIdToString(resolved.packagetId)
|
|
);
|
|
}
|
|
}
|
|
else {
|
|
resolution = loader(name, containingFile, compilerOptions, resolutionHost.getCompilerHost?.() || resolutionHost, redirectedReference, containingSourceFile, mode);
|
|
perDirectoryResolution.set(name, mode, resolution);
|
|
if (resolutionHost.onDiscoveredSymlink && resolutionIsSymlink(resolution)) {
|
|
resolutionHost.onDiscoveredSymlink();
|
|
}
|
|
}
|
|
resolutionsInFile.set(name, mode, resolution);
|
|
watchFailedLookupLocationsOfExternalModuleResolutions(name, resolution, path, getResolutionWithResolvedFileName);
|
|
if (existingResolution) {
|
|
stopWatchFailedLookupLocationOfResolution(existingResolution, path, getResolutionWithResolvedFileName);
|
|
}
|
|
|
|
if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) {
|
|
filesWithChangedSetOfUnresolvedImports.push(path);
|
|
// reset log changes to avoid recording the same file multiple times
|
|
logChanges = false;
|
|
}
|
|
}
|
|
else {
|
|
const host = resolutionHost.getCompilerHost?.() || resolutionHost;
|
|
if (isTraceEnabled(compilerOptions, host) && !seenNamesInFile.has(name, mode)) {
|
|
const resolved = getResolutionWithResolvedFileName(resolution);
|
|
trace(
|
|
host,
|
|
loader === resolveModuleName as unknown ?
|
|
resolved?.resolvedFileName ?
|
|
resolved.packagetId ?
|
|
Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_successfully_resolved_to_2_with_Package_ID_3 :
|
|
Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_successfully_resolved_to_2 :
|
|
Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_not_resolved :
|
|
resolved?.resolvedFileName ?
|
|
resolved.packagetId ?
|
|
Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_of_old_program_it_was_successfully_resolved_to_2_with_Package_ID_3 :
|
|
Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_of_old_program_it_was_successfully_resolved_to_2 :
|
|
Diagnostics.Reusing_resolution_of_type_reference_directive_0_from_1_of_old_program_it_was_not_resolved,
|
|
name,
|
|
containingFile,
|
|
resolved?.resolvedFileName,
|
|
resolved?.packagetId && packageIdToString(resolved.packagetId)
|
|
);
|
|
}
|
|
}
|
|
Debug.assert(resolution !== undefined && !resolution.isInvalidated);
|
|
seenNamesInFile.set(name, mode, true);
|
|
resolvedModules.push(getResolutionWithResolvedFileName(resolution));
|
|
}
|
|
|
|
if (containingSourceFile && resolutionInfo) {
|
|
resolutionInfo.reusedNames?.forEach(literal => seenNamesInFile.set(literal.text, getModeForUsageLocation(containingSourceFile, literal), true));
|
|
reusedNames = undefined;
|
|
}
|
|
if (resolutionsInFile.size() !== seenNamesInFile.size()) {
|
|
// Stop watching and remove the unused name
|
|
resolutionsInFile.forEach((resolution, name, mode) => {
|
|
if (!seenNamesInFile.has(name, mode) && !contains(reusedNames, name)) {
|
|
stopWatchFailedLookupLocationOfResolution(resolution, path, getResolutionWithResolvedFileName);
|
|
resolutionsInFile.delete(name, mode);
|
|
}
|
|
});
|
|
}
|
|
|
|
return resolvedModules;
|
|
|
|
function resolutionIsEqualTo(oldResolution: T | undefined, newResolution: T | undefined): boolean {
|
|
if (oldResolution === newResolution) {
|
|
return true;
|
|
}
|
|
if (!oldResolution || !newResolution) {
|
|
return false;
|
|
}
|
|
const oldResult = getResolutionWithResolvedFileName(oldResolution);
|
|
const newResult = getResolutionWithResolvedFileName(newResolution);
|
|
if (oldResult === newResult) {
|
|
return true;
|
|
}
|
|
if (!oldResult || !newResult) {
|
|
return false;
|
|
}
|
|
return oldResult.resolvedFileName === newResult.resolvedFileName;
|
|
}
|
|
}
|
|
|
|
function resolveTypeReferenceDirectives(typeDirectiveNames: string[] | readonly FileReference[], containingFile: string, redirectedReference?: ResolvedProjectReference, containingFileMode?: SourceFile["impliedNodeFormat"]): (ResolvedTypeReferenceDirective | undefined)[] {
|
|
return resolveNamesWithLocalCache<CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations, ResolvedTypeReferenceDirective>({
|
|
names: typeDirectiveNames,
|
|
containingFile,
|
|
redirectedReference,
|
|
cache: resolvedTypeReferenceDirectives,
|
|
perDirectoryCacheWithRedirects: perDirectoryResolvedTypeReferenceDirectives,
|
|
loader: resolveTypeReferenceDirective,
|
|
getResolutionWithResolvedFileName: getResolvedTypeReferenceDirective,
|
|
shouldRetryResolution: resolution => resolution.resolvedTypeReferenceDirective === undefined,
|
|
containingSourceFileMode: containingFileMode
|
|
});
|
|
}
|
|
|
|
function resolveModuleNames(
|
|
moduleNames: string[],
|
|
containingFile: string,
|
|
reusedNames: string[] | undefined,
|
|
redirectedReference?: ResolvedProjectReference,
|
|
containingSourceFile?: SourceFile,
|
|
resolutionInfo?: ModuleResolutionInfo
|
|
): (ResolvedModuleFull | undefined)[] {
|
|
return resolveNamesWithLocalCache<CachedResolvedModuleWithFailedLookupLocations, ResolvedModuleFull>({
|
|
names: moduleNames,
|
|
containingFile,
|
|
redirectedReference,
|
|
cache: resolvedModuleNames,
|
|
perDirectoryCacheWithRedirects: perDirectoryResolvedModuleNames,
|
|
loader: resolveModuleName,
|
|
getResolutionWithResolvedFileName: getResolvedModule,
|
|
shouldRetryResolution: resolution => !resolution.resolvedModule || !resolutionExtensionIsTSOrJson(resolution.resolvedModule.extension),
|
|
reusedNames,
|
|
resolutionInfo,
|
|
logChanges: logChangesWhenResolvingModule,
|
|
containingSourceFile,
|
|
});
|
|
}
|
|
|
|
function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): CachedResolvedModuleWithFailedLookupLocations | undefined {
|
|
const cache = resolvedModuleNames.get(resolutionHost.toPath(containingFile));
|
|
if (!cache) return undefined;
|
|
return cache.get(moduleName, resolutionMode);
|
|
}
|
|
|
|
function isNodeModulesAtTypesDirectory(dirPath: Path) {
|
|
return endsWith(dirPath, "/node_modules/@types");
|
|
}
|
|
|
|
function getDirectoryToWatchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path): DirectoryOfFailedLookupWatch | undefined {
|
|
if (isInDirectoryPath(rootPath, failedLookupLocationPath)) {
|
|
// Ensure failed look up is normalized path
|
|
failedLookupLocation = isRootedDiskPath(failedLookupLocation) ? normalizePath(failedLookupLocation) : getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory());
|
|
const failedLookupPathSplit = failedLookupLocationPath.split(directorySeparator);
|
|
const failedLookupSplit = failedLookupLocation.split(directorySeparator);
|
|
Debug.assert(failedLookupSplit.length === failedLookupPathSplit.length, `FailedLookup: ${failedLookupLocation} failedLookupLocationPath: ${failedLookupLocationPath}`);
|
|
if (failedLookupPathSplit.length > rootSplitLength + 1) {
|
|
// Instead of watching root, watch directory in root to avoid watching excluded directories not needed for module resolution
|
|
return {
|
|
dir: failedLookupSplit.slice(0, rootSplitLength + 1).join(directorySeparator),
|
|
dirPath: failedLookupPathSplit.slice(0, rootSplitLength + 1).join(directorySeparator) as Path
|
|
};
|
|
}
|
|
else {
|
|
// Always watch root directory non recursively
|
|
return {
|
|
dir: rootDir!,
|
|
dirPath: rootPath,
|
|
nonRecursive: false
|
|
};
|
|
}
|
|
}
|
|
|
|
return getDirectoryToWatchFromFailedLookupLocationDirectory(
|
|
getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())),
|
|
getDirectoryPath(failedLookupLocationPath)
|
|
);
|
|
}
|
|
|
|
function getDirectoryToWatchFromFailedLookupLocationDirectory(dir: string, dirPath: Path): DirectoryOfFailedLookupWatch | undefined {
|
|
// If directory path contains node module, get the most parent node_modules directory for watching
|
|
while (pathContainsNodeModules(dirPath)) {
|
|
dir = getDirectoryPath(dir);
|
|
dirPath = getDirectoryPath(dirPath);
|
|
}
|
|
|
|
// If the directory is node_modules use it to watch, always watch it recursively
|
|
if (isNodeModulesDirectory(dirPath)) {
|
|
return canWatchDirectoryOrFile(getDirectoryPath(dirPath)) ? { dir, dirPath } : undefined;
|
|
}
|
|
|
|
let nonRecursive = true;
|
|
// Use some ancestor of the root directory
|
|
let subDirectoryPath: Path | undefined, subDirectory: string | undefined;
|
|
if (rootPath !== undefined) {
|
|
while (!isInDirectoryPath(dirPath, rootPath)) {
|
|
const parentPath = getDirectoryPath(dirPath);
|
|
if (parentPath === dirPath) {
|
|
break;
|
|
}
|
|
nonRecursive = false;
|
|
subDirectoryPath = dirPath;
|
|
subDirectory = dir;
|
|
dirPath = parentPath;
|
|
dir = getDirectoryPath(dir);
|
|
}
|
|
}
|
|
|
|
return canWatchDirectoryOrFile(dirPath) ? { dir: subDirectory || dir, dirPath: subDirectoryPath || dirPath, nonRecursive } : undefined;
|
|
}
|
|
|
|
function isPathWithDefaultFailedLookupExtension(path: Path) {
|
|
return fileExtensionIsOneOf(path, failedLookupDefaultExtensions);
|
|
}
|
|
|
|
function watchFailedLookupLocationsOfExternalModuleResolutions<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>(
|
|
name: string,
|
|
resolution: T,
|
|
filePath: Path,
|
|
getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>,
|
|
) {
|
|
if (resolution.refCount) {
|
|
resolution.refCount++;
|
|
Debug.assertIsDefined(resolution.files);
|
|
}
|
|
else {
|
|
resolution.refCount = 1;
|
|
Debug.assert(length(resolution.files) === 0); // This resolution shouldnt be referenced by any file yet
|
|
if (isExternalModuleNameRelative(name)) {
|
|
watchFailedLookupLocationOfResolution(resolution);
|
|
}
|
|
else {
|
|
nonRelativeExternalModuleResolutions.add(name, resolution);
|
|
}
|
|
const resolved = getResolutionWithResolvedFileName(resolution);
|
|
if (resolved && resolved.resolvedFileName) {
|
|
resolvedFileToResolution.add(resolutionHost.toPath(resolved.resolvedFileName), resolution);
|
|
}
|
|
}
|
|
(resolution.files || (resolution.files = [])).push(filePath);
|
|
}
|
|
|
|
function watchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) {
|
|
Debug.assert(!!resolution.refCount);
|
|
|
|
const { failedLookupLocations, affectingLocations } = resolution;
|
|
if (!failedLookupLocations.length && !affectingLocations.length) return;
|
|
if (failedLookupLocations.length) resolutionsWithFailedLookups.push(resolution);
|
|
|
|
let setAtRoot = false;
|
|
for (const failedLookupLocation of failedLookupLocations) {
|
|
const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation);
|
|
const toWatch = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath);
|
|
if (toWatch) {
|
|
const { dir, dirPath, nonRecursive } = toWatch;
|
|
// If the failed lookup location path is not one of the supported extensions,
|
|
// store it in the custom path
|
|
if (!isPathWithDefaultFailedLookupExtension(failedLookupLocationPath)) {
|
|
const refCount = customFailedLookupPaths.get(failedLookupLocationPath) || 0;
|
|
customFailedLookupPaths.set(failedLookupLocationPath, refCount + 1);
|
|
}
|
|
if (dirPath === rootPath) {
|
|
Debug.assert(!nonRecursive);
|
|
setAtRoot = true;
|
|
}
|
|
else {
|
|
setDirectoryWatcher(dir, dirPath, nonRecursive);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (setAtRoot) {
|
|
// This is always non recursive
|
|
setDirectoryWatcher(rootDir!, rootPath, /*nonRecursive*/ true); // TODO: GH#18217
|
|
}
|
|
watchAffectingLocationsOfResolution(resolution, !failedLookupLocations.length);
|
|
}
|
|
|
|
function watchAffectingLocationsOfResolution(resolution: ResolutionWithFailedLookupLocations, addToResolutionsWithOnlyAffectingLocations: boolean) {
|
|
Debug.assert(!!resolution.refCount);
|
|
const { affectingLocations } = resolution;
|
|
if (!affectingLocations.length) return;
|
|
if (addToResolutionsWithOnlyAffectingLocations) resolutionsWithOnlyAffectingLocations.push(resolution);
|
|
// Watch package json
|
|
for (const affectingLocation of affectingLocations) {
|
|
createFileWatcherOfAffectingLocation(affectingLocation, /*forResolution*/ true);
|
|
}
|
|
}
|
|
|
|
function createFileWatcherOfAffectingLocation(affectingLocation: string, forResolution: boolean) {
|
|
const fileWatcher = fileWatchesOfAffectingLocations.get(affectingLocation);
|
|
if (fileWatcher) {
|
|
if (forResolution) fileWatcher.resolutions++;
|
|
else fileWatcher.files++;
|
|
return;
|
|
}
|
|
let locationToWatch = affectingLocation;
|
|
if (resolutionHost.realpath) {
|
|
locationToWatch = resolutionHost.realpath(affectingLocation);
|
|
if (affectingLocation !== locationToWatch) {
|
|
const fileWatcher = fileWatchesOfAffectingLocations.get(locationToWatch);
|
|
if (fileWatcher) {
|
|
if (forResolution) fileWatcher.resolutions++;
|
|
else fileWatcher.files++;
|
|
fileWatcher.paths.add(affectingLocation);
|
|
fileWatchesOfAffectingLocations.set(affectingLocation, fileWatcher);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
const paths = new Set<string>();
|
|
paths.add(locationToWatch);
|
|
let actualWatcher = canWatchDirectoryOrFile(resolutionHost.toPath(locationToWatch)) ?
|
|
resolutionHost.watchAffectingFileLocation(locationToWatch, (fileName, eventKind) => {
|
|
cachedDirectoryStructureHost?.addOrDeleteFile(fileName, resolutionHost.toPath(locationToWatch), eventKind);
|
|
const packageJsonMap = moduleResolutionCache.getPackageJsonInfoCache().getInternalMap();
|
|
paths.forEach(path => {
|
|
if (watcher.resolutions) (affectingPathChecks ??= new Set()).add(path);
|
|
if (watcher.files) (affectingPathChecksForFile ??= new Set()).add(path);
|
|
packageJsonMap?.delete(resolutionHost.toPath(path));
|
|
});
|
|
resolutionHost.scheduleInvalidateResolutionsOfFailedLookupLocations();
|
|
}) : noopFileWatcher;
|
|
const watcher: FileWatcherOfAffectingLocation = {
|
|
watcher: actualWatcher !== noopFileWatcher ? {
|
|
close: () => {
|
|
actualWatcher.close();
|
|
// Ensure when watching symlinked package.json, we can close the actual file watcher only once
|
|
actualWatcher = noopFileWatcher;
|
|
}
|
|
} : actualWatcher,
|
|
resolutions: forResolution ? 1 : 0,
|
|
files: forResolution ? 0 : 1,
|
|
paths,
|
|
};
|
|
fileWatchesOfAffectingLocations.set(locationToWatch, watcher);
|
|
if (affectingLocation !== locationToWatch) {
|
|
fileWatchesOfAffectingLocations.set(affectingLocation, watcher);
|
|
paths.add(affectingLocation);
|
|
}
|
|
}
|
|
|
|
function watchFailedLookupLocationOfNonRelativeModuleResolutions(resolutions: ResolutionWithFailedLookupLocations[], name: string) {
|
|
const program = resolutionHost.getCurrentProgram();
|
|
if (!program || !program.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(name)) {
|
|
resolutions.forEach(watchFailedLookupLocationOfResolution);
|
|
}
|
|
else {
|
|
resolutions.forEach(resolution => watchAffectingLocationsOfResolution(resolution, /*addToResolutionWithOnlyAffectingLocations*/ true));
|
|
}
|
|
}
|
|
|
|
function setDirectoryWatcher(dir: string, dirPath: Path, nonRecursive?: boolean) {
|
|
const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath);
|
|
if (dirWatcher) {
|
|
Debug.assert(!!nonRecursive === !!dirWatcher.nonRecursive);
|
|
dirWatcher.refCount++;
|
|
}
|
|
else {
|
|
directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath, nonRecursive), refCount: 1, nonRecursive });
|
|
}
|
|
}
|
|
|
|
function stopWatchFailedLookupLocationOfResolution<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>(
|
|
resolution: T,
|
|
filePath: Path,
|
|
getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>,
|
|
) {
|
|
unorderedRemoveItem(Debug.checkDefined(resolution.files), filePath);
|
|
resolution.refCount!--;
|
|
if (resolution.refCount) {
|
|
return;
|
|
}
|
|
const resolved = getResolutionWithResolvedFileName(resolution);
|
|
if (resolved && resolved.resolvedFileName) {
|
|
resolvedFileToResolution.remove(resolutionHost.toPath(resolved.resolvedFileName), resolution);
|
|
}
|
|
|
|
const { failedLookupLocations, affectingLocations } = resolution;
|
|
if (unorderedRemoveItem(resolutionsWithFailedLookups, resolution)) {
|
|
let removeAtRoot = false;
|
|
for (const failedLookupLocation of failedLookupLocations) {
|
|
const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation);
|
|
const toWatch = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath);
|
|
if (toWatch) {
|
|
const { dirPath } = toWatch;
|
|
const refCount = customFailedLookupPaths.get(failedLookupLocationPath);
|
|
if (refCount) {
|
|
if (refCount === 1) {
|
|
customFailedLookupPaths.delete(failedLookupLocationPath);
|
|
}
|
|
else {
|
|
Debug.assert(refCount > 1);
|
|
customFailedLookupPaths.set(failedLookupLocationPath, refCount - 1);
|
|
}
|
|
}
|
|
|
|
if (dirPath === rootPath) {
|
|
removeAtRoot = true;
|
|
}
|
|
else {
|
|
removeDirectoryWatcher(dirPath);
|
|
}
|
|
}
|
|
}
|
|
if (removeAtRoot) {
|
|
removeDirectoryWatcher(rootPath);
|
|
}
|
|
}
|
|
else if (affectingLocations.length) {
|
|
unorderedRemoveItem(resolutionsWithOnlyAffectingLocations, resolution);
|
|
}
|
|
|
|
for (const affectingLocation of affectingLocations) {
|
|
const watcher = fileWatchesOfAffectingLocations.get(affectingLocation)!;
|
|
watcher.resolutions--;
|
|
}
|
|
}
|
|
|
|
function removeDirectoryWatcher(dirPath: string) {
|
|
const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath)!;
|
|
// Do not close the watcher yet since it might be needed by other failed lookup locations.
|
|
dirWatcher.refCount--;
|
|
}
|
|
|
|
function createDirectoryWatcher(directory: string, dirPath: Path, nonRecursive: boolean | undefined) {
|
|
return resolutionHost.watchDirectoryOfFailedLookupLocation(directory, fileOrDirectory => {
|
|
const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory);
|
|
if (cachedDirectoryStructureHost) {
|
|
// Since the file existence changed, update the sourceFiles cache
|
|
cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
|
|
}
|
|
|
|
scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath);
|
|
}, nonRecursive ? WatchDirectoryFlags.None : WatchDirectoryFlags.Recursive);
|
|
}
|
|
|
|
function removeResolutionsOfFileFromCache<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>(
|
|
cache: ESMap<string, ModeAwareCache<T>>,
|
|
filePath: Path,
|
|
getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>,
|
|
) {
|
|
// Deleted file, stop watching failed lookups for all the resolutions in the file
|
|
const resolutions = cache.get(filePath);
|
|
if (resolutions) {
|
|
resolutions.forEach(resolution => stopWatchFailedLookupLocationOfResolution(resolution, filePath, getResolutionWithResolvedFileName));
|
|
cache.delete(filePath);
|
|
}
|
|
}
|
|
|
|
function removeResolutionsFromProjectReferenceRedirects(filePath: Path) {
|
|
if (!fileExtensionIs(filePath, Extension.Json)) return;
|
|
|
|
const program = resolutionHost.getCurrentProgram();
|
|
if (!program) return;
|
|
|
|
// If this file is input file for the referenced project, get it
|
|
const resolvedProjectReference = program.getResolvedProjectReferenceByPath(filePath);
|
|
if (!resolvedProjectReference) return;
|
|
|
|
// filePath is for the projectReference and the containing file is from this project reference, invalidate the resolution
|
|
resolvedProjectReference.commandLine.fileNames.forEach(f => removeResolutionsOfFile(resolutionHost.toPath(f)));
|
|
}
|
|
|
|
function removeResolutionsOfFile(filePath: Path) {
|
|
removeResolutionsOfFileFromCache(resolvedModuleNames, filePath, getResolvedModule);
|
|
removeResolutionsOfFileFromCache(resolvedTypeReferenceDirectives, filePath, getResolvedTypeReferenceDirective);
|
|
}
|
|
|
|
function invalidateResolutions(resolutions: ResolutionWithFailedLookupLocations[] | undefined, canInvalidate: (resolution: ResolutionWithFailedLookupLocations) => boolean) {
|
|
if (!resolutions) return false;
|
|
let invalidated = false;
|
|
for (const resolution of resolutions) {
|
|
if (resolution.isInvalidated || !canInvalidate(resolution)) continue;
|
|
resolution.isInvalidated = invalidated = true;
|
|
for (const containingFilePath of Debug.checkDefined(resolution.files)) {
|
|
(filesWithInvalidatedResolutions ??= new Set()).add(containingFilePath);
|
|
// When its a file with inferred types resolution, invalidate type reference directive resolution
|
|
hasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames || endsWith(containingFilePath, inferredTypesContainingFile);
|
|
}
|
|
}
|
|
return invalidated;
|
|
}
|
|
|
|
function invalidateResolutionOfFile(filePath: Path) {
|
|
removeResolutionsOfFile(filePath);
|
|
// Resolution is invalidated if the resulting file name is same as the deleted file path
|
|
const prevHasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames;
|
|
if (invalidateResolutions(resolvedFileToResolution.get(filePath), returnTrue) &&
|
|
hasChangedAutomaticTypeDirectiveNames &&
|
|
!prevHasChangedAutomaticTypeDirectiveNames) {
|
|
resolutionHost.onChangedAutomaticTypeDirectiveNames();
|
|
}
|
|
}
|
|
|
|
function setFilesWithInvalidatedNonRelativeUnresolvedImports(filesMap: ReadonlyESMap<Path, readonly string[]>) {
|
|
Debug.assert(filesWithInvalidatedNonRelativeUnresolvedImports === filesMap || filesWithInvalidatedNonRelativeUnresolvedImports === undefined);
|
|
filesWithInvalidatedNonRelativeUnresolvedImports = filesMap;
|
|
}
|
|
|
|
function scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath: Path, isCreatingWatchedDirectory: boolean) {
|
|
if (isCreatingWatchedDirectory) {
|
|
// Watching directory is created
|
|
// Invalidate any resolution has failed lookup in this directory
|
|
(isInDirectoryChecks ||= new Set()).add(fileOrDirectoryPath);
|
|
}
|
|
else {
|
|
// If something to do with folder/file starting with "." in node_modules folder, skip it
|
|
const updatedPath = removeIgnoredPath(fileOrDirectoryPath);
|
|
if (!updatedPath) return false;
|
|
fileOrDirectoryPath = updatedPath;
|
|
|
|
// prevent saving an open file from over-eagerly triggering invalidation
|
|
if (resolutionHost.fileIsOpen(fileOrDirectoryPath)) {
|
|
return false;
|
|
}
|
|
|
|
// Some file or directory in the watching directory is created
|
|
// Return early if it does not have any of the watching extension or not the custom failed lookup path
|
|
const dirOfFileOrDirectory = getDirectoryPath(fileOrDirectoryPath);
|
|
if (isNodeModulesAtTypesDirectory(fileOrDirectoryPath) || isNodeModulesDirectory(fileOrDirectoryPath) ||
|
|
isNodeModulesAtTypesDirectory(dirOfFileOrDirectory) || isNodeModulesDirectory(dirOfFileOrDirectory)) {
|
|
// Invalidate any resolution from this directory
|
|
(failedLookupChecks ||= new Set()).add(fileOrDirectoryPath);
|
|
(startsWithPathChecks ||= new Set()).add(fileOrDirectoryPath);
|
|
}
|
|
else {
|
|
if (!isPathWithDefaultFailedLookupExtension(fileOrDirectoryPath) && !customFailedLookupPaths.has(fileOrDirectoryPath)) {
|
|
return false;
|
|
}
|
|
// Ignore emits from the program
|
|
if (isEmittedFileOfProgram(resolutionHost.getCurrentProgram(), fileOrDirectoryPath)) {
|
|
return false;
|
|
}
|
|
// Resolution need to be invalidated if failed lookup location is same as the file or directory getting created
|
|
(failedLookupChecks ||= new Set()).add(fileOrDirectoryPath);
|
|
|
|
// If the invalidated file is from a node_modules package, invalidate everything else
|
|
// in the package since we might not get notifications for other files in the package.
|
|
// This hardens our logic against unreliable file watchers.
|
|
const packagePath = parseNodeModuleFromPath(fileOrDirectoryPath);
|
|
if (packagePath) (startsWithPathChecks ||= new Set()).add(packagePath as Path);
|
|
}
|
|
}
|
|
resolutionHost.scheduleInvalidateResolutionsOfFailedLookupLocations();
|
|
}
|
|
|
|
function invalidateResolutionsOfFailedLookupLocations() {
|
|
let invalidated = false;
|
|
if (affectingPathChecksForFile) {
|
|
resolutionHost.getCurrentProgram()?.getSourceFiles().forEach(f => {
|
|
if (some(f.packageJsonLocations, location => affectingPathChecksForFile!.has(location))) {
|
|
(filesWithInvalidatedResolutions ??= new Set()).add(f.path);
|
|
invalidated = true;
|
|
}
|
|
});
|
|
affectingPathChecksForFile = undefined;
|
|
}
|
|
|
|
if (!failedLookupChecks && !startsWithPathChecks && !isInDirectoryChecks && !affectingPathChecks) {
|
|
return invalidated;
|
|
}
|
|
|
|
invalidated = invalidateResolutions(resolutionsWithFailedLookups, canInvalidateFailedLookupResolution) || invalidated;
|
|
const packageJsonMap = moduleResolutionCache.getPackageJsonInfoCache().getInternalMap();
|
|
if (packageJsonMap && (failedLookupChecks || startsWithPathChecks || isInDirectoryChecks)) {
|
|
packageJsonMap.forEach((_value, path) => isInvalidatedFailedLookup(path) ? packageJsonMap.delete(path) : undefined);
|
|
}
|
|
failedLookupChecks = undefined;
|
|
startsWithPathChecks = undefined;
|
|
isInDirectoryChecks = undefined;
|
|
invalidated = invalidateResolutions(resolutionsWithOnlyAffectingLocations, canInvalidatedFailedLookupResolutionWithAffectingLocation) || invalidated;
|
|
affectingPathChecks = undefined;
|
|
return invalidated;
|
|
}
|
|
|
|
function canInvalidateFailedLookupResolution(resolution: ResolutionWithFailedLookupLocations) {
|
|
if (canInvalidatedFailedLookupResolutionWithAffectingLocation(resolution)) return true;
|
|
if (!failedLookupChecks && !startsWithPathChecks && !isInDirectoryChecks) return false;
|
|
return resolution.failedLookupLocations.some(location => isInvalidatedFailedLookup(resolutionHost.toPath(location)));
|
|
}
|
|
|
|
function isInvalidatedFailedLookup(locationPath: Path) {
|
|
return failedLookupChecks?.has(locationPath) ||
|
|
firstDefinedIterator(startsWithPathChecks?.keys() || emptyIterator, fileOrDirectoryPath => startsWith(locationPath, fileOrDirectoryPath) ? true : undefined) ||
|
|
firstDefinedIterator(isInDirectoryChecks?.keys() || emptyIterator, fileOrDirectoryPath => isInDirectoryPath(fileOrDirectoryPath, locationPath) ? true : undefined);
|
|
}
|
|
|
|
function canInvalidatedFailedLookupResolutionWithAffectingLocation(resolution: ResolutionWithFailedLookupLocations) {
|
|
return !!affectingPathChecks && resolution.affectingLocations.some(location => affectingPathChecks!.has(location));
|
|
}
|
|
|
|
function closeTypeRootsWatch() {
|
|
clearMap(typeRootsWatches, closeFileWatcher);
|
|
}
|
|
|
|
function getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot: string, typeRootPath: Path): Path | undefined {
|
|
if (isInDirectoryPath(rootPath, typeRootPath)) {
|
|
return rootPath;
|
|
}
|
|
const toWatch = getDirectoryToWatchFromFailedLookupLocationDirectory(typeRoot, typeRootPath);
|
|
return toWatch && directoryWatchesOfFailedLookups.has(toWatch.dirPath) ? toWatch.dirPath : undefined;
|
|
}
|
|
|
|
function createTypeRootsWatch(typeRootPath: Path, typeRoot: string): FileWatcher {
|
|
// Create new watch and recursive info
|
|
return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrDirectory => {
|
|
const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory);
|
|
if (cachedDirectoryStructureHost) {
|
|
// Since the file existence changed, update the sourceFiles cache
|
|
cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
|
|
}
|
|
|
|
// For now just recompile
|
|
// We could potentially store more data here about whether it was/would be really be used or not
|
|
// and with that determine to trigger compilation but for now this is enough
|
|
hasChangedAutomaticTypeDirectiveNames = true;
|
|
resolutionHost.onChangedAutomaticTypeDirectiveNames();
|
|
|
|
// Since directory watchers invoked are flaky, the failed lookup location events might not be triggered
|
|
// So handle to failed lookup locations here as well to ensure we are invalidating resolutions
|
|
const dirPath = getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot, typeRootPath);
|
|
if (dirPath) {
|
|
scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath);
|
|
}
|
|
}, WatchDirectoryFlags.Recursive);
|
|
}
|
|
|
|
/**
|
|
* Watches the types that would get added as part of getAutomaticTypeDirectiveNames
|
|
* To be called when compiler options change
|
|
*/
|
|
function updateTypeRootsWatch() {
|
|
const options = resolutionHost.getCompilationSettings();
|
|
if (options.types) {
|
|
// No need to do any watch since resolution cache is going to handle the failed lookups
|
|
// for the types added by this
|
|
closeTypeRootsWatch();
|
|
return;
|
|
}
|
|
|
|
// we need to assume the directories exist to ensure that we can get all the type root directories that get included
|
|
// But filter directories that are at root level to say directory doesnt exist, so that we arent watching them
|
|
const typeRoots = getEffectiveTypeRoots(options, { directoryExists: directoryExistsForTypeRootWatch, getCurrentDirectory });
|
|
if (typeRoots) {
|
|
mutateMap(
|
|
typeRootsWatches,
|
|
arrayToMap(typeRoots, tr => resolutionHost.toPath(tr)),
|
|
{
|
|
createNewValue: createTypeRootsWatch,
|
|
onDeleteValue: closeFileWatcher
|
|
}
|
|
);
|
|
}
|
|
else {
|
|
closeTypeRootsWatch();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Use this function to return if directory exists to get type roots to watch
|
|
* If we return directory exists then only the paths will be added to type roots
|
|
* Hence return true for all directories except root directories which are filtered from watching
|
|
*/
|
|
function directoryExistsForTypeRootWatch(nodeTypesDirectory: string) {
|
|
const dir = getDirectoryPath(getDirectoryPath(nodeTypesDirectory));
|
|
const dirPath = resolutionHost.toPath(dir);
|
|
return dirPath === rootPath || canWatchDirectoryOrFile(dirPath);
|
|
}
|
|
}
|
|
|
|
function resolutionIsSymlink(resolution: ResolutionWithFailedLookupLocations) {
|
|
return !!(
|
|
(resolution as ResolvedModuleWithFailedLookupLocations).resolvedModule?.originalPath ||
|
|
(resolution as ResolvedTypeReferenceDirectiveWithFailedLookupLocations).resolvedTypeReferenceDirective?.originalPath
|
|
);
|
|
}
|