Handle package.json watch in tsc and tsserver. (#49328)

* refactoring

* Maintain packagejson paths per resolution so we dont have invalidate everything on package json change

* Fix todo
This commit is contained in:
Sheetal Nandi
2022-06-08 10:22:29 -07:00
committed by GitHub
parent b8f6488323
commit 5cedf3e680
85 changed files with 1974 additions and 550 deletions
+135 -42
View File
@@ -32,6 +32,7 @@ namespace ts {
interface ResolutionWithFailedLookupLocations {
readonly failedLookupLocations: string[];
readonly affectingLocations: string[];
isInvalidated?: boolean;
refCount?: number;
// Files that have this resolution using
@@ -54,6 +55,7 @@ namespace ts {
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;
@@ -68,11 +70,18 @@ namespace ts {
onDiscoveredSymlink?(): void;
}
interface DirectoryWatchesOfFailedLookup {
/** watcher for the directory of failed lookup */
interface WatcherWithRefCount {
/** watcher for the lookup */
watcher: FileWatcher;
/** ref count keeping this directory watch alive */
/** ref count keeping this watch alive */
refCount: number;
}
interface FileWatcherOfAffectingLocation extends WatcherWithRefCount {
paths: Set<Path>;
}
interface DirectoryWatchesOfFailedLookup extends WatcherWithRefCount {
/** is the directory watched being non recursive */
nonRecursive?: boolean;
}
@@ -153,12 +162,14 @@ namespace ts {
const nonRelativeExternalModuleResolutions = createMultiMap<ResolutionWithFailedLookupLocations>();
const resolutionsWithFailedLookups: ResolutionWithFailedLookupLocations[] = [];
const resolutionsWithOnlyAffectingLocations: ResolutionWithFailedLookupLocations[] = [];
const resolvedFileToResolution = createMultiMap<ResolutionWithFailedLookupLocations>();
let hasChangedAutomaticTypeDirectiveNames = false;
let failedLookupChecks: Path[] | undefined;
let affectingPathChecks: Set<Path> | undefined;
let failedLookupChecks: Set<Path> | undefined;
let startsWithPathChecks: Set<Path> | undefined;
let isInDirectoryChecks: Path[] | undefined;
let isInDirectoryChecks: Set<Path> | undefined;
const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory!()); // TODO: GH#18217
const cachedDirectoryStructureHost = resolutionHost.getCachedDirectoryStructureHost();
@@ -197,6 +208,7 @@ namespace ts {
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;
@@ -245,6 +257,7 @@ namespace ts {
function clear() {
clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf);
clearMap(fileWatchesOfAffectingLocations, closeFileWatcherOf);
customFailedLookupPaths.clear();
nonRelativeExternalModuleResolutions.clear();
closeTypeRootsWatch();
@@ -252,9 +265,11 @@ namespace ts {
resolvedTypeReferenceDirectives.clear();
resolvedFileToResolution.clear();
resolutionsWithFailedLookups.length = 0;
resolutionsWithOnlyAffectingLocations.length = 0;
failedLookupChecks = undefined;
startsWithPathChecks = undefined;
isInDirectoryChecks = undefined;
affectingPathChecks = undefined;
// perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update
// (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution)
clearPerDirectoryResolutions();
@@ -311,6 +326,15 @@ namespace ts {
watcher.watcher.close();
}
});
fileWatchesOfAffectingLocations.forEach((watcher, path) => {
if (watcher.refCount === 0) {
fileWatchesOfAffectingLocations.delete(path);
watcher.watcher.close();
// Ensure when watching symlinked package.json, we can close the actual file watcher only once
watcher.watcher = noopFileWatcher;
}
});
hasChangedAutomaticTypeDirectiveNames = false;
}
@@ -326,7 +350,7 @@ namespace ts {
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 } = loadModuleFromGlobalCache(
const { resolvedModule, failedLookupLocations, affectingLocations } = loadModuleFromGlobalCache(
Debug.checkDefined(resolutionHost.globalCacheResolutionModuleName)(moduleName),
resolutionHost.projectName,
compilerOptions,
@@ -338,6 +362,7 @@ namespace ts {
// 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;
}
}
@@ -649,9 +674,9 @@ namespace ts {
function watchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) {
Debug.assert(!!resolution.refCount);
const { failedLookupLocations } = resolution;
if (!failedLookupLocations.length) return;
resolutionsWithFailedLookups.push(resolution);
const { failedLookupLocations, affectingLocations } = resolution;
if (!failedLookupLocations.length && !affectingLocations.length) return;
if (failedLookupLocations.length) resolutionsWithFailedLookups.push(resolution);
let setAtRoot = false;
for (const failedLookupLocation of failedLookupLocations) {
@@ -679,6 +704,57 @@ namespace ts {
// 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);
if (addToResolutionsWithOnlyAffectingLocations) resolutionsWithOnlyAffectingLocations.push(resolution);
const { affectingLocations } = resolution;
// Watch package json
for (const affectingLocation of affectingLocations) {
createFileWatcherOfAffectingLocation(affectingLocation);
}
}
function createFileWatcherOfAffectingLocation(affectingLocation: string) {
const path = resolutionHost.toPath(affectingLocation);
const fileWatcher = fileWatchesOfAffectingLocations.get(path);
if (fileWatcher) {
fileWatcher.refCount++;
return;
}
let locationToWatch = affectingLocation;
let locationToWatchPath = path;
if (resolutionHost.realpath) {
locationToWatch = resolutionHost.realpath(affectingLocation);
locationToWatchPath = resolutionHost.toPath(locationToWatch);
if (path !== locationToWatchPath) {
const fileWatcher = fileWatchesOfAffectingLocations.get(locationToWatchPath);
if (fileWatcher) {
fileWatcher.refCount++;
fileWatcher.paths.add(path);
fileWatchesOfAffectingLocations.set(path, fileWatcher);
return;
}
}
}
const paths = new Set<Path>();
paths.add(locationToWatchPath);
const watcher: FileWatcherOfAffectingLocation = {
watcher: resolutionHost.watchAffectingFileLocation(locationToWatch, (fileName, eventKind) => {
cachedDirectoryStructureHost?.addOrDeleteFile(fileName, path, eventKind);
paths.forEach(path => (affectingPathChecks ||= new Set()).add(path));
resolutionHost.scheduleInvalidateResolutionsOfFailedLookupLocations();
}),
refCount: 1,
paths,
};
fileWatchesOfAffectingLocations.set(locationToWatchPath, watcher);
if (path !== locationToWatchPath) {
fileWatchesOfAffectingLocations.set(path, watcher);
paths.add(path);
}
}
function watchFailedLookupLocationOfNonRelativeModuleResolutions(resolutions: ResolutionWithFailedLookupLocations[], name: string) {
@@ -686,6 +762,9 @@ namespace ts {
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) {
@@ -714,39 +793,44 @@ namespace ts {
resolvedFileToResolution.remove(resolutionHost.toPath(resolved.resolvedFileName), resolution);
}
if (!unorderedRemoveItem(resolutionsWithFailedLookups, resolution)) {
// If not watching failed lookups, it wont be there in resolutionsWithFailedLookups
return;
}
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);
}
}
const { failedLookupLocations } = 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);
if (dirPath === rootPath) {
removeAtRoot = true;
}
else {
Debug.assert(refCount > 1);
customFailedLookupPaths.set(failedLookupLocationPath, refCount - 1);
removeDirectoryWatcher(dirPath);
}
}
if (dirPath === rootPath) {
removeAtRoot = true;
}
else {
removeDirectoryWatcher(dirPath);
}
}
if (removeAtRoot) {
removeDirectoryWatcher(rootPath);
}
}
if (removeAtRoot) {
removeDirectoryWatcher(rootPath);
else if (affectingLocations.length) {
unorderedRemoveItem(resolutionsWithOnlyAffectingLocations, resolution);
}
for (const affectingLocation of affectingLocations) {
const watcher = fileWatchesOfAffectingLocations.get(resolutionHost.toPath(affectingLocation))!;
watcher.refCount--;
}
}
@@ -835,7 +919,7 @@ namespace ts {
if (isCreatingWatchedDirectory) {
// Watching directory is created
// Invalidate any resolution has failed lookup in this directory
(isInDirectoryChecks ||= []).push(fileOrDirectoryPath);
(isInDirectoryChecks ||= new Set()).add(fileOrDirectoryPath);
}
else {
// If something to do with folder/file starting with "." in node_modules folder, skip it
@@ -854,7 +938,7 @@ namespace ts {
if (isNodeModulesAtTypesDirectory(fileOrDirectoryPath) || isNodeModulesDirectory(fileOrDirectoryPath) ||
isNodeModulesAtTypesDirectory(dirOfFileOrDirectory) || isNodeModulesDirectory(dirOfFileOrDirectory)) {
// Invalidate any resolution from this directory
(failedLookupChecks ||= []).push(fileOrDirectoryPath);
(failedLookupChecks ||= new Set()).add(fileOrDirectoryPath);
(startsWithPathChecks ||= new Set()).add(fileOrDirectoryPath);
}
else {
@@ -866,7 +950,7 @@ namespace ts {
return false;
}
// Resolution need to be invalidated if failed lookup location is same as the file or directory getting created
(failedLookupChecks ||= []).push(fileOrDirectoryPath);
(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.
@@ -879,26 +963,35 @@ namespace ts {
}
function invalidateResolutionsOfFailedLookupLocations() {
if (!failedLookupChecks && !startsWithPathChecks && !isInDirectoryChecks) {
if (!failedLookupChecks && !startsWithPathChecks && !isInDirectoryChecks && !affectingPathChecks) {
return false;
}
const invalidated = invalidateResolutions(resolutionsWithFailedLookups, canInvalidateFailedLookupResolution);
let invalidated = invalidateResolutions(resolutionsWithFailedLookups, canInvalidateFailedLookupResolution);
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 => {
const locationPath = resolutionHost.toPath(location);
return contains(failedLookupChecks, locationPath) ||
return failedLookupChecks?.has(locationPath) ||
firstDefinedIterator(startsWithPathChecks?.keys() || emptyIterator, fileOrDirectoryPath => startsWith(locationPath, fileOrDirectoryPath) ? true : undefined) ||
isInDirectoryChecks?.some(fileOrDirectoryPath => isInDirectoryPath(fileOrDirectoryPath, locationPath));
firstDefinedIterator(isInDirectoryChecks?.keys() || emptyIterator, fileOrDirectoryPath => isInDirectoryPath(fileOrDirectoryPath, locationPath) ? true : undefined);
});
}
function canInvalidatedFailedLookupResolutionWithAffectingLocation(resolution: ResolutionWithFailedLookupLocations) {
return !!affectingPathChecks && resolution.affectingLocations.some(location =>
affectingPathChecks!.has(resolutionHost.toPath(location)));
}
function closeTypeRootsWatch() {
clearMap(typeRootsWatches, closeFileWatcher);
}