Merge pull request #23484 from Microsoft/typingInstallerWatch

Use watch recursive directories instead of watchFile for node_modules and bower components
This commit is contained in:
Sheetal Nandi 2018-04-19 09:59:58 -07:00 committed by GitHub
commit 0526ff5fad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 153 additions and 38 deletions

View file

@ -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 = {
@ -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);
}
}

View file

@ -141,7 +141,19 @@ 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);
@ -149,7 +161,9 @@ namespace ts.projectSystem {
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)", () => {
@ -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);

View file

@ -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>();

View file

@ -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;
}
}

View file

@ -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";
}
function getDirectoryExcludingNodeModulesOrBowerComponents(f: string) {
const indexOfNodeModules = f.indexOf("/node_modules/");
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 {
@ -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;
@ -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}`);
}
@ -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) {
@ -367,7 +385,7 @@ 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);
@ -375,43 +393,104 @@ namespace ts.server.typingsInstaller {
}
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;
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 => {
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 {