Skip to content

Commit b006768

Browse files
authored
When watching failed lookups, watch packageDir if its a symlink otherwise the path we use to watch (#58139)
1 parent 6092c2d commit b006768

File tree

111 files changed

+95512
-79252
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+95512
-79252
lines changed

src/compiler/resolutionCache.ts

Lines changed: 168 additions & 12 deletions
Large diffs are not rendered by default.

src/compiler/sys.ts

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
getDirectoryPath,
1919
getFallbackOptions,
2020
getNormalizedAbsolutePath,
21+
getRelativePathFromDirectory,
2122
getRelativePathToDirectoryOrUrl,
2223
getRootLength,
2324
getStringComparer,
@@ -607,15 +608,17 @@ function createDirectoryWatcherSupportingRecursive({
607608
watcher: FileWatcher;
608609
childWatches: ChildWatches;
609610
refCount: number;
611+
targetWatcher: ChildDirectoryWatcher | undefined;
612+
links: Set<string> | undefined;
610613
}
611614

612-
const cache = new Map<string, HostDirectoryWatcher>();
615+
const cache = new Map<Path, HostDirectoryWatcher>();
613616
const callbackCache = createMultiMap<Path, { dirName: string; callback: DirectoryWatcherCallback; }>();
614617
const cacheToUpdateChildWatches = new Map<Path, { dirName: string; options: WatchOptions | undefined; fileNames: string[]; }>();
615618
let timerToUpdateChildWatches: any;
616619

617620
const filePathComparer = getStringComparer(!useCaseSensitiveFileNames);
618-
const toCanonicalFilePath = createGetCanonicalFileName(useCaseSensitiveFileNames);
621+
const toCanonicalFilePath = createGetCanonicalFileName(useCaseSensitiveFileNames) as (fileName: string) => Path;
619622

620623
return (dirName, callback, recursive, options) =>
621624
recursive ?
@@ -625,8 +628,13 @@ function createDirectoryWatcherSupportingRecursive({
625628
/**
626629
* Create the directory watcher for the dirPath.
627630
*/
628-
function createDirectoryWatcher(dirName: string, options: WatchOptions | undefined, callback?: DirectoryWatcherCallback): ChildDirectoryWatcher {
629-
const dirPath = toCanonicalFilePath(dirName) as Path;
631+
function createDirectoryWatcher(
632+
dirName: string,
633+
options: WatchOptions | undefined,
634+
callback?: DirectoryWatcherCallback,
635+
link?: string,
636+
): ChildDirectoryWatcher {
637+
const dirPath = toCanonicalFilePath(dirName);
630638
let directoryWatcher = cache.get(dirPath);
631639
if (directoryWatcher) {
632640
directoryWatcher.refCount++;
@@ -640,7 +648,7 @@ function createDirectoryWatcherSupportingRecursive({
640648

641649
if (options?.synchronousWatchDirectory) {
642650
// Call the actual callback
643-
invokeCallbacks(dirPath, fileName);
651+
if (!cache.get(dirPath)?.targetWatcher) invokeCallbacks(dirName, dirPath, fileName);
644652

645653
// Iterate through existing children and update the watches if needed
646654
updateChildWatches(dirName, dirPath, options);
@@ -654,11 +662,15 @@ function createDirectoryWatcherSupportingRecursive({
654662
),
655663
refCount: 1,
656664
childWatches: emptyArray,
665+
targetWatcher: undefined,
666+
links: undefined,
657667
};
658668
cache.set(dirPath, directoryWatcher);
659669
updateChildWatches(dirName, dirPath, options);
660670
}
661671

672+
if (link) (directoryWatcher.links ??= new Set()).add(link);
673+
662674
const callbackToAdd = callback && { dirName, callback };
663675
if (callbackToAdd) {
664676
callbackCache.add(dirPath, callbackToAdd);
@@ -669,21 +681,24 @@ function createDirectoryWatcherSupportingRecursive({
669681
close: () => {
670682
const directoryWatcher = Debug.checkDefined(cache.get(dirPath));
671683
if (callbackToAdd) callbackCache.remove(dirPath, callbackToAdd);
684+
if (link) directoryWatcher.links?.delete(link);
672685
directoryWatcher.refCount--;
673686

674687
if (directoryWatcher.refCount) return;
675688

676689
cache.delete(dirPath);
690+
directoryWatcher.links = undefined;
677691
closeFileWatcherOf(directoryWatcher);
692+
closeTargetWatcher(directoryWatcher);
678693
directoryWatcher.childWatches.forEach(closeFileWatcher);
679694
},
680695
};
681696
}
682697

683698
type InvokeMap = Map<Path, string[] | true>;
684-
function invokeCallbacks(dirPath: Path, fileName: string): void;
685-
function invokeCallbacks(dirPath: Path, invokeMap: InvokeMap, fileNames: string[] | undefined): void;
686-
function invokeCallbacks(dirPath: Path, fileNameOrInvokeMap: string | InvokeMap, fileNames?: string[]) {
699+
function invokeCallbacks(dirName: string, dirPath: Path, fileName: string): void;
700+
function invokeCallbacks(dirName: string, dirPath: Path, invokeMap: InvokeMap, fileNames: string[] | undefined): void;
701+
function invokeCallbacks(dirName: string, dirPath: Path, fileNameOrInvokeMap: string | InvokeMap, fileNames?: string[]) {
687702
let fileName: string | undefined;
688703
let invokeMap: InvokeMap | undefined;
689704
if (isString(fileNameOrInvokeMap)) {
@@ -715,6 +730,15 @@ function createDirectoryWatcherSupportingRecursive({
715730
}
716731
}
717732
});
733+
cache.get(dirPath)?.links?.forEach(link => {
734+
const toPathInLink = (fileName: string) => combinePaths(link, getRelativePathFromDirectory(dirName, fileName, toCanonicalFilePath));
735+
if (invokeMap) {
736+
invokeCallbacks(link, toCanonicalFilePath(link), invokeMap, fileNames?.map(toPathInLink));
737+
}
738+
else {
739+
invokeCallbacks(link, toCanonicalFilePath(link), toPathInLink(fileName!));
740+
}
741+
});
718742
}
719743

720744
function nonSyncUpdateChildWatches(dirName: string, dirPath: Path, fileName: string, options: WatchOptions | undefined) {
@@ -727,7 +751,8 @@ function createDirectoryWatcherSupportingRecursive({
727751
}
728752

729753
// Call the actual callbacks and remove child watches
730-
invokeCallbacks(dirPath, fileName);
754+
invokeCallbacks(dirName, dirPath, fileName);
755+
closeTargetWatcher(parentWatcher);
731756
removeChildWatches(parentWatcher);
732757
}
733758

@@ -760,7 +785,7 @@ function createDirectoryWatcherSupportingRecursive({
760785
// Because the child refresh is fresh, we would need to invalidate whole root directory being watched
761786
// to ensure that all the changes are reflected at this time
762787
const hasChanges = updateChildWatches(dirName, dirPath, options);
763-
invokeCallbacks(dirPath, invokeMap, hasChanges ? undefined : fileNames);
788+
if (!cache.get(dirPath)?.targetWatcher) invokeCallbacks(dirName, dirPath, invokeMap, hasChanges ? undefined : fileNames);
764789
}
765790

766791
sysLog(`sysLog:: invokingWatchers:: Elapsed:: ${timestamp() - start}ms:: ${cacheToUpdateChildWatches.size}`);
@@ -792,24 +817,46 @@ function createDirectoryWatcherSupportingRecursive({
792817
}
793818
}
794819

820+
function closeTargetWatcher(watcher: HostDirectoryWatcher | undefined) {
821+
if (watcher?.targetWatcher) {
822+
watcher.targetWatcher.close();
823+
watcher.targetWatcher = undefined;
824+
}
825+
}
826+
795827
function updateChildWatches(parentDir: string, parentDirPath: Path, options: WatchOptions | undefined) {
796828
// Iterate through existing children and update the watches if needed
797829
const parentWatcher = cache.get(parentDirPath);
798830
if (!parentWatcher) return false;
831+
const target = normalizePath(realpath(parentDir));
832+
let hasChanges;
799833
let newChildWatches: ChildDirectoryWatcher[] | undefined;
800-
const hasChanges = enumerateInsertsAndDeletes<string, ChildDirectoryWatcher>(
801-
fileSystemEntryExists(parentDir, FileSystemEntryKind.Directory) ? mapDefined(getAccessibleSortedChildDirectories(parentDir), child => {
802-
const childFullName = getNormalizedAbsolutePath(child, parentDir);
803-
// Filter our the symbolic link directories since those arent included in recursive watch
804-
// which is same behaviour when recursive: true is passed to fs.watch
805-
return !isIgnoredPath(childFullName, options) && filePathComparer(childFullName, normalizePath(realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined;
806-
}) : emptyArray,
807-
parentWatcher.childWatches,
808-
(child, childWatcher) => filePathComparer(child, childWatcher.dirName),
809-
createAndAddChildDirectoryWatcher,
810-
closeFileWatcher,
811-
addChildDirectoryWatcher,
812-
);
834+
if (filePathComparer(target, parentDir) === Comparison.EqualTo) {
835+
// if (parentWatcher.target) closeFileWatcher
836+
hasChanges = enumerateInsertsAndDeletes<string, ChildDirectoryWatcher>(
837+
fileSystemEntryExists(parentDir, FileSystemEntryKind.Directory) ? mapDefined(getAccessibleSortedChildDirectories(parentDir), child => {
838+
const childFullName = getNormalizedAbsolutePath(child, parentDir);
839+
// Filter our the symbolic link directories since those arent included in recursive watch
840+
// which is same behaviour when recursive: true is passed to fs.watch
841+
return !isIgnoredPath(childFullName, options) && filePathComparer(childFullName, normalizePath(realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined;
842+
}) : emptyArray,
843+
parentWatcher.childWatches,
844+
(child, childWatcher) => filePathComparer(child, childWatcher.dirName),
845+
createAndAddChildDirectoryWatcher,
846+
closeFileWatcher,
847+
addChildDirectoryWatcher,
848+
);
849+
}
850+
else if (parentWatcher.targetWatcher && filePathComparer(target, parentWatcher.targetWatcher.dirName) === Comparison.EqualTo) {
851+
hasChanges = false;
852+
Debug.assert(parentWatcher.childWatches === emptyArray);
853+
}
854+
else {
855+
closeTargetWatcher(parentWatcher);
856+
parentWatcher.targetWatcher = createDirectoryWatcher(target, options, /*callback*/ undefined, parentDir);
857+
parentWatcher.childWatches.forEach(closeFileWatcher);
858+
hasChanges = true;
859+
}
813860
parentWatcher.childWatches = newChildWatches || emptyArray;
814861
return hasChanges;
815862

src/harness/incrementalUtils.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@ export function verifyResolutionCache(
271271
verifyResolutionSet(expected.resolutionsWithOnlyAffectingLocations, actual.resolutionsWithOnlyAffectingLocations, `resolutionsWithOnlyAffectingLocations`);
272272
verifyDirectoryWatchesOfFailedLookups(expected.directoryWatchesOfFailedLookups, actual.directoryWatchesOfFailedLookups);
273273
verifyFileWatchesOfAffectingLocations(expected.fileWatchesOfAffectingLocations, actual.fileWatchesOfAffectingLocations);
274+
verifyPackageDirWatchers(expected.packageDirWatchers, actual.packageDirWatchers);
275+
verifyDirPathToSymlinkPackageRefCount(expected.dirPathToSymlinkPackageRefCount, actual.dirPathToSymlinkPackageRefCount);
274276

275277
// Stop watching resolutions to verify everything gets closed.
276278
expected.startCachingPerDirectoryResolution();
@@ -368,11 +370,17 @@ export function verifyResolutionCache(
368370
}
369371

370372
function verifyDirectoryWatchesOfFailedLookups(expected: Map<string, ts.DirectoryWatchesOfFailedLookup>, actual: Map<string, ts.DirectoryWatchesOfFailedLookup>) {
371-
verifyMap(expected, actual, (expected, actual, caption) => {
372-
ts.Debug.assert(expected?.refCount === actual?.refCount, `${projectName}:: ${caption}:: refCount`);
373-
ts.Debug.assert(!!expected?.refCount, `${projectName}:: ${caption}:: expected refCount to be non zero`);
374-
ts.Debug.assert(expected?.nonRecursive === actual?.nonRecursive, `${projectName}:: ${caption}:: nonRecursive`);
375-
}, "directoryWatchesOfFailedLookups");
373+
verifyMap(expected, actual, verifyDirectoryWatchesOfFailedLookup, "directoryWatchesOfFailedLookups");
374+
}
375+
376+
function verifyDirectoryWatchesOfFailedLookup(
377+
expected: ts.DirectoryWatchesOfFailedLookup | undefined,
378+
actual: ts.DirectoryWatchesOfFailedLookup | undefined,
379+
caption: string,
380+
) {
381+
ts.Debug.assert(expected?.refCount === actual?.refCount, `${projectName}:: ${caption}:: refCount`);
382+
ts.Debug.assert(!!expected?.refCount, `${projectName}:: ${caption}:: expected refCount to be non zero`);
383+
ts.Debug.assert(expected?.nonRecursive === actual?.nonRecursive, `${projectName}:: ${caption}:: nonRecursive`);
376384
}
377385

378386
function verifyFileWatchesOfAffectingLocations(
@@ -391,6 +399,40 @@ export function verifyResolutionCache(
391399
ts.Debug.assert(expected?.files === actual?.files, `${projectName}:: ${caption}:: files`);
392400
verifySet(expected?.symlinks, actual?.symlinks, `${projectName}:: ${caption}:: symlinks`);
393401
}
402+
403+
function verifyPackageDirWatchers(
404+
expected: Map<ts.Path, ts.PackageDirWatcher>,
405+
actual: Map<ts.Path, ts.PackageDirWatcher>,
406+
) {
407+
verifyMap(expected, actual, verifyPackageDirWatcher, "packageDirWatchers");
408+
}
409+
410+
function verifyPackageDirWatcher(
411+
expected: ts.PackageDirWatcher | undefined,
412+
actual: ts.PackageDirWatcher | undefined,
413+
caption: string,
414+
) {
415+
ts.Debug.assert(expected?.isSymlink === actual?.isSymlink, `${projectName}:: ${caption}:: isSymlink`);
416+
verifyMap(expected?.dirPathToWatcher, actual?.dirPathToWatcher, verfiyDirPathToWatcherOfPackageDirWatcher, `${projectName}:: ${caption}:: dirPathToWatcher`);
417+
}
418+
419+
function verfiyDirPathToWatcherOfPackageDirWatcher(
420+
expected: ts.DirPathToWatcherOfPackageDirWatcher | undefined,
421+
actual: ts.DirPathToWatcherOfPackageDirWatcher | undefined,
422+
caption: string,
423+
) {
424+
ts.Debug.assert(expected?.refCount === actual?.refCount, `${projectName}:: ${caption}:: refCount`);
425+
verifyDirectoryWatchesOfFailedLookup(expected?.watcher, actual?.watcher, `${projectName}:: ${caption}:: directoryWatchesOfFailedLookup`);
426+
}
427+
428+
function verifyDirPathToSymlinkPackageRefCount(
429+
expected: Map<ts.Path, number>,
430+
actual: Map<ts.Path, number>,
431+
) {
432+
verifyMap(expected, actual, (expected, actual, caption) => {
433+
ts.Debug.assert(expected === actual, `${projectName}:: ${caption}`);
434+
}, "dirPathToSymlinkPackageRefCount");
435+
}
394436
}
395437

396438
function verifyMap<Key extends string, Expected, Actual>(

src/testRunner/tests.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import "./unittests/tscWatch/projectsWithReferences";
134134
import "./unittests/tscWatch/resolutionCache";
135135
import "./unittests/tscWatch/resolveJsonModuleWithIncremental";
136136
import "./unittests/tscWatch/sourceOfProjectReferenceRedirect";
137+
import "./unittests/tscWatch/symlinks";
137138
import "./unittests/tscWatch/watchApi";
138139
import "./unittests/tscWatch/watchEnvironment";
139140
import "./unittests/tsserver/applyChangesToOpenFiles";

src/testRunner/unittests/canWatch.ts

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -55,34 +55,44 @@ describe("unittests:: canWatch::", () => {
5555
scenario: string,
5656
forPath: "node_modules" | "node_modules/@types" | "",
5757
) {
58-
baselineCanWatch(
59-
scenario,
60-
() => `Determines whether to watch given failed lookup location (file that didnt exist) when resolving module.\r\nIt also determines the directory to watch and whether to watch it recursively or not.`,
61-
(paths, longestPathLength, baseline) => {
62-
const recursive = "Recursive";
63-
const maxLength = longestPathLength + ts.combinePaths(forPath, "dir/subdir/somefile.d.ts").length;
64-
const maxLengths = [maxLength, maxLength, recursive.length] as const;
65-
baselineCanWatchForRoot(paths, baseline, (rootPathCompoments, root) => {
66-
pushHeader(baseline, ["Location", "getDirectoryToWatchFailedLookupLocation", recursive], maxLengths);
67-
paths.forEach(path => {
68-
baselineGetDirectoryToWatchFailedLookupLocation(combinePaths(path, forPath, "somefile.d.ts"), root, rootPathCompoments, maxLengths);
69-
baselineGetDirectoryToWatchFailedLookupLocation(combinePaths(path, forPath, "dir/somefile.d.ts"), root, rootPathCompoments, maxLengths);
70-
baselineGetDirectoryToWatchFailedLookupLocation(combinePaths(path, forPath, "dir/subdir/somefile.d.ts"), root, rootPathCompoments, maxLengths);
58+
["file", "dir", "subDir"].forEach(type => {
59+
baselineCanWatch(
60+
`${scenario}In${type}`,
61+
() => `Determines whether to watch given failed lookup location (file that didnt exist) when resolving module.\r\nIt also determines the directory to watch and whether to watch it recursively or not.`,
62+
(paths, longestPathLength, baseline) => {
63+
const recursive = "Recursive";
64+
const maxLength = longestPathLength + ts.combinePaths(forPath, "dir/subdir/somefile.d.ts").length;
65+
const maxLengths = [maxLength, maxLength, recursive.length, maxLength] as const;
66+
baselineCanWatchForRoot(paths, baseline, (rootPathCompoments, root) => {
67+
pushHeader(baseline, ["Location", "getDirectoryToWatchFailedLookupLocation", recursive, "Location if not symlink"], maxLengths);
68+
paths.forEach(path => {
69+
let subPath;
70+
switch (type) {
71+
case "file":
72+
subPath = "somefile.d.ts";
73+
break;
74+
case "dir":
75+
subPath = "dir/somefile.d.ts";
76+
break;
77+
case "subDir":
78+
subPath = "dir/subdir/somefile.d.ts";
79+
break;
80+
}
81+
const testPath = combinePaths(path, forPath, subPath);
82+
const result = ts.getDirectoryToWatchFailedLookupLocation(
83+
testPath,
84+
testPath,
85+
root,
86+
root,
87+
rootPathCompoments,
88+
ts.returnUndefined,
89+
);
90+
pushRow(baseline, [testPath, result ? result.packageDir ?? result.dir : "", result ? `${!result.nonRecursive}` : "", result?.packageDir ? result.dir : ""], maxLengths);
91+
});
7192
});
72-
});
73-
function baselineGetDirectoryToWatchFailedLookupLocation(path: ts.Path, root: ts.Path, rootPathCompoments: Readonly<ts.PathPathComponents>, maxLengths: readonly number[]) {
74-
const result = ts.getDirectoryToWatchFailedLookupLocation(
75-
path,
76-
path,
77-
root,
78-
root,
79-
rootPathCompoments,
80-
ts.returnUndefined,
81-
);
82-
pushRow(baseline, [path, result ? result.dir : "", result ? `${!result.nonRecursive}` : ""], maxLengths);
83-
}
84-
},
85-
);
93+
},
94+
);
95+
});
8696
}
8797

8898
baselineCanWatch(

0 commit comments

Comments
 (0)