diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 0c500d5500e..bcf71b4016a 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -2,7 +2,7 @@ namespace ts { export type FileWatcherCallback = (fileName: string, removed?: boolean) => void; - export type DirectoryWatcherCallback = (directoryName: string) => void; + export type DirectoryWatcherCallback = (fileName: string) => void; export interface WatchedFile { fileName: string; callback: FileWatcherCallback; diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index c69821ced5f..e5b140237e5 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -107,6 +107,30 @@ namespace ts { } } + function checkConfiguredProjectNumber(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.configuredProjects.length, expected, `expected ${expected} configured project(s)`); + } + + function checkInferredProjectNumber(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); + } + + function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) { + checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles); + } + + function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[]) { + checkMapKeys("watchedDirectories", host.watchedDirectories, expectedDirectories); + } + + function checkConfiguredProjectActualFiles(project: server.Project, expectedFiles: string[]) { + checkFileNames("configuredProjects project, actualFileNames", project.getFileNames(), expectedFiles); + } + + function checkConfiguredProjectRootFiles(project: server.Project, expectedFiles: string[]) { + checkFileNames("configuredProjects project, rootFileNames", project.getRootFiles(), expectedFiles); + } + class TestServerHost implements server.ServerHost { args: string[] = []; newLine: "\n"; @@ -188,6 +212,26 @@ namespace ts { }; } + triggerDirectoryWatcherCallback(directoryName: string, fileName: string): void { + const path = this.toPath(directoryName); + const callbacks = lookUp(this.watchedDirectories, path); + if (callbacks) { + for (const callback of callbacks) { + callback.cb(fileName); + } + } + } + + triggerFileWatcherCallback(fileName: string): void { + const path = this.toPath(fileName); + const callbacks = lookUp(this.watchedFiles, path); + if (callbacks) { + for (const callback of callbacks) { + callback(path, /*removed*/ true); + } + } + } + watchFile(fileName: string, callback: FileWatcherCallback) { const path = this.toPath(fileName); const callbacks = lookUp(this.watchedFiles, path) || (this.watchedFiles[path] = []); @@ -204,7 +248,7 @@ namespace ts { } // TOOD: record and invoke callbacks to simulate timer events - readonly setTimeout = (callback: (...args: any[]) => void, ms: number, ...args: any[]): any => void 0; + readonly setTimeout = setTimeout; readonly clearTimeout = (timeoutId: any): void => void 0; readonly readFile = (s: string) => (this.fs.get(this.toPath(s))).content; readonly resolvePath = (s: string) => s; @@ -216,7 +260,20 @@ namespace ts { readonly exit = () => notImplemented(); } - describe("tsserver project system:", () => { + describe("tsserver-project-system", () => { + const commonFile1: FileOrFolder = { + path: "/a/b/commonFile1.ts", + content: "let x = 1" + }; + const commonFile2: FileOrFolder = { + path: "/a/b/commonFile2.ts", + content: "let y = 1" + }; + const libFile: FileOrFolder = { + path: "/a/lib/lib.d.ts", + content: libFileContent + }; + it("create inferred project", () => { const appFile: FileOrFolder = { path: "/a/b/c/app.ts", @@ -225,10 +282,7 @@ namespace ts { console.log(f) ` }; - const libFile: FileOrFolder = { - path: "/a/lib/lib.d.ts", - content: libFileContent - }; + const moduleFile: FileOrFolder = { path: "/a/b/c/module.d.ts", content: `export let x: number` @@ -238,13 +292,13 @@ namespace ts { const { configFileName } = projectService.openClientFile(appFile.path); assert(!configFileName, `should not find config, got: '${configFileName}`); - assert.equal(projectService.inferredProjects.length, 1, "expected one inferred project"); - assert.equal(projectService.configuredProjects.length, 0, "expected no configured project"); + checkConfiguredProjectNumber(projectService, 0); + checkInferredProjectNumber(projectService, 1); const project = projectService.inferredProjects[0]; checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); - checkMapKeys("watchedDirectories", host.watchedDirectories, ["/a/b/c", "/a/b", "/a"]); + checkWatchedDirectories(host, ["/a/b/c", "/a/b", "/a"]); }); it("create configured project without file list", () => { @@ -258,10 +312,6 @@ namespace ts { ] }` }; - const libFile: FileOrFolder = { - path: "/a/lib/lib.d.ts", - content: libFileContent - }; const file1: FileOrFolder = { path: "/a/b/c/f1.ts", content: "let x = 1" @@ -274,21 +324,130 @@ namespace ts { path: "/a/b/e/f3.ts", content: "let z = 1" }; + const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [ configFile, libFile, file1, file2, file3 ]); const projectService = new server.ProjectService(host, nullLogger); const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); assert(configFileName, "should find config file"); assert.isTrue(!configFileErrors, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); - assert.equal(projectService.inferredProjects.length, 0, "expected no inferred project"); - assert.equal(projectService.configuredProjects.length, 1, "expected one configured project"); + checkInferredProjectNumber(projectService, 0); + checkConfiguredProjectNumber(projectService, 1); const project = projectService.configuredProjects[0]; - checkFileNames("configuredProjects project, actualFileNames", project.getFileNames(), [file1.path, libFile.path, file2.path]); - checkFileNames("configuredProjects project, rootFileNames", project.getRootFiles(), [file1.path, file2.path]); + checkConfiguredProjectActualFiles(project, [file1.path, libFile.path, file2.path]); + checkConfiguredProjectRootFiles(project, [file1.path, file2.path]); + // watching all files except one that was open + checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); + checkWatchedDirectories(host, [getDirectoryPath(configFile.path)]); + }); - checkMapKeys("watchedFiles", host.watchedFiles, [configFile.path, file2.path, libFile.path]); // watching all files except one that was open - checkMapKeys("watchedDirectories", host.watchedDirectories, [getDirectoryPath(configFile.path)]); + it("add and then remove a config file in a folder with loose files", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "files": ["commonFile1.ts"] + }` + }; + const filesWithoutConfig = [ libFile, commonFile1, commonFile2 ]; + const filesWithConfig = [ libFile, commonFile1, commonFile2, configFile ]; + const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", filesWithoutConfig); + const projectService = new server.ProjectService(host, nullLogger); + projectService.openClientFile(commonFile1.path); + projectService.openClientFile(commonFile2.path); + + checkInferredProjectNumber(projectService, 2); + checkWatchedDirectories(host, ["/a/b", "/a"]); + + // Add a tsconfig file + host.reloadFS(filesWithConfig); + host.triggerDirectoryWatcherCallback("/a/b", configFile.path); + + checkInferredProjectNumber(projectService, 1); + checkConfiguredProjectNumber(projectService, 1); + // watching all files except one that was open + checkWatchedFiles(host, [libFile.path, configFile.path]); + + // remove the tsconfig file + host.reloadFS(filesWithoutConfig); + host.triggerFileWatcherCallback(configFile.path); + checkInferredProjectNumber(projectService, 2); + checkConfiguredProjectNumber(projectService, 0); + checkWatchedDirectories(host, ["/a/b", "/a"]); + }); + + it("add new files to a configured project without file list", (done: () => void) => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, libFile, configFile]); + const projectService = new server.ProjectService(host, nullLogger); + projectService.openClientFile(commonFile1.path); + checkWatchedDirectories(host, ["/a/b"]); + checkConfiguredProjectNumber(projectService, 1); + + const project = projectService.configuredProjects[0]; + checkConfiguredProjectRootFiles(project, [commonFile1.path]); + + // add a new ts file + host.reloadFS([commonFile1, commonFile2, libFile, configFile]); + host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path); + // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. + setTimeout(() => { + checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + done(); + }, 1000); + }); + + it("should ignore non-existing files specified in the config file", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": [ + "commonFile1.ts", + "commonFile3.ts" + ] + }` + }; + const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, configFile]); + const projectService = new server.ProjectService(host, nullLogger); + projectService.openClientFile(commonFile1.path); + projectService.openClientFile(commonFile2.path); + + checkConfiguredProjectNumber(projectService, 1); + const project = projectService.configuredProjects[0]; + checkConfiguredProjectRootFiles(project, [commonFile1.path]); + checkInferredProjectNumber(projectService, 1); + }); + + it("handle recreated files correctly", (done: () => void) => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, configFile]); + const projectService = new server.ProjectService(host, nullLogger); + projectService.openClientFile(commonFile1.path); + + checkConfiguredProjectNumber(projectService, 1); + const project = projectService.configuredProjects[0]; + checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + + // delete commonFile1 + projectService.closeClientFile(commonFile1.path); + host.reloadFS([configFile]); + host.triggerDirectoryWatcherCallback("/a/b", commonFile1.path); + host.setTimeout(() => { + // re-add commonFile1 + host.reloadFS([commonFile1, configFile]); + projectService.openClientFile(commonFile1.path); + host.setTimeout(() => { + checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + done(); + }, 500); + }, 500); }); }); } \ No newline at end of file