Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/compiler/sys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,18 +317,22 @@ namespace ts {
const newTime = modifiedTime.getTime();
if (oldTime !== newTime) {
watchedFile.mtime = modifiedTime;
const eventKind = oldTime === 0
? FileWatcherEventKind.Created
: newTime === 0
? FileWatcherEventKind.Deleted
: FileWatcherEventKind.Changed;
watchedFile.callback(watchedFile.fileName, eventKind);
watchedFile.callback(watchedFile.fileName, getFileWatcherEventKind(oldTime, newTime));
return true;
}

return false;
}

/*@internal*/
export function getFileWatcherEventKind(oldTime: number, newTime: number) {
return oldTime === 0
? FileWatcherEventKind.Created
: newTime === 0
? FileWatcherEventKind.Deleted
: FileWatcherEventKind.Changed;
}

/*@internal*/
export interface RecursiveDirectoryWatcherHost {
watchDirectory: HostWatchDirectory;
Expand Down
24 changes: 19 additions & 5 deletions src/harness/unittests/tsserverProjectSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3028,7 +3028,7 @@ namespace ts.projectSystem {
const project = projectService.configuredProjects.get(configFile.path);
assert.isDefined(project);
checkProjectActualFiles(project, [file1.path, libFile.path, module1.path, module2.path, configFile.path]);
checkWatchedFiles(host, [libFile.path, module1.path, module2.path, configFile.path]);
checkWatchedFiles(host, [libFile.path, configFile.path]);
checkWatchedDirectories(host, [], /*recursive*/ false);
const watchedRecursiveDirectories = getTypeRootsFromLocation(root + "/a/b/src");
watchedRecursiveDirectories.push(`${root}/a/b/src`, `${root}/a/b/node_modules`);
Expand Down Expand Up @@ -6632,6 +6632,7 @@ namespace ts.projectSystem {
projectFiles.push(find(filesAndFoldersToAdd, f => f.path === lodashIndexPath));
// we would now not have failed lookup in the parent of appFolder since lodash is available
recursiveWatchedDirectories.length = 1;
recursiveWatchedDirectories.push(`${root}/a/b/node_modules`);
// npm installation complete, timeout after reload fs
timeoutAfterReloadFs = true;
verifyAfterPartialOrCompleteNpmInstall(2);
Expand All @@ -6654,7 +6655,7 @@ namespace ts.projectSystem {
const projectFilePaths = map(projectFiles, f => f.path);
checkProjectActualFiles(project, projectFilePaths);

const filesWatched = filter(projectFilePaths, p => p !== app.path);
const filesWatched = filter(projectFilePaths, p => p !== app.path && p.indexOf("/a/b/node_modules") === -1);
checkWatchedFiles(host, filesWatched);
checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true);
checkWatchedDirectories(host, [], /*recursive*/ false);
Expand Down Expand Up @@ -7689,10 +7690,23 @@ namespace ts.projectSystem {
}

function verifyWatchesWithConfigFile(host: TestServerHost, files: FileOrFolder[], openFile: FileOrFolder) {
checkWatchedFiles(host, mapDefined(files, f => f === openFile ? undefined : f.path));
const nodeModulesDirs = createMap<true>();
checkWatchedFiles(host, mapDefined(files, f => {
if (f === openFile) {
return undefined;
}

const indexOfNodeModules = f.path.indexOf("/node_modules/");
if (indexOfNodeModules === -1) {
return f.path;
}

nodeModulesDirs.set(f.path.substr(0, indexOfNodeModules + "/node_modules".length), true);
return undefined;
}));
checkWatchedDirectories(host, [], /*recursive*/ false);
const configDirectory = getDirectoryPath(configFile.path);
checkWatchedDirectories(host, [configDirectory, `${configDirectory}/${nodeModulesAtTypes}`], /*recursive*/ true);
checkWatchedDirectories(host, [configDirectory, `${configDirectory}/${nodeModulesAtTypes}`].concat(arrayFrom(nodeModulesDirs.keys())), /*recursive*/ true);
}

describe("from files in same folder", () => {
Expand Down Expand Up @@ -7892,7 +7906,7 @@ namespace ts.projectSystem {
verifyTrace(resolutionTrace, expectedTrace);

const currentDirectory = getDirectoryPath(file1.path);
const watchedFiles = mapDefined(files, f => f === file1 ? undefined : f.path);
const watchedFiles = mapDefined(files, f => f === file1 || f.path.indexOf("/node_modules/") !== -1 ? undefined : f.path);
forEachAncestorDirectory(currentDirectory, d => {
watchedFiles.push(combinePaths(d, "tsconfig.json"), combinePaths(d, "jsconfig.json"));
});
Expand Down
96 changes: 86 additions & 10 deletions src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ namespace ts.server {
ClosedScriptInfo = "Closed Script info",
ConfigFileForInferredRoot = "Config file for the inferred project root",
FailedLookupLocation = "Directory of Failed lookup locations in module resolution",
TypeRoots = "Type root directory"
TypeRoots = "Type root directory",
ScriptInfoInNodeModules = "node_modules for closed script infos",
}

const enum ConfigFileWatcherStatus {
Expand Down Expand Up @@ -306,6 +307,10 @@ namespace ts.server {
syntaxOnly?: boolean;
}

interface ScriptInfoInNodeModulesWatcher extends FileWatcher {
refCount: number;
}

function getDetailWatchInfo(watchType: WatchType, project: Project | undefined) {
return `Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`;
}
Expand All @@ -325,6 +330,7 @@ namespace ts.server {
* Container of all known scripts
*/
private readonly filenameToScriptInfo = createMap<ScriptInfo>();
private readonly scriptInfoInNodeModulesWatchers = createMap <ScriptInfoInNodeModulesWatcher>();
/**
* Map to the real path of the infos
*/
Expand Down Expand Up @@ -1742,16 +1748,86 @@ namespace ts.server {
if (!info.isDynamicOrHasMixedContent() &&
(!this.globalCacheLocationDirectoryPath ||
!startsWith(info.path, this.globalCacheLocationDirectoryPath))) {
const { fileName } = info;
info.fileWatcher = this.watchFactory.watchFilePath(
this.host,
fileName,
(fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path),
PollingInterval.Medium,
info.path,
WatchType.ClosedScriptInfo
);

const indexOfNodeModules = info.path.indexOf("/node_modules/");
if (indexOfNodeModules === -1) {
info.fileWatcher = this.watchFactory.watchFilePath(
this.host,
info.fileName,
(fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path),
PollingInterval.Medium,
info.path,
WatchType.ClosedScriptInfo
);
}
else {
info.mTime = this.getModifiedTime(info);
info.fileWatcher = this.watchClosedScriptInfoInNodeModules(info.path.substr(0, indexOfNodeModules) as Path);
}
}
}


private watchClosedScriptInfoInNodeModules(dir: Path): ScriptInfoInNodeModulesWatcher {
// Watch only directory
const existing = this.scriptInfoInNodeModulesWatchers.get(dir);
if (existing) {
existing.refCount++;
return existing;
}

const watchDir = dir + "/node_modules" as Path;
let watcher = this.watchFactory.watchDirectory(
this.host,
watchDir,
(fileOrDirectory) => {
const fileOrDirectoryPath = this.toPath(fileOrDirectory);
if (fileOrDirectoryPath.indexOf("/.staging") || fileOrDirectoryPath.indexOf("/.bin")) return;
// Has extension
Debug.assert(result.refCount > 0);
if (watchDir === fileOrDirectoryPath) {
this.refreshScriptInfosInDirectory(watchDir);
}
else {
this.refreshScriptInfosInDirectory(hasExtension(fileOrDirectoryPath) ? getDirectoryPath(fileOrDirectoryPath) : fileOrDirectoryPath);
}
},
WatchDirectoryFlags.Recursive,
WatchType.ScriptInfoInNodeModules
);
const result: ScriptInfoInNodeModulesWatcher = {
close: () => {
if (result.refCount === 1) {
watcher.close();
watcher = undefined;
this.scriptInfoInNodeModulesWatchers.delete(dir);
}
else {
result.refCount--;
}
},
refCount: 1
};
this.scriptInfoInNodeModulesWatchers.set(dir, result);
return result;
}

private getModifiedTime(info: ScriptInfo) {
return (this.host.getModifiedTime(info.path) || new Date()).getTime();
}

private refreshScriptInfosInDirectory(dir: Path) {
dir = dir + directorySeparator as Path;
this.filenameToScriptInfo.forEach(info => {
if (startsWith(info.path, dir)) {
const mTime = this.getModifiedTime(info);
if (mTime !== info.mTime) {
const eventKind = getFileWatcherEventKind(info.mTime, mTime);
info.mTime = mTime;
this.onSourceFileChanged(info.fileName, eventKind, info.path);
}
}
});
}

private stopWatchingScriptInfo(info: ScriptInfo) {
Expand Down
3 changes: 3 additions & 0 deletions src/server/scriptInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ namespace ts.server {
/** Set to real path if path is different from info.path */
private realpath: Path | undefined;

/*@internal*/
mTime: number;

constructor(
private readonly host: ServerHost,
readonly fileName: NormalizedPath,
Expand Down
4 changes: 4 additions & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7968,6 +7968,7 @@ declare namespace ts.server {
* Container of all known scripts
*/
private readonly filenameToScriptInfo;
private readonly scriptInfoInNodeModulesWatchers;
/**
* maps external project file name to list of config files that were the part of this project
*/
Expand Down Expand Up @@ -8127,6 +8128,9 @@ declare namespace ts.server {
private createInferredProject;
getScriptInfo(uncheckedFileName: string): ScriptInfo;
private watchClosedScriptInfo;
private watchClosedScriptInfoInNodeModules;
private getModifiedTime;
private refreshScriptInfosInDirectory;
private stopWatchingScriptInfo;
getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: {
fileExists(path: string): boolean;
Expand Down