diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 8b48aa5c041..b4c428d5e5a 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -60,15 +60,12 @@ namespace ts { watcher: FileWatcher; /** ref count keeping this directory watch alive */ refCount: number; - /** map of refcount for the subDirectory */ - subDirectoryMap?: Map; } interface DirectoryOfFailedLookupWatch { dir: string; dirPath: Path; ignore?: true; - subDirectory?: Path; } export const maxNumberOfFilesToIterateForInvalidation = 256; @@ -403,20 +400,21 @@ namespace ts { } // Use some ancestor of the root directory - let subDirectory: Path | undefined; + let subDirectoryPath: Path | undefined, subDirectory: string | undefined; if (rootPath !== undefined) { while (!isInDirectoryPath(dirPath, rootPath)) { const parentPath = getDirectoryPath(dirPath); if (parentPath === dirPath) { break; } - subDirectory = dirPath.slice(parentPath.length + directorySeparator.length) as Path; + subDirectoryPath = dirPath; + subDirectory = dir; dirPath = parentPath; dir = getDirectoryPath(dir); } } - return filterFSRootDirectoriesToWatch({ dir, dirPath, subDirectory }, dirPath); + return filterFSRootDirectoriesToWatch({ dir: subDirectory || dir, dirPath: subDirectoryPath || dirPath }, dirPath); } function isPathWithDefaultFailedLookupExtension(path: Path) { @@ -439,7 +437,7 @@ namespace ts { let setAtRoot = false; for (const failedLookupLocation of failedLookupLocations) { const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); - const { dir, dirPath, ignore , subDirectory } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); + const { dir, dirPath, ignore } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); if (!ignore) { // If the failed lookup location path is not one of the supported extensions, // store it in the custom path @@ -451,7 +449,7 @@ namespace ts { setAtRoot = true; } else { - setDirectoryWatcher(dir, dirPath, subDirectory); + setDirectoryWatcher(dir, dirPath); } } } @@ -461,20 +459,13 @@ namespace ts { } } - function setDirectoryWatcher(dir: string, dirPath: Path, subDirectory?: Path) { - let dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); + function setDirectoryWatcher(dir: string, dirPath: Path) { + const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); if (dirWatcher) { dirWatcher.refCount++; } else { - dirWatcher = { watcher: createDirectoryWatcher(dir, dirPath), refCount: 1 }; - directoryWatchesOfFailedLookups.set(dirPath, dirWatcher); - } - - if (subDirectory) { - const subDirectoryMap = dirWatcher.subDirectoryMap || (dirWatcher.subDirectoryMap = createMap()); - const existing = subDirectoryMap.get(subDirectory) || 0; - subDirectoryMap.set(subDirectory, existing + 1); + directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath), refCount: 1 }); } } @@ -492,7 +483,7 @@ namespace ts { let removeAtRoot = false; for (const failedLookupLocation of failedLookupLocations) { const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); - const { dirPath, ignore, subDirectory } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); + const { dirPath, ignore } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); if (!ignore) { const refCount = customFailedLookupPaths.get(failedLookupLocationPath); if (refCount) { @@ -509,7 +500,7 @@ namespace ts { removeAtRoot = true; } else { - removeDirectoryWatcher(dirPath, subDirectory); + removeDirectoryWatcher(dirPath); } } } @@ -518,30 +509,12 @@ namespace ts { } } - function removeDirectoryWatcher(dirPath: string, subDirectory?: Path) { + function removeDirectoryWatcher(dirPath: string) { const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath)!; - if (subDirectory) { - const existing = dirWatcher.subDirectoryMap!.get(subDirectory)!; - if (existing === 1) { - dirWatcher.subDirectoryMap!.delete(subDirectory); - } - else { - dirWatcher.subDirectoryMap!.set(subDirectory, existing - 1); - } - } // Do not close the watcher yet since it might be needed by other failed lookup locations. dirWatcher.refCount--; } - function inWatchedSubdirectory(dirPath: Path, fileOrDirectoryPath: Path) { - const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); - if (!dirWatcher || !dirWatcher.subDirectoryMap) return false; - return forEachKey(dirWatcher.subDirectoryMap, subDirectory => { - const fullSubDirectory = `${dirPath}/${subDirectory}` as Path; - return fullSubDirectory === fileOrDirectoryPath || isInDirectoryPath(fullSubDirectory, fileOrDirectoryPath); - }); - } - function createDirectoryWatcher(directory: string, dirPath: Path) { return resolutionHost.watchDirectoryOfFailedLookupLocation(directory, fileOrDirectory => { const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); @@ -550,13 +523,8 @@ namespace ts { cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); } - // If the files are added to project root or node_modules directory, always run through the invalidation process - // Otherwise run through invalidation only if adding to the immediate directory - if (!allFilesHaveInvalidatedResolution && - (dirPath === rootPath || isNodeModulesDirectory(dirPath) || getDirectoryPath(fileOrDirectoryPath) === dirPath || inWatchedSubdirectory(dirPath, fileOrDirectoryPath))) { - if (invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath)) { - resolutionHost.onInvalidatedResolution(); - } + if (!allFilesHaveInvalidatedResolution && invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath)) { + resolutionHost.onInvalidatedResolution(); } }, WatchDirectoryFlags.Recursive); } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 44b6c8ea887..e6fffbc5e79 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -10,9 +10,12 @@ namespace ts.tscWatch { import checkArray = TestFSWithWatch.checkArray; import libFile = TestFSWithWatch.libFile; import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; + import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; + import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; import checkOutputContains = TestFSWithWatch.checkOutputContains; import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; + import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; export function checkProgramActualFiles(program: Program, expectedFiles: string[]) { checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); @@ -2379,7 +2382,7 @@ declare module "fs" { }); describe("tsc-watch when watchDirectories implementation", () => { - function verifyRenamingFileInSubFolder(tscWatchDirectory: TestFSWithWatch.Tsc_WatchDirectory) { + function verifyRenamingFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { const projectFolder = "/a/username/project"; const projectSrcFolder = `${projectFolder}/src`; const configFile: File = { @@ -2399,8 +2402,8 @@ declare module "fs" { const projectFolders = [projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`]; // Watching files config file, file, lib file const expectedWatchedFiles = files.map(f => f.path); - const expectedWatchedDirectories = tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory ? projectFolders : emptyArray; - if (tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.WatchFile) { + const expectedWatchedDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? projectFolders : emptyArray; + if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) { expectedWatchedFiles.push(...projectFolders); } @@ -2410,7 +2413,7 @@ declare module "fs" { file.path = file.path.replace("file1.ts", "file2.ts"); expectedWatchedFiles[0] = file.path; host.reloadFS(files); - if (tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.DynamicPolling) { + if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { // With dynamic polling the fs change would be detected only by running timeouts host.runQueuedTimeoutCallbacks(); } @@ -2429,21 +2432,21 @@ declare module "fs" { checkWatchedDirectories(host, emptyArray, /*recursive*/ true); // Watching config file, file, lib file and directories - TestFSWithWatch.checkMultiMapEachKeyWithCount("watchedFiles", host.watchedFiles, expectedWatchedFiles, 1); - TestFSWithWatch.checkMultiMapEachKeyWithCount("watchedDirectories", host.watchedDirectories, expectedWatchedDirectories, 1); + checkWatchedFilesDetailed(host, expectedWatchedFiles, 1); + checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, 1, /*recursive*/ false); } } it("uses watchFile when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.WatchFile); + verifyRenamingFileInSubFolder(Tsc_WatchDirectory.WatchFile); }); it("uses non recursive watchDirectory when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory); + verifyRenamingFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory); }); it("uses non recursive dynamic polling when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.DynamicPolling); + verifyRenamingFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); }); it("when there are symlinks to folders in recursive folders", () => { @@ -2482,7 +2485,7 @@ declare module "fs" { }; const files = [file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB]; const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory); + environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory); const host = createWatchedSystem(files, { environmentVariables, currentDirectory: cwd }); createWatchOfConfigFile("tsconfig.json", host); checkWatchedDirectories(host, emptyArray, /*recursive*/ true); @@ -2491,4 +2494,46 @@ declare module "fs" { }); }); }); + + describe("tsc-watch with modules linked to sibling folder", () => { + const projectRoot = "/user/username/projects/project"; + const mainPackageRoot = `${projectRoot}/main`; + const linkedPackageRoot = `${projectRoot}/linked-package`; + const mainFile: File = { + path: `${mainPackageRoot}/index.ts`, + content: "import { Foo } from '@scoped/linked-package'" + }; + const config: File = { + path: `${mainPackageRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, + files: ["index.ts"] + }) + }; + const linkedPackageInMain: SymLink = { + path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, + symLink: `${linkedPackageRoot}` + }; + const linkedPackageJson: File = { + path: `${linkedPackageRoot}/package.json`, + content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) + }; + const linkedPackageIndex: File = { + path: `${linkedPackageRoot}/dist/index.d.ts`, + content: "export * from './other';" + }; + const linkedPackageOther: File = { + path: `${linkedPackageRoot}/dist/other.d.ts`, + content: 'export declare const Foo = "BAR";' + }; + + it("verify watched directories", () => { + const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; + const host = createWatchedSystem(files, { currentDirectory: mainPackageRoot }); + 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); + }); + }); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index f6efe8b99dd..4518a740a85 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -19,6 +19,7 @@ namespace ts.projectSystem { export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; import safeList = TestFSWithWatch.safeList; + import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; export const customTypesMap = { path: "/typesMap.json", @@ -6259,7 +6260,7 @@ namespace ts.projectSystem { } function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: ReadonlyArray, nTimes: number) { - TestFSWithWatch.checkMultiMapEachKeyWithCount(callback, calledMaps[callback], expectedKeys, nTimes); + TestFSWithWatch.checkMultiMapKeyCount(callback, calledMaps[callback], expectedKeys, nTimes); } function verifyNoHostCalls() { @@ -7813,14 +7814,10 @@ new C();` checkCompleteEvent(session, 2, expectedSequenceId); } - function createSingleWatchMap(paths: string[]) { - return arrayToMap(paths, p => p, () => 1); - } - function verifyWatchedFilesAndDirectories(host: TestServerHost, files: string[], directories: string[]) { - checkWatchedFilesDetailed(host, createSingleWatchMap(files.filter(f => f !== recognizersDateTimeSrcFile.path))); + checkWatchedFilesDetailed(host, files.filter(f => f !== recognizersDateTimeSrcFile.path), 1); checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectoriesDetailed(host, createSingleWatchMap(directories), /*recursive*/ true); + checkWatchedDirectoriesDetailed(host, directories, 1, /*recursive*/ true); } function createSessionAndOpenFile(host: TestServerHost) { @@ -7842,7 +7839,7 @@ new C();` const filesAfterCompilation = [...filesWithNodeModulesSetup, recongnizerTextDistTypingFile]; const watchedDirectoriesWithResolvedModule = [`${recognizersDateTime}/src`, withPathMapping ? packages : recognizersDateTime, ...getTypeRootsFromLocation(recognizersDateTime)]; - const watchedDirectoriesWithUnresolvedModule = [recognizersDateTime, ...watchedDirectoriesWithResolvedModule, ...getNodeModuleDirectories(packages)]; + const watchedDirectoriesWithUnresolvedModule = [recognizersDateTime, ...(withPathMapping ? [recognizersText] : emptyArray), ...watchedDirectoriesWithResolvedModule, ...getNodeModuleDirectories(packages)]; function verifyProjectWithResolvedModule(session: TestSession) { const projectService = session.getProjectService(); @@ -8315,7 +8312,7 @@ new C();` }); describe("tsserverProjectSystem watchDirectories implementation", () => { - function verifyCompletionListWithNewFileInSubFolder(tscWatchDirectory: TestFSWithWatch.Tsc_WatchDirectory) { + function verifyCompletionListWithNewFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { const projectFolder = "/a/username/project"; const projectSrcFolder = `${projectFolder}/src`; const configFile: File = { @@ -8336,9 +8333,9 @@ new C();` // All closed files(files other than index), project folder, project/src folder and project/node_modules/@types folder const expectedWatchedFiles = arrayToMap(fileNames.slice(1), s => s, () => 1); const expectedWatchedDirectories = createMap(); - const mapOfDirectories = tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory ? + const mapOfDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? expectedWatchedDirectories : - tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.WatchFile ? + tscWatchDirectory === Tsc_WatchDirectory.WatchFile ? expectedWatchedFiles : createMap(); // For failed resolution lookup and tsconfig files @@ -8385,15 +8382,15 @@ new C();` } it("uses watchFile when file is added to subfolder, completion list has new file", () => { - verifyCompletionListWithNewFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.WatchFile); + verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.WatchFile); }); it("uses non recursive watchDirectory when file is added to subfolder, completion list has new file", () => { - verifyCompletionListWithNewFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory); + verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory); }); it("uses dynamic polling when file is added to subfolder, completion list has new file", () => { - verifyCompletionListWithNewFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.DynamicPolling); + verifyCompletionListWithNewFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); }); }); diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 0afa67880a4..484d3302c1b 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -175,7 +175,10 @@ interface Array {}` } } - export function checkMultiMapKeyCount(caption: string, actual: MultiMap, expectedKeys: Map) { + export function checkMultiMapKeyCount(caption: string, actual: MultiMap, expectedKeys: ReadonlyMap): void; + export function checkMultiMapKeyCount(caption: string, actual: MultiMap, expectedKeys: ReadonlyArray, eachKeyCount: number): void; + export function checkMultiMapKeyCount(caption: string, actual: MultiMap, expectedKeysMapOrArray: ReadonlyMap | ReadonlyArray, eachKeyCount?: number) { + const expectedKeys = isArray(expectedKeysMapOrArray) ? arrayToMap(expectedKeysMapOrArray, s => s, () => eachKeyCount!) : expectedKeysMapOrArray; verifyMapSize(caption, actual, arrayFrom(expectedKeys.keys())); expectedKeys.forEach((count, name) => { assert.isTrue(actual.has(name), `${caption}: expected to contain ${name}, actual keys: ${arrayFrom(actual.keys())}`); @@ -183,10 +186,6 @@ interface Array {}` }); } - export function checkMultiMapEachKeyWithCount(caption: string, actual: MultiMap, expectedKeys: ReadonlyArray, count: number) { - return checkMultiMapKeyCount(caption, actual, arrayToMap(expectedKeys, s => s, () => count)); - } - export function checkArray(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { assert.equal(actual.length, expected.length, `${caption}: incorrect actual number of files, expected:\r\n${expected.join("\r\n")}\r\ngot: ${actual.join("\r\n")}`); for (const f of expected) { @@ -198,16 +197,31 @@ interface Array {}` checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles); } - export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: Map) { - checkMultiMapKeyCount("watchedFiles", host.watchedFiles, expectedFiles); + export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: ReadonlyMap): void; + export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: ReadonlyArray, eachFileWatchCount: number): void; + export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: ReadonlyMap | ReadonlyArray, eachFileWatchCount?: number) { + if (isArray(expectedFiles)) { + checkMultiMapKeyCount("watchedFiles", host.watchedFiles, expectedFiles, eachFileWatchCount!); + } + else { + 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, recursive: boolean) { - checkMultiMapKeyCount(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); + export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: ReadonlyMap, recursive: boolean): void; + export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: ReadonlyArray, eachDirectoryWatchCount: number, recursive: boolean): void; + export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: ReadonlyMap | ReadonlyArray, recursiveOrEachDirectoryWatchCount: boolean | number, recursive?: boolean) { + if (isArray(expectedDirectories)) { + checkMultiMapKeyCount(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories, recursiveOrEachDirectoryWatchCount as number); + } + else { + recursive = recursiveOrEachDirectoryWatchCount as boolean; + checkMultiMapKeyCount(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); + } } export function checkOutputContains(host: TestServerHost, expected: ReadonlyArray) {