mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
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:
+135
-42
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user