Skip to content

Commit 2c58d3d

Browse files
committed
Instead of watching individual script infos, watch the node modules folder for script infos in node modules
1 parent a5a26ec commit 2c58d3d

File tree

5 files changed

+135
-23
lines changed

5 files changed

+135
-23
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
@@ -271,7 +271,8 @@ namespace ts.server {
271271
ClosedScriptInfo = "Closed Script info",
272272
ConfigFileForInferredRoot = "Config file for the inferred project root",
273273
FailedLookupLocation = "Directory of Failed lookup locations in module resolution",
274-
TypeRoots = "Type root directory"
274+
TypeRoots = "Type root directory",
275+
NodeModulesForClosedScriptInfo = "node_modules for closed script infos in them",
275276
}
276277

277278
const enum ConfigFileWatcherStatus {
@@ -333,10 +334,18 @@ namespace ts.server {
333334
return !!(infoOrFileName as ScriptInfo).containingProjects;
334335
}
335336

337+
interface ScriptInfoInNodeModulesWatcher extends FileWatcher {
338+
refCount: number;
339+
}
340+
336341
function getDetailWatchInfo(watchType: WatchType, project: Project | undefined) {
337342
return `Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`;
338343
}
339344

345+
function isScriptInfoWatchedFromNodeModules(info: ScriptInfo) {
346+
return !info.isScriptOpen() && info.mTime !== undefined;
347+
}
348+
340349
function updateProjectIfDirty(project: Project) {
341350
return project.dirty && project.updateGraph();
342351
}
@@ -353,6 +362,7 @@ namespace ts.server {
353362
* Container of all known scripts
354363
*/
355364
private readonly filenameToScriptInfo = createMap<ScriptInfo>();
365+
private readonly scriptInfoInNodeModulesWatchers = createMap <ScriptInfoInNodeModulesWatcher>();
356366
/**
357367
* Contains all the deleted script info's version information so that
358368
* it does not reset when creating script info again
@@ -1850,18 +1860,97 @@ namespace ts.server {
18501860
if (!info.isDynamicOrHasMixedContent() &&
18511861
(!this.globalCacheLocationDirectoryPath ||
18521862
!startsWith(info.path, this.globalCacheLocationDirectoryPath))) {
1853-
const { fileName } = info;
1854-
info.fileWatcher = this.watchFactory.watchFilePath(
1855-
this.host,
1856-
fileName,
1857-
(fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path),
1858-
PollingInterval.Medium,
1859-
info.path,
1860-
WatchType.ClosedScriptInfo
1861-
);
1863+
const indexOfNodeModules = info.path.indexOf("/node_modules/");
1864+
if (!this.host.getModifiedTime || indexOfNodeModules === -1) {
1865+
info.fileWatcher = this.watchFactory.watchFilePath(
1866+
this.host,
1867+
info.fileName,
1868+
(fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path),
1869+
PollingInterval.Medium,
1870+
info.path,
1871+
WatchType.ClosedScriptInfo
1872+
);
1873+
}
1874+
else {
1875+
info.mTime = this.getModifiedTime(info);
1876+
info.fileWatcher = this.watchClosedScriptInfoInNodeModules(info.path.substr(0, indexOfNodeModules) as Path);
1877+
}
1878+
}
1879+
}
1880+
1881+
private watchClosedScriptInfoInNodeModules(dir: Path): ScriptInfoInNodeModulesWatcher {
1882+
// Watch only directory
1883+
const existing = this.scriptInfoInNodeModulesWatchers.get(dir);
1884+
if (existing) {
1885+
existing.refCount++;
1886+
return existing;
1887+
}
1888+
1889+
const watchDir = dir + "/node_modules" as Path;
1890+
const watcher = this.watchFactory.watchDirectory(
1891+
this.host,
1892+
watchDir,
1893+
(fileOrDirectory) => {
1894+
const fileOrDirectoryPath = this.toPath(fileOrDirectory);
1895+
// Has extension
1896+
Debug.assert(result.refCount > 0);
1897+
if (watchDir === fileOrDirectoryPath) {
1898+
this.refreshScriptInfosInDirectory(watchDir);
1899+
}
1900+
else {
1901+
const info = this.getScriptInfoForPath(fileOrDirectoryPath);
1902+
if (info) {
1903+
if (isScriptInfoWatchedFromNodeModules(info)) {
1904+
this.refreshScriptInfo(info);
1905+
}
1906+
}
1907+
// Folder
1908+
else if (!hasExtension(fileOrDirectoryPath)) {
1909+
this.refreshScriptInfosInDirectory(fileOrDirectoryPath);
1910+
}
1911+
}
1912+
},
1913+
WatchDirectoryFlags.Recursive,
1914+
WatchType.NodeModulesForClosedScriptInfo
1915+
);
1916+
const result: ScriptInfoInNodeModulesWatcher = {
1917+
close: () => {
1918+
if (result.refCount === 1) {
1919+
watcher.close();
1920+
this.scriptInfoInNodeModulesWatchers.delete(dir);
1921+
}
1922+
else {
1923+
result.refCount--;
1924+
}
1925+
},
1926+
refCount: 1
1927+
};
1928+
this.scriptInfoInNodeModulesWatchers.set(dir, result);
1929+
return result;
1930+
}
1931+
1932+
private getModifiedTime(info: ScriptInfo) {
1933+
return (this.host.getModifiedTime!(info.path) || missingFileModifiedTime).getTime();
1934+
}
1935+
1936+
private refreshScriptInfo(info: ScriptInfo) {
1937+
const mTime = this.getModifiedTime(info);
1938+
if (mTime !== info.mTime) {
1939+
const eventKind = getFileWatcherEventKind(info.mTime!, mTime);
1940+
info.mTime = mTime;
1941+
this.onSourceFileChanged(info.fileName, eventKind, info.path);
18621942
}
18631943
}
18641944

1945+
private refreshScriptInfosInDirectory(dir: Path) {
1946+
dir = dir + directorySeparator as Path;
1947+
this.filenameToScriptInfo.forEach(info => {
1948+
if (isScriptInfoWatchedFromNodeModules(info) && startsWith(info.path, dir)) {
1949+
this.refreshScriptInfo(info);
1950+
}
1951+
});
1952+
}
1953+
18651954
private stopWatchingScriptInfo(info: ScriptInfo) {
18661955
if (info.fileWatcher) {
18671956
info.fileWatcher.close();

src/server/scriptInfo.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ namespace ts.server {
236236
/*@internal*/
237237
cacheSourceFile: DocumentRegistrySourceFileCache;
238238

239+
/*@internal*/
240+
mTime?: number;
241+
239242
constructor(
240243
private readonly host: ServerHost,
241244
readonly fileName: NormalizedPath,

src/testRunner/unittests/tsserverProjectSystem.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3108,7 +3108,7 @@ namespace ts.projectSystem {
31083108
const project = projectService.configuredProjects.get(configFile.path)!;
31093109
assert.isDefined(project);
31103110
checkProjectActualFiles(project, [file1.path, libFile.path, module1.path, module2.path, configFile.path]);
3111-
checkWatchedFiles(host, [libFile.path, module1.path, module2.path, configFile.path]);
3111+
checkWatchedFiles(host, [libFile.path, configFile.path]);
31123112
checkWatchedDirectories(host, [], /*recursive*/ false);
31133113
const watchedRecursiveDirectories = getTypeRootsFromLocation(root + "/a/b/src");
31143114
watchedRecursiveDirectories.push(`${root}/a/b/src/node_modules`, `${root}/a/b/node_modules`);
@@ -7201,7 +7201,7 @@ namespace ts.projectSystem {
72017201
const projectFilePaths = map(projectFiles, f => f.path);
72027202
checkProjectActualFiles(project, projectFilePaths);
72037203

7204-
const filesWatched = filter(projectFilePaths, p => p !== app.path);
7204+
const filesWatched = filter(projectFilePaths, p => p !== app.path && p.indexOf("/a/b/node_modules") === -1);
72057205
checkWatchedFiles(host, filesWatched);
72067206
checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true);
72077207
checkWatchedDirectories(host, [], /*recursive*/ false);
@@ -8424,10 +8424,21 @@ new C();`
84248424
}
84258425

84268426
function verifyWatchesWithConfigFile(host: TestServerHost, files: File[], openFile: File, extraExpectedDirectories?: ReadonlyArray<string>) {
8427-
checkWatchedFiles(host, mapDefined(files, f => f === openFile ? undefined : f.path));
8427+
const expectedRecursiveDirectories = arrayToSet([projectLocation, `${projectLocation}/${nodeModulesAtTypes}`, ...(extraExpectedDirectories || emptyArray)]);
8428+
checkWatchedFiles(host, mapDefined(files, f => {
8429+
if (f === openFile) {
8430+
return undefined;
8431+
}
8432+
const indexOfNodeModules = f.path.indexOf("/node_modules/");
8433+
if (indexOfNodeModules === -1) {
8434+
return f.path;
8435+
}
8436+
expectedRecursiveDirectories.set(f.path.substr(0, indexOfNodeModules + "/node_modules".length), true);
8437+
return undefined;
8438+
}));
84288439
checkWatchedDirectories(host, [], /*recursive*/ false);
8429-
checkWatchedDirectories(host, [projectLocation, `${projectLocation}/${nodeModulesAtTypes}`, ...(extraExpectedDirectories || emptyArray)], /*recursive*/ true);
8430-
}
8440+
checkWatchedDirectories(host, arrayFrom(expectedRecursiveDirectories.keys()), /*recursive*/ true);
8441+
}
84318442

84328443
describe("from files in same folder", () => {
84338444
function getFiles(fileContent: string) {
@@ -8628,7 +8639,7 @@ new C();`
86288639
verifyTrace(resolutionTrace, expectedTrace);
86298640

86308641
const currentDirectory = getDirectoryPath(file1.path);
8631-
const watchedFiles = mapDefined(files, f => f === file1 ? undefined : f.path);
8642+
const watchedFiles = mapDefined(files, f => f === file1 || f.path.indexOf("/node_modules/") !== -1 ? undefined : f.path);
86328643
forEachAncestorDirectory(currentDirectory, d => {
86338644
watchedFiles.push(combinePaths(d, "tsconfig.json"), combinePaths(d, "jsconfig.json"));
86348645
});
@@ -9535,7 +9546,7 @@ export function Test2() {
95359546
});
95369547
});
95379548

9538-
describe("duplicate packages", () => {
9549+
describe("tsserverProjectSystem duplicate packages", () => {
95399550
// Tests that 'moduleSpecifiers.ts' will import from the redirecting file, and not from the file it redirects to, if that can provide a global module specifier.
95409551
it("works with import fixes", () => {
95419552
const packageContent = "export const foo: number;";

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8506,6 +8506,7 @@ declare namespace ts.server {
85068506
* Container of all known scripts
85078507
*/
85088508
private readonly filenameToScriptInfo;
8509+
private readonly scriptInfoInNodeModulesWatchers;
85098510
/**
85108511
* Contains all the deleted script info's version information so that
85118512
* it does not reset when creating script info again
@@ -8676,6 +8677,10 @@ declare namespace ts.server {
86768677
private createInferredProject;
86778678
getScriptInfo(uncheckedFileName: string): ScriptInfo | undefined;
86788679
private watchClosedScriptInfo;
8680+
private watchClosedScriptInfoInNodeModules;
8681+
private getModifiedTime;
8682+
private refreshScriptInfo;
8683+
private refreshScriptInfosInDirectory;
86798684
private stopWatchingScriptInfo;
86808685
private getOrCreateScriptInfoNotOpenedByClientForNormalizedPath;
86818686
private getOrCreateScriptInfoOpenedByClientForNormalizedPath;

0 commit comments

Comments
 (0)