diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index d05d835050..9bf543254a 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -398,8 +398,17 @@ namespace ts { function getDirectoryToWatchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path): DirectoryOfFailedLookupWatch { if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { - // Always watch root directory recursively - return { dir: rootDir!, dirPath: rootPath }; // TODO: GH#18217 + failedLookupLocation = isRootedDiskPath(failedLookupLocation) ? failedLookupLocation : getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory()); + Debug.assert(failedLookupLocation.length === failedLookupLocationPath.length, `FailedLookup: ${failedLookupLocation} failedLookupLocationPath: ${failedLookupLocationPath}`); // tslint:disable-line + const subDirectoryInRoot = failedLookupLocationPath.indexOf(directorySeparator, rootPath.length + 1); + if (subDirectoryInRoot !== -1) { + // Instead of watching root, watch directory in root to avoid watching excluded directories not needed for module resolution + return { dir: failedLookupLocation.substr(0, subDirectoryInRoot), dirPath: failedLookupLocationPath.substr(0, subDirectoryInRoot) as Path }; + } + else { + // Always watch root directory non recursively + return { dir: rootDir!, dirPath: rootPath, nonRecursive: false }; // TODO: GH#18217 + } } return getDirectoryToWatchFromFailedLookupLocationDirectory( @@ -478,6 +487,7 @@ namespace ts { customFailedLookupPaths.set(failedLookupLocationPath, refCount + 1); } if (dirPath === rootPath) { + Debug.assert(!nonRecursive); setAtRoot = true; } else { @@ -487,8 +497,8 @@ namespace ts { } if (setAtRoot) { - // This is always recursive - setDirectoryWatcher(rootDir!, rootPath); // TODO: GH#18217 + // This is always non recursive + setDirectoryWatcher(rootDir!, rootPath, /*nonRecursive*/ true); // TODO: GH#18217 } } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 572c59bb30..2a2ed4ef7d 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -583,6 +583,10 @@ interface Array {}` return; } this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created); + if (isFsFolder(fileOrDirectory)) { + this.invokeDirectoryWatcher(fileOrDirectory.fullPath, ""); + this.invokeWatchedDirectoriesRecursiveCallback(fileOrDirectory.fullPath, ""); + } this.invokeDirectoryWatcher(folder.fullPath, fileOrDirectory.fullPath); } @@ -599,12 +603,11 @@ interface Array {}` this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); if (isFsFolder(fileOrDirectory)) { Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming); - const relativePath = this.getRelativePathToDirectory(fileOrDirectory.fullPath, fileOrDirectory.fullPath); // Invoke directory and recursive directory watcher for the folder // Here we arent invoking recursive directory watchers for the base folders // since that is something we would want to do for both file as well as folder we are deleting - this.invokeWatchedDirectoriesCallback(fileOrDirectory.fullPath, relativePath); - this.invokeWatchedDirectoriesRecursiveCallback(fileOrDirectory.fullPath, relativePath); + this.invokeWatchedDirectoriesCallback(fileOrDirectory.fullPath, ""); + this.invokeWatchedDirectoriesRecursiveCallback(fileOrDirectory.fullPath, ""); } if (basePath !== fileOrDirectory.path) { diff --git a/src/testRunner/unittests/tscWatchMode.ts b/src/testRunner/unittests/tscWatchMode.ts index e6c805148e..880c494645 100644 --- a/src/testRunner/unittests/tscWatchMode.ts +++ b/src/testRunner/unittests/tscWatchMode.ts @@ -2262,8 +2262,11 @@ declare module "fs" { const files = [file, module, libFile]; const host = createWatchedSystem(files, { currentDirectory }); const watch = createWatchOfFilesAndCompilerOptions([file.path], host); + checkProgramActualFiles(watch(), [file.path, libFile.path]); checkOutputErrorsInitial(host, [getDiagnosticModuleNotFoundOfFile(watch(), file, "qqq")]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, [`${currentDirectory}/node_modules`, `${currentDirectory}/node_modules/@types`], /*recursive*/ true); host.renameFolder(`${currentDirectory}/node_modules2`, `${currentDirectory}/node_modules`); host.runQueuedTimeoutCallbacks(); @@ -2622,7 +2625,7 @@ declare module "fs" { createWatchOfConfigFile("tsconfig.json", host); checkWatchedFilesDetailed(host, [libFile.path, mainFile.path, config.path, linkedPackageIndex.path, linkedPackageOther.path], 1); checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectoriesDetailed(host, [mainPackageRoot, linkedPackageRoot, `${mainPackageRoot}/node_modules/@types`, `${projectRoot}/node_modules/@types`], 1, /*recursive*/ true); + checkWatchedDirectoriesDetailed(host, [`${mainPackageRoot}/@scoped`, `${mainPackageRoot}/node_modules`, linkedPackageRoot, `${mainPackageRoot}/node_modules/@types`, `${projectRoot}/node_modules/@types`], 1, /*recursive*/ true); }); }); diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index ff5ee24bf5..b4611b10e7 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -622,8 +622,8 @@ namespace ts.projectSystem { const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, ["/a/b/c", combinePaths(getDirectoryPath(appFile.path), nodeModulesAtTypes)], /*recursive*/ true); + checkWatchedDirectories(host, ["/a/b/c"], /*recursive*/ false); + checkWatchedDirectories(host, [combinePaths(getDirectoryPath(appFile.path), nodeModulesAtTypes)], /*recursive*/ true); }); it("can handle tsconfig file name with difference casing", () => { @@ -3035,21 +3035,18 @@ namespace ts.projectSystem { assert.isDefined(project); checkProjectActualFiles(project, map(files, file => file.path)); checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); - checkWatchedDirectories(host, [], /*recursive*/ false); - const watchedRecursiveDirectories = ["/a/b/node_modules/@types"]; - watchedRecursiveDirectories.push("/a/b"); - checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + checkWatchedDirectoriesDetailed(host, ["/a/b"], 1, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); files.push(file2); host.reloadFS(files); host.runQueuedTimeoutCallbacks(); - watchedRecursiveDirectories.pop(); checkNumberOfProjects(projectService, { configuredProjects: 1 }); assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); // On next file open the files file2a should be closed and not watched any more projectService.openClientFile(file2.path); @@ -3057,8 +3054,8 @@ namespace ts.projectSystem { assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); checkWatchedFiles(host, [libFile.path, configFile.path]); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); }); @@ -3098,7 +3095,7 @@ namespace ts.projectSystem { checkWatchedFiles(host, [libFile.path, module1.path, module2.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`); + watchedRecursiveDirectories.push(`${root}/a/b/src/node_modules`, `${root}/a/b/node_modules`); checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); }); @@ -6780,7 +6777,7 @@ namespace ts.projectSystem { const { configFileName } = projectService.openClientFile(file1.path); assert.equal(configFileName, tsconfigFile.path as server.NormalizedPath, `should find config`); // tslint:disable-line no-unnecessary-type-assertion (TODO: GH#18217) checkNumberOfConfiguredProjects(projectService, 1); - const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, canonicalFrontendDir].concat(getNodeModuleDirectories(getDirectoryPath(canonicalFrontendDir))); + const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, `${canonicalFrontendDir}/types`, `${canonicalFrontendDir}/node_modules`].concat(getNodeModuleDirectories(getDirectoryPath(canonicalFrontendDir))); const project = projectService.configuredProjects.get(canonicalConfigPath)!; verifyProjectAndWatchedDirectories(); @@ -6934,7 +6931,7 @@ namespace ts.projectSystem { const projectService = createProjectService(host); const { configFileName } = projectService.openClientFile(app.path); assert.equal(configFileName, tsconfigJson.path as server.NormalizedPath, `should find config`); // TODO: GH#18217 - const recursiveWatchedDirectories: string[] = [appFolder].concat(getNodeModuleDirectories(getDirectoryPath(appFolder))); + const recursiveWatchedDirectories: string[] = [`${appFolder}`, `${appFolder}/node_modules`].concat(getNodeModuleDirectories(getDirectoryPath(appFolder))); verifyProject(); let timeoutAfterReloadFs = timeoutDuringPartialInstallation; @@ -7014,7 +7011,7 @@ namespace ts.projectSystem { const lodashIndexPath = root + "/a/b/node_modules/@types/lodash/index.d.ts"; 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.length = 2; // npm installation complete, timeout after reload fs timeoutAfterReloadFs = true; verifyAfterPartialOrCompleteNpmInstall(2); @@ -7534,9 +7531,9 @@ namespace ts.projectSystem { const openFiles = [file1.path]; const watchedRecursiveDirectories = useSlashRootAsSomeNotRootFolderInUserDirectory ? // Folders of node_modules lookup not in changedRoot - ["a/b/project", "a/b/node_modules", "a/node_modules", "node_modules"].map(v => rootFolder + v) : + ["a/b/project", "a/b/project/node_modules", "a/b/node_modules", "a/node_modules", "node_modules"].map(v => rootFolder + v) : // Folder of tsconfig - ["/a/b/project"]; + ["/a/b/project", "/a/b/project/node_modules"]; const host = createServerHost(projectFiles); const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); const projectService = session.getProjectService(); @@ -7565,7 +7562,7 @@ namespace ts.projectSystem { host.reloadFS(projectFiles); host.runQueuedTimeoutCallbacks(); if (useSlashRootAsSomeNotRootFolderInUserDirectory) { - watchedRecursiveDirectories.length = 2; + watchedRecursiveDirectories.length = 3; } else { // file2 addition wont be detected @@ -7993,10 +7990,10 @@ new C();` checkCompleteEvent(session, 2, expectedSequenceId); } - function verifyWatchedFilesAndDirectories(host: TestServerHost, files: string[], recursiveDirectories: string[], nonRecursiveDirectories: string[]) { + function verifyWatchedFilesAndDirectories(host: TestServerHost, files: string[], recursiveDirectories: ReadonlyMap, nonRecursiveDirectories: string[]) { checkWatchedFilesDetailed(host, files.filter(f => f !== recognizersDateTimeSrcFile.path), 1); - checkWatchedDirectoriesDetailed(host, nonRecursiveDirectories, 1, /*recursive*/ false); - checkWatchedDirectoriesDetailed(host, recursiveDirectories, 1, /*recursive*/ true); + checkWatchedDirectoriesDetailed(host, nonRecursiveDirectories, 1, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, recursiveDirectories, /*recursive*/ true); } function createSessionAndOpenFile(host: TestServerHost) { @@ -8017,8 +8014,16 @@ new C();` const filesWithNodeModulesSetup = [...filesWithSources, nodeModulesRecorgnizersText]; const filesAfterCompilation = [...filesWithNodeModulesSetup, recongnizerTextDistTypingFile]; - const watchedDirectoriesWithResolvedModule = [`${recognizersDateTime}/src`, ...(withPathMapping ? emptyArray : [recognizersDateTime]), ...getTypeRootsFromLocation(recognizersDateTime)]; - const watchedDirectoriesWithUnresolvedModule = [recognizersDateTime, ...(withPathMapping ? [recognizersText] : emptyArray), ...watchedDirectoriesWithResolvedModule, ...getNodeModuleDirectories(packages)]; + const watchedDirectoriesWithResolvedModule = arrayToMap(getTypeRootsFromLocation(recognizersDateTime), k => k, () => 1); + watchedDirectoriesWithResolvedModule.set(`${recognizersDateTime}/src`, withPathMapping ? 1 : 2); // wild card + failed lookups + if (!withPathMapping) { + watchedDirectoriesWithResolvedModule.set(`${recognizersDateTime}/node_modules`, 1); // failed lookups + } + const watchedDirectoriesWithUnresolvedModule = cloneMap(watchedDirectoriesWithResolvedModule); + watchedDirectoriesWithUnresolvedModule.set(`${recognizersDateTime}/src`, 2); // wild card + failed lookups + [`${recognizersDateTime}/node_modules`, ...(withPathMapping ? [recognizersText] : emptyArray), ...getNodeModuleDirectories(packages)].forEach(d => { + watchedDirectoriesWithUnresolvedModule.set(d, 1); + }); const nonRecursiveWatchedDirectories = withPathMapping ? [packages] : emptyArray; function verifyProjectWithResolvedModule(session: TestSession) { @@ -8251,11 +8256,10 @@ new C();` return `Reusing resolution of module '${moduleName}' to file '${file.path}' from old program.`; } - function verifyWatchesWithConfigFile(host: TestServerHost, files: File[], openFile: File) { + function verifyWatchesWithConfigFile(host: TestServerHost, files: File[], openFile: File, extraExpectedDirectories?: ReadonlyArray) { checkWatchedFiles(host, mapDefined(files, f => f === openFile ? undefined : f.path)); checkWatchedDirectories(host, [], /*recursive*/ false); - const configDirectory = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, [configDirectory, `${configDirectory}/${nodeModulesAtTypes}`], /*recursive*/ true); + checkWatchedDirectories(host, [projectLocation, `${projectLocation}/${nodeModulesAtTypes}`, ...(extraExpectedDirectories || emptyArray)], /*recursive*/ true); } describe("from files in same folder", () => { @@ -8299,6 +8303,7 @@ new C();` }); it("non relative module name", () => { + const expectedNonRelativeDirectories = [`${projectLocation}/node_modules`, `${projectLocation}/src`]; const module1Name = "module1"; const module2Name = "module2"; const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; @@ -8312,7 +8317,7 @@ new C();` const expectedTrace = getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name); getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); verifyTrace(resolutionTrace, expectedTrace); - verifyWatchesWithConfigFile(host, files, file1); + verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); file1.content += fileContent; file2.content += fileContent; @@ -8322,7 +8327,7 @@ new C();` getExpectedReusingResolutionFromOldProgram(file1, module1Name), getExpectedReusingResolutionFromOldProgram(file1, module2Name) ]); - verifyWatchesWithConfigFile(host, files, file1); + verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); }); }); @@ -8391,6 +8396,7 @@ new C();` }); it("non relative module name", () => { + const expectedNonRelativeDirectories = [`${projectLocation}/node_modules`, `${projectLocation}/product`]; const module1Name = "module1"; const module2Name = "module2"; const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; @@ -8410,7 +8416,7 @@ new C();` getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module1, module1Name, getDirectoryPath(file4.path), expectedTrace); getExpectedNonRelativeModuleResolutionFromCacheTrace(host, file3, module2, module2Name, getDirectoryPath(file4.path), expectedTrace); verifyTrace(resolutionTrace, expectedTrace); - verifyWatchesWithConfigFile(host, files, file1); + verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); file1.content += fileContent; file2.content += fileContent; @@ -8423,7 +8429,7 @@ new C();` getExpectedReusingResolutionFromOldProgram(file1, module1Name), getExpectedReusingResolutionFromOldProgram(file1, module2Name) ]); - verifyWatchesWithConfigFile(host, files, file1); + verifyWatchesWithConfigFile(host, files, file1, expectedNonRelativeDirectories); }); it("non relative module name from inferred project", () => { @@ -8460,7 +8466,7 @@ new C();` watchedFiles.push(combinePaths(d, "tsconfig.json"), combinePaths(d, "jsconfig.json")); }); const watchedRecursiveDirectories = getTypeRootsFromLocation(currentDirectory).concat([ - currentDirectory, `${projectLocation}/product/${nodeModules}`, + `${currentDirectory}/node_modules`, `${currentDirectory}/feature`, `${projectLocation}/product/${nodeModules}`, `${projectLocation}/${nodeModules}`, `${projectLocation}/product/test/${nodeModules}`, `${projectLocation}/product/test/src/${nodeModules}` ]); @@ -8530,7 +8536,6 @@ export const x = 10;` outDir: "../out", baseUrl: "./", typeRoots: ["typings"] - } }) }; @@ -8546,13 +8551,15 @@ export const x = 10;` checkWatchedDirectories(host, emptyArray, /*recursive*/ false); // since fs resolves to ambient module, shouldnt watch failed lookup } else { - checkWatchedDirectoriesDetailed(host, [`${projectRoot}`], 1, /*recursive*/ false); // failed lookup for fs + checkWatchedDirectoriesDetailed(host, [`${projectRoot}`, `${projectRoot}/src`], 1, /*recursive*/ false); // failed lookup for fs } const expectedWatchedDirectories = createMap(); - expectedWatchedDirectories.set(`${projectRoot}/src`, 2); // Wild card and failed lookup + expectedWatchedDirectories.set(`${projectRoot}/src`, 1); // Wild card + expectedWatchedDirectories.set(`${projectRoot}/src/somefolder`, 1); // failedLookup for somefolder/module2 + expectedWatchedDirectories.set(`${projectRoot}/src/node_modules`, 1); // failed lookup for somefolder/module2 expectedWatchedDirectories.set(`${projectRoot}/somefolder`, 1); // failed lookup for somefolder/module2 expectedWatchedDirectories.set(`${projectRoot}/node_modules`, 1); // failed lookup for with node_modules/@types/fs - expectedWatchedDirectories.set(`${projectRoot}/src/typings`, 1); // typeroot directory + expectedWatchedDirectories.set(`${projectRoot}/src/typings`, useNodeFile ? 1 : 2); // typeroot directory + failed lookup if not using node file checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, /*recursive*/ true); } diff --git a/src/testRunner/unittests/typingsInstaller.ts b/src/testRunner/unittests/typingsInstaller.ts index a267048335..b9f6565c07 100644 --- a/src/testRunner/unittests/typingsInstaller.ts +++ b/src/testRunner/unittests/typingsInstaller.ts @@ -146,8 +146,10 @@ namespace ts.projectSystem { checkWatchedDirectories(host, emptyArray, /*recursive*/ false); const expectedWatchedDirectoriesRecursive = createMap(); - expectedWatchedDirectoriesRecursive.set("/a/b", 2); // TypingInstaller and wild card + expectedWatchedDirectoriesRecursive.set("/a/b", 1); // wild card expectedWatchedDirectoriesRecursive.set("/a/b/node_modules/@types", 1); // type root watch + expectedWatchedDirectoriesRecursive.set("/a/b/node_modules", 1); // TypingInstaller + expectedWatchedDirectoriesRecursive.set("/a/b/bower_components", 1); // TypingInstaller checkWatchedDirectoriesDetailed(host, expectedWatchedDirectoriesRecursive, /*recursive*/ true); installer.installAll(/*expectedCount*/ 1); @@ -844,9 +846,7 @@ namespace ts.projectSystem { checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - const watchedRecursiveDirectoriesExpected = createMap(); - watchedRecursiveDirectoriesExpected.set("/", 2); // wild card + type installer - checkWatchedDirectoriesDetailed(host, watchedRecursiveDirectoriesExpected, /*recursive*/ true); + checkWatchedDirectoriesDetailed(host, ["/", "/node_modules", "/bower_components"], 1, /*recursive*/ true); installer.installAll(/*expectedCount*/ 1); diff --git a/src/typingsInstallerCore/typingsInstaller.ts b/src/typingsInstallerCore/typingsInstaller.ts index 66e8e6fe3b..44a51a0177 100644 --- a/src/typingsInstallerCore/typingsInstaller.ts +++ b/src/typingsInstallerCore/typingsInstaller.ts @@ -64,18 +64,24 @@ namespace ts.server.typingsInstaller { onRequestCompleted: RequestCompletedAction; } - function isPackageOrBowerJson(fileName: string) { - const base = getBaseFileName(fileName); - return base === "package.json" || base === "bower.json"; + function endsWith(str: string, suffix: string, caseSensitive: boolean): boolean { + const expectedPos = str.length - suffix.length; + return expectedPos >= 0 && + (str.indexOf(suffix, expectedPos) === expectedPos || + (!caseSensitive && compareStringsCaseInsensitive(str.substr(expectedPos), suffix) === Comparison.EqualTo)); } - 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); + function isPackageOrBowerJson(fileName: string, caseSensitive: boolean) { + return endsWith(fileName, "/package.json", caseSensitive) || endsWith(fileName, "/bower.json", caseSensitive); + } + + function sameFiles(a: string, b: string, caseSensitive: boolean) { + return a === b || (!caseSensitive && compareStringsCaseInsensitive(a, b) === Comparison.EqualTo); + } + + const enum ProjectWatcherType { + FileWatcher = "FileWatcher", + DirectoryWatcher = "DirectoryWatcher" } type ProjectWatchers = Map & { isInvoked?: boolean; }; @@ -88,7 +94,7 @@ namespace ts.server.typingsInstaller { private safeList: JsTyping.SafeList | undefined; readonly pendingRunRequests: PendingRequest[] = []; private readonly toCanonicalFileName: GetCanonicalFileName; - private readonly globalCacheCanonicalPackageJsonPath: string; + private readonly globalCachePackageJsonPath: string; private installRunCount = 1; private inFlightRequestCount = 0; @@ -103,7 +109,7 @@ namespace ts.server.typingsInstaller { private readonly throttleLimit: number, protected readonly log = nullLog) { this.toCanonicalFileName = createGetCanonicalFileName(installTypingHost.useCaseSensitiveFileNames); - this.globalCacheCanonicalPackageJsonPath = combinePaths(this.toCanonicalFileName(globalCachePath), "package.json"); + this.globalCachePackageJsonPath = combinePaths(globalCachePath, "package.json"); if (this.log.isEnabled()) { this.log.writeLine(`Global cache location '${globalCachePath}', safe file path '${safeListPath}', types map path ${typesMapLocation}`); } @@ -406,84 +412,79 @@ namespace ts.server.typingsInstaller { watchers.isInvoked = false; const isLoggingEnabled = this.log.isEnabled(); - const createProjectWatcher = (path: string, createWatch: (path: string) => FileWatcher) => { - toRemove.delete(path); - if (watchers.has(path)) { + const createProjectWatcher = (path: string, projectWatcherType: ProjectWatcherType) => { + const canonicalPath = this.toCanonicalFileName(path); + toRemove.delete(canonicalPath); + if (watchers.has(canonicalPath)) { return; } - watchers.set(path, createWatch(path)); - }; - const createProjectFileWatcher = (file: string): FileWatcher => { if (isLoggingEnabled) { - this.log.writeLine(`FileWatcher:: Added:: WatchInfo: ${file}`); + this.log.writeLine(`${projectWatcherType}:: Added:: WatchInfo: ${path}`); } - const watcher = this.installTypingHost.watchFile!(file, (f, eventKind) => { // TODO: GH#18217 - 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); + const watcher = projectWatcherType === ProjectWatcherType.FileWatcher ? + this.installTypingHost.watchFile!(path, (f, eventKind) => { // TODO: GH#18217 + if (isLoggingEnabled) { + this.log.writeLine(`FileWatcher:: Triggered with ${f} eventKind: ${FileWatcherEventKind[eventKind]}:: WatchInfo: ${path}:: handler is already invoked '${watchers.isInvoked}'`); + } + if (!watchers.isInvoked) { + watchers.isInvoked = true; + this.sendResponse({ projectName, kind: ActionInvalidate }); + } + }, /*pollingInterval*/ 2000) : + this.installTypingHost.watchDirectory!(path, f => { // TODO: GH#18217 + if (isLoggingEnabled) { + this.log.writeLine(`DirectoryWatcher:: Triggered with ${f} :: WatchInfo: ${path} recursive :: handler is already invoked '${watchers.isInvoked}'`); + } + if (watchers.isInvoked || !fileExtensionIs(f, Extension.Json)) { + return; + } - return isLoggingEnabled ? { + if (isPackageOrBowerJson(f, this.installTypingHost.useCaseSensitiveFileNames) && + !sameFiles(f, this.globalCachePackageJsonPath, this.installTypingHost.useCaseSensitiveFileNames)) { + watchers.isInvoked = true; + this.sendResponse({ projectName, kind: ActionInvalidate }); + } + }, /*recursive*/ true); + + watchers.set(canonicalPath, isLoggingEnabled ? { close: () => { - this.log.writeLine(`FileWatcher:: Closed:: WatchInfo: ${file}`); + this.log.writeLine(`${projectWatcherType}:: Closed:: WatchInfo: ${path}`); 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 => { // TODO: GH#18217 - 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; + } : watcher); }; // Create watches from list of files for (const file of files) { - const filePath = this.toCanonicalFileName(file); - if (isPackageOrBowerJson(filePath)) { + if (file.endsWith("/package.json") || file.endsWith("/bower.json")) { // package.json or bower.json exists, watch the file to detect changes and update typings - createProjectWatcher(filePath, createProjectFileWatcher); + createProjectWatcher(file, ProjectWatcherType.FileWatcher); continue; } // path in projectRoot, watch project root - if (containsPath(projectRootPath, filePath, projectRootPath, !this.installTypingHost.useCaseSensitiveFileNames)) { - createProjectWatcher(projectRootPath, createProjectDirectoryWatcher); + if (containsPath(projectRootPath, file, projectRootPath, !this.installTypingHost.useCaseSensitiveFileNames)) { + const subDirectory = file.indexOf(directorySeparator, projectRootPath.length + 1); + if (subDirectory !== -1) { + // Watch subDirectory + createProjectWatcher(file.substr(0, subDirectory), ProjectWatcherType.DirectoryWatcher); + } + else { + // Watch the directory itself + createProjectWatcher(file, ProjectWatcherType.DirectoryWatcher); + } continue; } // path in global cache, watch global cache - if (containsPath(this.globalCachePath, filePath, projectRootPath, !this.installTypingHost.useCaseSensitiveFileNames)) { - createProjectWatcher(this.globalCachePath, createProjectDirectoryWatcher); + if (containsPath(this.globalCachePath, file, projectRootPath, !this.installTypingHost.useCaseSensitiveFileNames)) { + createProjectWatcher(this.globalCachePath, ProjectWatcherType.DirectoryWatcher); continue; } - // Get path without node_modules and bower_components - createProjectWatcher(getDirectoryExcludingNodeModulesOrBowerComponents(getDirectoryPath(filePath)), createProjectDirectoryWatcher); + // watch node_modules or bower_components + createProjectWatcher(file, ProjectWatcherType.DirectoryWatcher); } // Remove unused watches