From 37949a3d65fefacff6527c3551e60819176d1e10 Mon Sep 17 00:00:00 2001 From: Zhengbo Li Date: Tue, 7 Jun 2016 15:52:34 -0700 Subject: [PATCH] more tests for module resolution change and exclude --- src/compiler/program.ts | 1 + src/server/editorServices.ts | 2 +- src/services/services.ts | 1 + .../cases/unittests/tsserverProjectSystem.ts | 194 ++++++++++++++---- 4 files changed, 159 insertions(+), 39 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index bab554927e..5b64d066da 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1143,6 +1143,7 @@ namespace ts { // if any of these properties has changed - structure cannot be reused const oldOptions = oldProgram.getCompilerOptions(); if ((oldOptions.module !== options.module) || + (oldOptions.moduleResolution !== options.moduleResolution) || (oldOptions.noResolve !== options.noResolve) || (oldOptions.target !== options.target) || (oldOptions.noLib !== options.noLib) || diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 8eae43b503..628522c7aa 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -268,7 +268,7 @@ namespace ts.server { } removeRoot(info: ScriptInfo) { - if (!this.filenameToScript.contains(info.path)) { + if (this.filenameToScript.contains(info.path)) { this.filenameToScript.remove(info.path); this.roots = copyListRemovingItem(info, this.roots); this.resolvedModuleNames.remove(info.path); diff --git a/src/services/services.ts b/src/services/services.ts index 90740b257c..f39eb60a81 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2919,6 +2919,7 @@ namespace ts { const changesInCompilationSettingsAffectSyntax = oldSettings && (oldSettings.target !== newSettings.target || oldSettings.module !== newSettings.module || + oldSettings.moduleResolution !== newSettings.moduleResolution || oldSettings.noResolve !== newSettings.noResolve || oldSettings.jsx !== newSettings.jsx || oldSettings.allowJs !== newSettings.allowJs); diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index e5b140237e..103e5bc7d9 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -107,11 +107,11 @@ namespace ts { } } - function checkConfiguredProjectNumber(projectService: server.ProjectService, expected: number) { + function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { assert.equal(projectService.configuredProjects.length, expected, `expected ${expected} configured project(s)`); } - function checkInferredProjectNumber(projectService: server.ProjectService, expected: number) { + function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) { assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); } @@ -131,6 +131,8 @@ namespace ts { checkFileNames("configuredProjects project, rootFileNames", project.getRootFiles(), expectedFiles); } + type TimeOutCallback = () => any; + class TestServerHost implements server.ServerHost { args: string[] = []; newLine: "\n"; @@ -138,6 +140,7 @@ namespace ts { private fs: ts.FileMap; private getCanonicalFileName: (s: string) => string; private toPath: (f: string) => Path; + private callbackQueue: TimeOutCallback[] = []; readonly watchedDirectories: Map<{ cb: DirectoryWatcherCallback, recursive: boolean }[]> = {}; readonly watchedFiles: Map = {}; @@ -222,12 +225,12 @@ namespace ts { } } - triggerFileWatcherCallback(fileName: string): void { + triggerFileWatcherCallback(fileName: string, removed?: boolean): void { const path = this.toPath(fileName); const callbacks = lookUp(this.watchedFiles, path); if (callbacks) { for (const callback of callbacks) { - callback(path, /*removed*/ true); + callback(path, removed); } } } @@ -248,8 +251,27 @@ namespace ts { } // TOOD: record and invoke callbacks to simulate timer events - readonly setTimeout = setTimeout; - readonly clearTimeout = (timeoutId: any): void => void 0; + readonly setTimeout = (callback: TimeOutCallback, time: number) => { + this.callbackQueue.push(callback); + return this.callbackQueue.length - 1; + }; + readonly clearTimeout = (timeoutId: any): void => { + if (typeof timeoutId === "number") { + this.callbackQueue.splice(timeoutId, 1); + } + }; + + checkTimeoutQueueLength(expected: number) { + assert.equal(this.callbackQueue.length, expected, `expected ${expected} timeout callbacks queued but found ${this.callbackQueue.length}.`); + } + + runQueuedTimeoutCallbacks() { + for (const callback of this.callbackQueue) { + callback(); + } + this.callbackQueue = []; + } + readonly readFile = (s: string) => (this.fs.get(this.toPath(s))).content; readonly resolvePath = (s: string) => s; readonly getExecutingFilePath = () => this.executingFilePath; @@ -292,8 +314,8 @@ namespace ts { const { configFileName } = projectService.openClientFile(appFile.path); assert(!configFileName, `should not find config, got: '${configFileName}`); - checkConfiguredProjectNumber(projectService, 0); - checkInferredProjectNumber(projectService, 1); + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 1); const project = projectService.inferredProjects[0]; @@ -331,8 +353,8 @@ namespace ts { assert(configFileName, "should find config file"); assert.isTrue(!configFileErrors, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); - checkInferredProjectNumber(projectService, 0); - checkConfiguredProjectNumber(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); const project = projectService.configuredProjects[0]; checkConfiguredProjectActualFiles(project, [file1.path, libFile.path, file2.path]); @@ -356,27 +378,28 @@ namespace ts { projectService.openClientFile(commonFile1.path); projectService.openClientFile(commonFile2.path); - checkInferredProjectNumber(projectService, 2); + checkNumberOfInferredProjects(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); + checkNumberOfInferredProjects(projectService, 1); + checkNumberOfConfiguredProjects(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); + + checkNumberOfInferredProjects(projectService, 2); + checkNumberOfConfiguredProjects(projectService, 0); checkWatchedDirectories(host, ["/a/b", "/a"]); }); - it("add new files to a configured project without file list", (done: () => void) => { + it("add new files to a configured project without file list", () => { const configFile: FileOrFolder = { path: "/a/b/tsconfig.json", content: `{}` @@ -385,7 +408,7 @@ namespace ts { const projectService = new server.ProjectService(host, nullLogger); projectService.openClientFile(commonFile1.path); checkWatchedDirectories(host, ["/a/b"]); - checkConfiguredProjectNumber(projectService, 1); + checkNumberOfConfiguredProjects(projectService, 1); const project = projectService.configuredProjects[0]; checkConfiguredProjectRootFiles(project, [commonFile1.path]); @@ -393,11 +416,9 @@ namespace ts { // add a new ts file host.reloadFS([commonFile1, commonFile2, libFile, configFile]); host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path); + host.runQueuedTimeoutCallbacks(); // 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); + checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); }); it("should ignore non-existing files specified in the config file", () => { @@ -416,13 +437,13 @@ namespace ts { projectService.openClientFile(commonFile1.path); projectService.openClientFile(commonFile2.path); - checkConfiguredProjectNumber(projectService, 1); + checkNumberOfConfiguredProjects(projectService, 1); const project = projectService.configuredProjects[0]; checkConfiguredProjectRootFiles(project, [commonFile1.path]); - checkInferredProjectNumber(projectService, 1); + checkNumberOfInferredProjects(projectService, 1); }); - it("handle recreated files correctly", (done: () => void) => { + it("handle recreated files correctly", () => { const configFile: FileOrFolder = { path: "/a/b/tsconfig.json", content: `{}` @@ -431,23 +452,120 @@ namespace ts { const projectService = new server.ProjectService(host, nullLogger); projectService.openClientFile(commonFile1.path); - checkConfiguredProjectNumber(projectService, 1); + checkNumberOfConfiguredProjects(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); + // delete commonFile2 + host.reloadFS([commonFile1, configFile]); + host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path); + host.runQueuedTimeoutCallbacks(); + checkConfiguredProjectRootFiles(project, [commonFile1.path]); + + // re-add commonFile2 + host.reloadFS([commonFile1, commonFile2, configFile]); + host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path); + host.runQueuedTimeoutCallbacks(); + checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); }); + + it("should create new inferred projects for files excluded from a configured project", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}", "${commonFile2.path}"] + }` + }; + const files = [commonFile1, commonFile2, configFile]; + const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", files); + const projectService = new server.ProjectService(host, nullLogger); + projectService.openClientFile(commonFile1.path); + + const project = projectService.configuredProjects[0]; + checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + configFile.content = `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}"] + }`; + host.reloadFS(files); + host.triggerFileWatcherCallback(configFile.path); + + checkNumberOfConfiguredProjects(projectService, 1); + checkConfiguredProjectRootFiles(project, [commonFile1.path]); + + projectService.openClientFile(commonFile2.path); + checkNumberOfInferredProjects(projectService, 1); + }); + + it("files explicitly excluded in config file", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "exclude": ["/a/c"] + }` + }; + const excludedFile1: FileOrFolder = { + path: "/a/c/excluedFile1.ts", + content: `let t = 1;` + }; + + const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, excludedFile1, configFile]); + const projectService = new server.ProjectService(host, nullLogger); + + projectService.openClientFile(commonFile1.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects[0]; + checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + projectService.openClientFile(excludedFile1.path); + checkNumberOfInferredProjects(projectService, 1); + }); + + it("should properly handle module resolution changes in config file", () => { + const file1: FileOrFolder = { + path: "/a/b/file1.ts", + content: `import { T } from "module1";` + } + const nodeModuleFile: FileOrFolder = { + path: "/a/b/node_modules/module1.ts", + content: `export interface T {}` + } + const classicModuleFile: FileOrFolder = { + path: "/a/module1.ts", + content: `export interface T {}` + } + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "moduleResolution": "node" + }, + "files": ["${file1.path}"] + }` + }; + const files = [file1, nodeModuleFile, classicModuleFile, configFile]; + const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", files); + const projectService = new server.ProjectService(host, nullLogger); + projectService.openClientFile(file1.path); + projectService.openClientFile(nodeModuleFile.path); + projectService.openClientFile(classicModuleFile.path); + + checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects[0]; + checkConfiguredProjectActualFiles(project, [file1.path, nodeModuleFile.path]); + checkNumberOfInferredProjects(projectService, 1); + + configFile.content = `{ + "compilerOptions": { + "moduleResolution": "classic" + }, + "files": ["${file1.path}"] + }`; + host.reloadFS(files); + host.triggerFileWatcherCallback(configFile.path); + checkConfiguredProjectActualFiles(project, [file1.path, classicModuleFile.path]); + checkNumberOfInferredProjects(projectService, 1); + }) }); } \ No newline at end of file