Skip to content

Use watch recursive directories instead of watchFile for node_modules and bower components #23484

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 19, 2018
Merged
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
8 changes: 5 additions & 3 deletions src/harness/unittests/tsserverProjectSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ namespace ts.projectSystem {
export import checkArray = TestFSWithWatch.checkArray;
export import libFile = TestFSWithWatch.libFile;
export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles;
import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories;
export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed;
export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories;
export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed;
import safeList = TestFSWithWatch.safeList;

export const customTypesMap = {
Expand Down Expand Up @@ -7821,8 +7823,8 @@ namespace ts.projectSystem {

checkWatchedDirectories(host, emptyArray, /*recursive*/ true);

TestFSWithWatch.checkMultiMapKeyCount("watchedFiles", host.watchedFiles, expectedWatchedFiles);
TestFSWithWatch.checkMultiMapKeyCount("watchedDirectories", host.watchedDirectories, expectedWatchedDirectories);
checkWatchedFilesDetailed(host, expectedWatchedFiles);
checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, /*recursive*/ false);
checkProjectActualFiles(project, fileNames);
}
}
Expand Down
30 changes: 27 additions & 3 deletions src/harness/unittests/typingsInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,29 @@ namespace ts.projectSystem {
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const p = configuredProjectAt(projectService, 0);
checkProjectActualFiles(p, [file1.path, tsconfig.path]);
checkWatchedFiles(host, [tsconfig.path, libFile.path, packageJson.path, "/a/b/bower_components", "/a/b/node_modules"]);

const expectedWatchedFiles = createMap<number>();
expectedWatchedFiles.set(tsconfig.path, 1); // tsserver
expectedWatchedFiles.set(libFile.path, 1); // tsserver
expectedWatchedFiles.set(packageJson.path, 1); // typing installer
checkWatchedFilesDetailed(host, expectedWatchedFiles);

checkWatchedDirectories(host, emptyArray, /*recursive*/ false);

const expectedWatchedDirectoriesRecursive = createMap<number>();
expectedWatchedDirectoriesRecursive.set("/a/b", 2); // TypingInstaller and wild card
expectedWatchedDirectoriesRecursive.set("/a/b/node_modules/@types", 1); // type root watch
checkWatchedDirectoriesDetailed(host, expectedWatchedDirectoriesRecursive, /*recursive*/ true);

installer.installAll(/*expectedCount*/ 1);

checkNumberOfProjects(projectService, { configuredProjects: 1 });
host.checkTimeoutQueueLengthAndRun(2);
checkProjectActualFiles(p, [file1.path, jquery.path, tsconfig.path]);
// should not watch jquery
checkWatchedFiles(host, [tsconfig.path, libFile.path, packageJson.path, "/a/b/bower_components", "/a/b/node_modules"]);
checkWatchedFilesDetailed(host, expectedWatchedFiles);
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
checkWatchedDirectoriesDetailed(host, expectedWatchedDirectoriesRecursive, /*recursive*/ true);
});

it("inferred project (typings installed)", () => {
Expand Down Expand Up @@ -827,7 +841,17 @@ namespace ts.projectSystem {
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const p = configuredProjectAt(projectService, 0);
checkProjectActualFiles(p, [app.path, jsconfig.path]);
checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules", libFile.path]);

const watchedFilesExpected = createMap<number>();
watchedFilesExpected.set(jsconfig.path, 1); // project files
watchedFilesExpected.set(libFile.path, 1); // project files
checkWatchedFilesDetailed(host, watchedFilesExpected);

checkWatchedDirectories(host, emptyArray, /*recursive*/ false);

const watchedRecursiveDirectoriesExpected = createMap<number>();
watchedRecursiveDirectoriesExpected.set("/", 2); // wild card + type installer
checkWatchedDirectoriesDetailed(host, watchedRecursiveDirectoriesExpected, /*recursive*/ true);

installer.installAll(/*expectedCount*/ 1);

Expand Down
10 changes: 9 additions & 1 deletion src/harness/virtualFileSystemWithWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,18 @@ interface Array<T> {}`
checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles);
}

export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive = false) {
export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: Map<number>) {
checkMultiMapKeyCount("watchedFiles", host.watchedFiles, expectedFiles);
}

export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive: boolean) {
checkMapKeys(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories);
}

export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: Map<number>, recursive: boolean) {
checkMultiMapKeyCount(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories);
}

export function checkOutputContains(host: TestServerHost, expected: ReadonlyArray<string>) {
const mapExpected = arrayToSet(expected);
const mapSeen = createMap<true>();
Expand Down
2 changes: 2 additions & 0 deletions src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,10 @@ declare namespace ts.server {

/* @internal */
export interface InstallTypingHost extends JsTyping.TypingResolutionHost {
useCaseSensitiveFileNames: boolean;
writeFile(path: string, content: string): void;
createDirectory(path: string): void;
watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher;
watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
}
}
141 changes: 110 additions & 31 deletions src/server/typingsInstaller/typingsInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ namespace ts.server.typingsInstaller {
onRequestCompleted: RequestCompletedAction;
}

function isPackageOrBowerJson(fileName: string) {
const base = getBaseFileName(fileName);
return base === "package.json" || base === "bower.json";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this take a flag for case sensitivity?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the file here is always canonical file name

}

function getDirectoryExcludingNodeModulesOrBowerComponents(f: string) {
const indexOfNodeModules = f.indexOf("/node_modules/");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have helpers for this? E.g. commonPackageFolders in core.ts.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But TI uses only these two locations.

const indexOfBowerComponents = f.indexOf("/bower_components/");
const subStrLength = indexOfNodeModules === -1 || indexOfBowerComponents === -1 ?
Math.max(indexOfNodeModules, indexOfBowerComponents) :
Math.min(indexOfNodeModules, indexOfBowerComponents);
return subStrLength === -1 ? f : f.substr(0, subStrLength);
}

type ProjectWatchers = Map<FileWatcher> & { isInvoked?: boolean; };

export abstract class TypingsInstaller {
Expand All @@ -73,6 +87,8 @@ namespace ts.server.typingsInstaller {
private readonly projectWatchers = createMap<ProjectWatchers>();
private safeList: JsTyping.SafeList | undefined;
readonly pendingRunRequests: PendingRequest[] = [];
private readonly toCanonicalFileName: GetCanonicalFileName;
private readonly globalCacheCanonicalPackageJsonPath: string;

private installRunCount = 1;
private inFlightRequestCount = 0;
Expand All @@ -86,6 +102,8 @@ namespace ts.server.typingsInstaller {
private readonly typesMapLocation: Path,
private readonly throttleLimit: number,
protected readonly log = nullLog) {
this.toCanonicalFileName = createGetCanonicalFileName(installTypingHost.useCaseSensitiveFileNames);
this.globalCacheCanonicalPackageJsonPath = combinePaths(this.toCanonicalFileName(globalCachePath), "package.json");
if (this.log.isEnabled()) {
this.log.writeLine(`Global cache location '${globalCachePath}', safe file path '${safeListPath}', types map path ${typesMapLocation}`);
}
Expand Down Expand Up @@ -147,7 +165,7 @@ namespace ts.server.typingsInstaller {
}

// start watching files
this.watchFiles(req.projectName, discoverTypingsResult.filesToWatch);
this.watchFiles(req.projectName, discoverTypingsResult.filesToWatch, req.projectRootPath);

// install typings
if (discoverTypingsResult.newTypingNames.length) {
Expand Down Expand Up @@ -367,51 +385,112 @@ namespace ts.server.typingsInstaller {
}
}

private watchFiles(projectName: string, files: string[]) {
private watchFiles(projectName: string, files: string[], projectRootPath: Path) {
if (!files.length) {
// shut down existing watchers
this.closeWatchers(projectName);
return;
}

let watchers = this.projectWatchers.get(projectName);
const toRemove = createMap<FileWatcher>();
if (!watchers) {
watchers = createMap();
this.projectWatchers.set(projectName, watchers);
}
else {
copyEntries(watchers, toRemove);
}

watchers.isInvoked = false;
// handler should be invoked once for the entire set of files since it will trigger full rediscovery of typings
watchers.isInvoked = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably a dumb question, but isn't watchers a map? Why would it have an isInvoked property?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its a expando on map.. This is so that we arent catching incorrect local variable. the state recides on the map (more on this #23438 (comment))


const isLoggingEnabled = this.log.isEnabled();
mutateMap(
watchers,
arrayToSet(files),
{
// Watch the missing files
createNewValue: file => {
if (isLoggingEnabled) {
this.log.writeLine(`FileWatcher:: Added:: WatchInfo: ${file}`);
}
const watcher = this.installTypingHost.watchFile(file, (f, eventKind) => {
if (isLoggingEnabled) {
this.log.writeLine(`FileWatcher:: Triggered with ${f} eventKind: ${FileWatcherEventKind[eventKind]}:: WatchInfo: ${file}:: handler is already invoked '${watchers.isInvoked}'`);
}
if (!watchers.isInvoked) {
watchers.isInvoked = true;
this.sendResponse({ projectName, kind: ActionInvalidate });
}
}, /*pollingInterval*/ 2000);
return isLoggingEnabled ? {
close: () => {
this.log.writeLine(`FileWatcher:: Closed:: WatchInfo: ${file}`);
}
} : watcher;
},
// Files that are no longer missing (e.g. because they are no longer required)
// should no longer be watched.
onDeleteValue: closeFileWatcher
const createProjectWatcher = (path: string, createWatch: (path: string) => FileWatcher) => {
toRemove.delete(path);
if (watchers.has(path)) {
return;
}

watchers.set(path, createWatch(path));
};
const createProjectFileWatcher = (file: string): FileWatcher => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to prefer lambdas over functions or vice versa?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lambdas are needed because we use this

if (isLoggingEnabled) {
this.log.writeLine(`FileWatcher:: Added:: WatchInfo: ${file}`);
}
const watcher = this.installTypingHost.watchFile(file, (f, eventKind) => {
if (isLoggingEnabled) {
this.log.writeLine(`FileWatcher:: Triggered with ${f} eventKind: ${FileWatcherEventKind[eventKind]}:: WatchInfo: ${file}:: handler is already invoked '${watchers.isInvoked}'`);
}
if (!watchers.isInvoked) {
watchers.isInvoked = true;
this.sendResponse({ projectName, kind: ActionInvalidate });
}
}, /*pollingInterval*/ 2000);

return isLoggingEnabled ? {
close: () => {
this.log.writeLine(`FileWatcher:: Closed:: WatchInfo: ${file}`);
watcher.close();
}
} : watcher;
};
const createProjectDirectoryWatcher = (dir: string): FileWatcher => {
if (isLoggingEnabled) {
this.log.writeLine(`DirectoryWatcher:: Added:: WatchInfo: ${dir} recursive`);
}
const watcher = this.installTypingHost.watchDirectory(dir, f => {
if (isLoggingEnabled) {
this.log.writeLine(`DirectoryWatcher:: Triggered with ${f} :: WatchInfo: ${dir} recursive :: handler is already invoked '${watchers.isInvoked}'`);
}
if (watchers.isInvoked) {
return;
}
f = this.toCanonicalFileName(f);
if (f !== this.globalCacheCanonicalPackageJsonPath && isPackageOrBowerJson(f)) {
watchers.isInvoked = true;
this.sendResponse({ projectName, kind: ActionInvalidate });
}
}, /*recursive*/ true);

return isLoggingEnabled ? {
close: () => {
this.log.writeLine(`DirectoryWatcher:: Closed:: WatchInfo: ${dir} recursive`);
watcher.close();
}
} : watcher;
};

// Create watches from list of files
for (const file of files) {
const filePath = this.toCanonicalFileName(file);
if (isPackageOrBowerJson(filePath)) {
// package.json or bower.json exists, watch the file to detect changes and update typings
createProjectWatcher(filePath, createProjectFileWatcher);
continue;
}

// path in projectRoot, watch project root
if (containsPath(projectRootPath, filePath, projectRootPath, !this.installTypingHost.useCaseSensitiveFileNames)) {
createProjectWatcher(projectRootPath, createProjectDirectoryWatcher);
continue;
}

// path in global cache, watch global cache
if (containsPath(this.globalCachePath, filePath, projectRootPath, !this.installTypingHost.useCaseSensitiveFileNames)) {
createProjectWatcher(this.globalCachePath, createProjectDirectoryWatcher);
continue;
}
);

// Get path without node_modules and bower_components
createProjectWatcher(getDirectoryExcludingNodeModulesOrBowerComponents(getDirectoryPath(filePath)), createProjectDirectoryWatcher);
}

// Remove unused watches
toRemove.forEach((watch, path) => {
watch.close();
watchers.delete(path);
});
}

private createSetTypings(request: DiscoverTypings, typings: string[]): SetTypings {
Expand Down