Skip to content

Commit b563978

Browse files
authored
Merge pull request #26135 from Microsoft/watchDirectoryOfNodeModulesScriptInfo
Instead of watching individual script infos, watch the node modules folder for script infos in node modules
2 parents 13deedf + e2edb69 commit b563978

File tree

5 files changed

+134
-22
lines changed

5 files changed

+134
-22
lines changed

src/compiler/sys.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -317,18 +317,22 @@ namespace ts {
317317
const newTime = modifiedTime.getTime();
318318
if (oldTime !== newTime) {
319319
watchedFile.mtime = modifiedTime;
320-
const eventKind = oldTime === 0
321-
? FileWatcherEventKind.Created
322-
: newTime === 0
323-
? FileWatcherEventKind.Deleted
324-
: FileWatcherEventKind.Changed;
325-
watchedFile.callback(watchedFile.fileName, eventKind);
320+
watchedFile.callback(watchedFile.fileName, getFileWatcherEventKind(oldTime, newTime));
326321
return true;
327322
}
328323

329324
return false;
330325
}
331326

327+
/*@internal*/
328+
export function getFileWatcherEventKind(oldTime: number, newTime: number) {
329+
return oldTime === 0
330+
? FileWatcherEventKind.Created
331+
: newTime === 0
332+
? FileWatcherEventKind.Deleted
333+
: FileWatcherEventKind.Changed;
334+
}
335+
332336
/*@internal*/
333337
export interface RecursiveDirectoryWatcherHost {
334338
watchDirectory: HostWatchDirectory;

src/server/editorServices.ts

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,8 @@ namespace ts.server {
291291
ClosedScriptInfo = "Closed Script info",
292292
ConfigFileForInferredRoot = "Config file for the inferred project root",
293293
FailedLookupLocation = "Directory of Failed lookup locations in module resolution",
294-
TypeRoots = "Type root directory"
294+
TypeRoots = "Type root directory",
295+
NodeModulesForClosedScriptInfo = "node_modules for closed script infos in them",
295296
}
296297

297298
const enum ConfigFileWatcherStatus {
@@ -353,10 +354,18 @@ namespace ts.server {
353354
return !!(infoOrFileName as ScriptInfo).containingProjects;
354355
}
355356

357+
interface ScriptInfoInNodeModulesWatcher extends FileWatcher {
358+
refCount: number;
359+
}
360+
356361
function getDetailWatchInfo(watchType: WatchType, project: Project | undefined) {
357362
return `Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`;
358363
}
359364

365+
function isScriptInfoWatchedFromNodeModules(info: ScriptInfo) {
366+
return !info.isScriptOpen() && info.mTime !== undefined;
367+
}
368+
360369
/*@internal*/
361370
export function updateProjectIfDirty(project: Project) {
362371
return project.dirty && project.updateGraph();
@@ -380,6 +389,7 @@ namespace ts.server {
380389
* Container of all known scripts
381390
*/
382391
private readonly filenameToScriptInfo = createMap<ScriptInfo>();
392+
private readonly scriptInfoInNodeModulesWatchers = createMap <ScriptInfoInNodeModulesWatcher>();
383393
/**
384394
* Contains all the deleted script info's version information so that
385395
* it does not reset when creating script info again
@@ -1923,18 +1933,97 @@ namespace ts.server {
19231933
if (!info.isDynamicOrHasMixedContent() &&
19241934
(!this.globalCacheLocationDirectoryPath ||
19251935
!startsWith(info.path, this.globalCacheLocationDirectoryPath))) {
1926-
const { fileName } = info;
1927-
info.fileWatcher = this.watchFactory.watchFilePath(
1928-
this.host,
1929-
fileName,
1930-
(fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path),
1931-
PollingInterval.Medium,
1932-
info.path,
1933-
WatchType.ClosedScriptInfo
1934-
);
1936+
const indexOfNodeModules = info.path.indexOf("/node_modules/");
1937+
if (!this.host.getModifiedTime || indexOfNodeModules === -1) {
1938+
info.fileWatcher = this.watchFactory.watchFilePath(
1939+
this.host,
1940+
info.fileName,
1941+
(fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path),
1942+
PollingInterval.Medium,
1943+
info.path,
1944+
WatchType.ClosedScriptInfo
1945+
);
1946+
}
1947+
else {
1948+
info.mTime = this.getModifiedTime(info);
1949+
info.fileWatcher = this.watchClosedScriptInfoInNodeModules(info.path.substr(0, indexOfNodeModules) as Path);
1950+
}
1951+
}
1952+
}
1953+
1954+
private watchClosedScriptInfoInNodeModules(dir: Path): ScriptInfoInNodeModulesWatcher {
1955+
// Watch only directory
1956+
const existing = this.scriptInfoInNodeModulesWatchers.get(dir);
1957+
if (existing) {
1958+
existing.refCount++;
1959+
return existing;
1960+
}
1961+
1962+
const watchDir = dir + "/node_modules" as Path;
1963+
const watcher = this.watchFactory.watchDirectory(
1964+
this.host,
1965+
watchDir,
1966+
(fileOrDirectory) => {
1967+
const fileOrDirectoryPath = this.toPath(fileOrDirectory);
1968+
// Has extension
1969+
Debug.assert(result.refCount > 0);
1970+
if (watchDir === fileOrDirectoryPath) {
1971+
this.refreshScriptInfosInDirectory(watchDir);
1972+
}
1973+
else {
1974+
const info = this.getScriptInfoForPath(fileOrDirectoryPath);
1975+
if (info) {
1976+
if (isScriptInfoWatchedFromNodeModules(info)) {
1977+
this.refreshScriptInfo(info);
1978+
}
1979+
}
1980+
// Folder
1981+
else if (!hasExtension(fileOrDirectoryPath)) {
1982+
this.refreshScriptInfosInDirectory(fileOrDirectoryPath);
1983+
}
1984+
}
1985+
},
1986+
WatchDirectoryFlags.Recursive,
1987+
WatchType.NodeModulesForClosedScriptInfo
1988+
);
1989+
const result: ScriptInfoInNodeModulesWatcher = {
1990+
close: () => {
1991+
if (result.refCount === 1) {
1992+
watcher.close();
1993+
this.scriptInfoInNodeModulesWatchers.delete(dir);
1994+
}
1995+
else {
1996+
result.refCount--;
1997+
}
1998+
},
1999+
refCount: 1
2000+
};
2001+
this.scriptInfoInNodeModulesWatchers.set(dir, result);
2002+
return result;
2003+
}
2004+
2005+
private getModifiedTime(info: ScriptInfo) {
2006+
return (this.host.getModifiedTime!(info.path) || missingFileModifiedTime).getTime();
2007+
}
2008+
2009+
private refreshScriptInfo(info: ScriptInfo) {
2010+
const mTime = this.getModifiedTime(info);
2011+
if (mTime !== info.mTime) {
2012+
const eventKind = getFileWatcherEventKind(info.mTime!, mTime);
2013+
info.mTime = mTime;
2014+
this.onSourceFileChanged(info.fileName, eventKind, info.path);
19352015
}
19362016
}
19372017

2018+
private refreshScriptInfosInDirectory(dir: Path) {
2019+
dir = dir + directorySeparator as Path;
2020+
this.filenameToScriptInfo.forEach(info => {
2021+
if (isScriptInfoWatchedFromNodeModules(info) && startsWith(info.path, dir)) {
2022+
this.refreshScriptInfo(info);
2023+
}
2024+
});
2025+
}
2026+
19382027
private stopWatchingScriptInfo(info: ScriptInfo) {
19392028
if (info.fileWatcher) {
19402029
info.fileWatcher.close();

src/server/scriptInfo.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@ namespace ts.server {
250250
/*@internal*/
251251
cacheSourceFile: DocumentRegistrySourceFileCache;
252252

253+
/*@internal*/
254+
mTime?: number;
255+
253256
constructor(
254257
private readonly host: ServerHost,
255258
readonly fileName: NormalizedPath,

src/testRunner/unittests/tsserverProjectSystem.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3136,7 +3136,7 @@ namespace ts.projectSystem {
31363136
const project = projectService.configuredProjects.get(configFile.path)!;
31373137
assert.isDefined(project);
31383138
checkProjectActualFiles(project, [file1.path, libFile.path, module1.path, module2.path, configFile.path]);
3139-
checkWatchedFiles(host, [libFile.path, module1.path, module2.path, configFile.path]);
3139+
checkWatchedFiles(host, [libFile.path, configFile.path]);
31403140
checkWatchedDirectories(host, [], /*recursive*/ false);
31413141
const watchedRecursiveDirectories = getTypeRootsFromLocation(root + "/a/b/src");
31423142
watchedRecursiveDirectories.push(`${root}/a/b/src/node_modules`, `${root}/a/b/node_modules`);
@@ -7435,7 +7435,7 @@ namespace ts.projectSystem {
74357435
const projectFilePaths = map(projectFiles, f => f.path);
74367436
checkProjectActualFiles(project, projectFilePaths);
74377437

7438-
const filesWatched = filter(projectFilePaths, p => p !== app.path);
7438+
const filesWatched = filter(projectFilePaths, p => p !== app.path && p.indexOf("/a/b/node_modules") === -1);
74397439
checkWatchedFiles(host, filesWatched);
74407440
checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true);
74417441
checkWatchedDirectories(host, [], /*recursive*/ false);
@@ -8658,10 +8658,21 @@ new C();`
86588658
}
86598659

86608660
function verifyWatchesWithConfigFile(host: TestServerHost, files: File[], openFile: File, extraExpectedDirectories?: ReadonlyArray<string>) {
8661-
checkWatchedFiles(host, mapDefined(files, f => f === openFile ? undefined : f.path));
8661+
const expectedRecursiveDirectories = arrayToSet([projectLocation, `${projectLocation}/${nodeModulesAtTypes}`, ...(extraExpectedDirectories || emptyArray)]);
8662+
checkWatchedFiles(host, mapDefined(files, f => {
8663+
if (f === openFile) {
8664+
return undefined;
8665+
}
8666+
const indexOfNodeModules = f.path.indexOf("/node_modules/");
8667+
if (indexOfNodeModules === -1) {
8668+
return f.path;
8669+
}
8670+
expectedRecursiveDirectories.set(f.path.substr(0, indexOfNodeModules + "/node_modules".length), true);
8671+
return undefined;
8672+
}));
86628673
checkWatchedDirectories(host, [], /*recursive*/ false);
8663-
checkWatchedDirectories(host, [projectLocation, `${projectLocation}/${nodeModulesAtTypes}`, ...(extraExpectedDirectories || emptyArray)], /*recursive*/ true);
8664-
}
8674+
checkWatchedDirectories(host, arrayFrom(expectedRecursiveDirectories.keys()), /*recursive*/ true);
8675+
}
86658676

86668677
describe("from files in same folder", () => {
86678678
function getFiles(fileContent: string) {
@@ -8862,7 +8873,7 @@ new C();`
88628873
verifyTrace(resolutionTrace, expectedTrace);
88638874

88648875
const currentDirectory = getDirectoryPath(file1.path);
8865-
const watchedFiles = mapDefined(files, f => f === file1 ? undefined : f.path);
8876+
const watchedFiles = mapDefined(files, f => f === file1 || f.path.indexOf("/node_modules/") !== -1 ? undefined : f.path);
88668877
forEachAncestorDirectory(currentDirectory, d => {
88678878
watchedFiles.push(combinePaths(d, "tsconfig.json"), combinePaths(d, "jsconfig.json"));
88688879
});

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8382,6 +8382,7 @@ declare namespace ts.server {
83828382
* Container of all known scripts
83838383
*/
83848384
private readonly filenameToScriptInfo;
8385+
private readonly scriptInfoInNodeModulesWatchers;
83858386
/**
83868387
* Contains all the deleted script info's version information so that
83878388
* it does not reset when creating script info again
@@ -8552,6 +8553,10 @@ declare namespace ts.server {
85528553
private createInferredProject;
85538554
getScriptInfo(uncheckedFileName: string): ScriptInfo | undefined;
85548555
private watchClosedScriptInfo;
8556+
private watchClosedScriptInfoInNodeModules;
8557+
private getModifiedTime;
8558+
private refreshScriptInfo;
8559+
private refreshScriptInfosInDirectory;
85558560
private stopWatchingScriptInfo;
85568561
private getOrCreateScriptInfoNotOpenedByClientForNormalizedPath;
85578562
private getOrCreateScriptInfoOpenedByClientForNormalizedPath;

0 commit comments

Comments
 (0)