diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index f225b4cacf8..610551cdbb8 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -2613,11 +2613,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge return isSourceFileLevelDeclarationInSystemJsModule(targetDeclaration, /*isExported*/ true); } + function isNameOfExportedDeclarationInNonES6Module(node: Node): boolean { + if (modulekind === ModuleKind.System || node.kind !== SyntaxKind.Identifier || nodeIsSynthesized(node)) { + return false; + } + + return !exportEquals && exportSpecifiers && hasProperty(exportSpecifiers, (node).text); + } + function emitPrefixUnaryExpression(node: PrefixUnaryExpression) { - const exportChanged = (node.operator === SyntaxKind.PlusPlusToken || node.operator === SyntaxKind.MinusMinusToken) && + const isPlusPlusOrMinusMinus = (node.operator === SyntaxKind.PlusPlusToken + || node.operator === SyntaxKind.MinusMinusToken); + const externalExportChanged = isPlusPlusOrMinusMinus && isNameOfExportedSourceLevelDeclarationInSystemExternalModule(node.operand); - if (exportChanged) { + if (externalExportChanged) { // emit // ++x // as @@ -2626,6 +2636,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge emitNodeWithoutSourceMap(node.operand); write(`", `); } + const internalExportChanged = isPlusPlusOrMinusMinus && + isNameOfExportedDeclarationInNonES6Module(node.operand); + + if (internalExportChanged) { + emitAliasEqual( node.operand); + } write(tokenToString(node.operator)); // In some cases, we need to emit a space between the operator and the operand. One obvious case @@ -2651,14 +2667,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge } emit(node.operand); - if (exportChanged) { + if (externalExportChanged) { write(")"); } } function emitPostfixUnaryExpression(node: PostfixUnaryExpression) { - const exportChanged = isNameOfExportedSourceLevelDeclarationInSystemExternalModule(node.operand); - if (exportChanged) { + const externalExportChanged = isNameOfExportedSourceLevelDeclarationInSystemExternalModule(node.operand); + const internalExportChanged = isNameOfExportedDeclarationInNonES6Module(node.operand); + + if (externalExportChanged) { // export function returns the value that was passes as the second argument // however for postfix unary expressions result value should be the value before modification. // emit 'x++' as '(export('x', ++x) - 1)' and 'x--' as '(export('x', --x) + 1)' @@ -2676,6 +2694,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge write(") + 1)"); } } + else if (internalExportChanged) { + emitAliasEqual( node.operand); + emit(node.operand); + if (node.operator === SyntaxKind.PlusPlusToken) { + write(" += 1"); + } + else { + write(" -= 1"); + } + } else { emit(node.operand); write(tokenToString(node.operator)); @@ -2777,24 +2805,50 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge } } + function emitAliasEqual(name: Identifier): boolean { + for (const specifier of exportSpecifiers[name.text]) { + emitStart(specifier.name); + emitContainingModuleName(specifier); + if (languageVersion === ScriptTarget.ES3 && name.text === "default") { + write('["default"]'); + } + else { + write("."); + emitNodeWithCommentsAndWithoutSourcemap(specifier.name); + } + emitEnd(specifier.name); + write(" = "); + } + return true; + } + function emitBinaryExpression(node: BinaryExpression) { if (languageVersion < ScriptTarget.ES6 && node.operatorToken.kind === SyntaxKind.EqualsToken && (node.left.kind === SyntaxKind.ObjectLiteralExpression || node.left.kind === SyntaxKind.ArrayLiteralExpression)) { emitDestructuring(node, node.parent.kind === SyntaxKind.ExpressionStatement); } else { - const exportChanged = - node.operatorToken.kind >= SyntaxKind.FirstAssignment && - node.operatorToken.kind <= SyntaxKind.LastAssignment && + const isAssignment = isAssignmentOperator(node.operatorToken.kind); + + const externalExportChanged = isAssignment && isNameOfExportedSourceLevelDeclarationInSystemExternalModule(node.left); - if (exportChanged) { + if (externalExportChanged) { // emit assignment 'x y' as 'exports("x", x y)' write(`${exportFunctionForFile}("`); emitNodeWithoutSourceMap(node.left); write(`", `); } + const internalExportChanged = isAssignment && + isNameOfExportedDeclarationInNonES6Module(node.left); + + if (internalExportChanged) { + // export { foo } + // emit foo = 2 as exports.foo = foo = 2 + emitAliasEqual(node.left); + } + if (node.operatorToken.kind === SyntaxKind.AsteriskAsteriskToken || node.operatorToken.kind === SyntaxKind.AsteriskAsteriskEqualsToken) { // Downleveled emit exponentiation operator using Math.pow emitExponentiationOperator(node); @@ -2815,7 +2869,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge decreaseIndentIf(indentedBeforeOperator, indentedAfterOperator); } - if (exportChanged) { + if (externalExportChanged) { write(")"); } } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index bab554927e7..5b64d066da1 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/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/src/services/services.ts b/src/services/services.ts index 9932a136383..021cd2fe672 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/baselines/reference/es6ExportClauseWithAssignmentInEs5.js b/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.js new file mode 100644 index 00000000000..94bc5d9281f --- /dev/null +++ b/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.js @@ -0,0 +1,36 @@ +//// [server.ts] + +var foo = 2; +foo = 3; + +var baz = 3; +baz = 4; + +var buzz = 10; +buzz += 3; + +var bizz = 8; +bizz++; // compiles to exports.bizz = bizz += 1 +bizz--; // similarly +++bizz; // compiles to exports.bizz = ++bizz + +export { foo, baz, baz as quux, buzz, bizz }; + + +//// [server.js] +"use strict"; +var foo = 2; +exports.foo = foo; +exports.foo = foo = 3; +var baz = 3; +exports.baz = baz; +exports.quux = baz; +exports.baz = exports.quux = baz = 4; +var buzz = 10; +exports.buzz = buzz; +exports.buzz = buzz += 3; +var bizz = 8; +exports.bizz = bizz; +exports.bizz = bizz += 1; // compiles to exports.bizz = bizz += 1 +exports.bizz = bizz -= 1; // similarly +exports.bizz = ++bizz; // compiles to exports.bizz = ++bizz diff --git a/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.symbols b/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.symbols new file mode 100644 index 00000000000..6f8dbb01ec5 --- /dev/null +++ b/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.symbols @@ -0,0 +1,40 @@ +=== tests/cases/compiler/server.ts === + +var foo = 2; +>foo : Symbol(foo, Decl(server.ts, 1, 3)) + +foo = 3; +>foo : Symbol(foo, Decl(server.ts, 1, 3)) + +var baz = 3; +>baz : Symbol(baz, Decl(server.ts, 4, 3)) + +baz = 4; +>baz : Symbol(baz, Decl(server.ts, 4, 3)) + +var buzz = 10; +>buzz : Symbol(buzz, Decl(server.ts, 7, 3)) + +buzz += 3; +>buzz : Symbol(buzz, Decl(server.ts, 7, 3)) + +var bizz = 8; +>bizz : Symbol(bizz, Decl(server.ts, 10, 3)) + +bizz++; // compiles to exports.bizz = bizz += 1 +>bizz : Symbol(bizz, Decl(server.ts, 10, 3)) + +bizz--; // similarly +>bizz : Symbol(bizz, Decl(server.ts, 10, 3)) + +++bizz; // compiles to exports.bizz = ++bizz +>bizz : Symbol(bizz, Decl(server.ts, 10, 3)) + +export { foo, baz, baz as quux, buzz, bizz }; +>foo : Symbol(foo, Decl(server.ts, 15, 8)) +>baz : Symbol(baz, Decl(server.ts, 15, 13)) +>baz : Symbol(quux, Decl(server.ts, 15, 18)) +>quux : Symbol(quux, Decl(server.ts, 15, 18)) +>buzz : Symbol(buzz, Decl(server.ts, 15, 31)) +>bizz : Symbol(bizz, Decl(server.ts, 15, 37)) + diff --git a/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.types b/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.types new file mode 100644 index 00000000000..3b6752d89db --- /dev/null +++ b/tests/baselines/reference/es6ExportClauseWithAssignmentInEs5.types @@ -0,0 +1,53 @@ +=== tests/cases/compiler/server.ts === + +var foo = 2; +>foo : number +>2 : number + +foo = 3; +>foo = 3 : number +>foo : number +>3 : number + +var baz = 3; +>baz : number +>3 : number + +baz = 4; +>baz = 4 : number +>baz : number +>4 : number + +var buzz = 10; +>buzz : number +>10 : number + +buzz += 3; +>buzz += 3 : number +>buzz : number +>3 : number + +var bizz = 8; +>bizz : number +>8 : number + +bizz++; // compiles to exports.bizz = bizz += 1 +>bizz++ : number +>bizz : number + +bizz--; // similarly +>bizz-- : number +>bizz : number + +++bizz; // compiles to exports.bizz = ++bizz +>++bizz : number +>bizz : number + +export { foo, baz, baz as quux, buzz, bizz }; +>foo : number +>baz : number +>baz : number +>quux : number +>buzz : number +>bizz : number + diff --git a/tests/cases/compiler/es6ExportClauseWithAssignmentInEs5.ts b/tests/cases/compiler/es6ExportClauseWithAssignmentInEs5.ts new file mode 100644 index 00000000000..2e86ba7aeea --- /dev/null +++ b/tests/cases/compiler/es6ExportClauseWithAssignmentInEs5.ts @@ -0,0 +1,19 @@ +// @target: es5 +// @module: commonjs + +// @filename: server.ts +var foo = 2; +foo = 3; + +var baz = 3; +baz = 4; + +var buzz = 10; +buzz += 3; + +var bizz = 8; +bizz++; // compiles to exports.bizz = bizz += 1 +bizz--; // similarly +++bizz; // compiles to exports.bizz = ++bizz + +export { foo, baz, baz as quux, buzz, bizz }; diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index c69821ced5f..cf382d09463 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -107,6 +107,32 @@ namespace ts { } } + function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.configuredProjects.length, expected, `expected ${expected} configured project(s)`); + } + + function checkNumberOfInferredProjects(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); + } + + type TimeOutCallback = () => any; + class TestServerHost implements server.ServerHost { args: string[] = []; newLine: "\n"; @@ -114,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 = {}; @@ -188,6 +215,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, removed?: boolean): void { + const path = this.toPath(fileName); + const callbacks = lookUp(this.watchedFiles, path); + if (callbacks) { + for (const callback of callbacks) { + callback(path, removed); + } + } + } + watchFile(fileName: string, callback: FileWatcherCallback) { const path = this.toPath(fileName); const callbacks = lookUp(this.watchedFiles, path) || (this.watchedFiles[path] = []); @@ -204,8 +251,27 @@ 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 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; @@ -216,7 +282,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 +304,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 +314,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"); + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(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 +334,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 +346,226 @@ 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"); + checkNumberOfInferredProjects(projectService, 0); + checkNumberOfConfiguredProjects(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); + + checkNumberOfInferredProjects(projectService, 2); + checkWatchedDirectories(host, ["/a/b", "/a"]); + + // Add a tsconfig file + host.reloadFS(filesWithConfig); + host.triggerDirectoryWatcherCallback("/a/b", configFile.path); + + 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); + + checkNumberOfInferredProjects(projectService, 2); + checkNumberOfConfiguredProjects(projectService, 0); + checkWatchedDirectories(host, ["/a/b", "/a"]); + }); + + it("add new files to a configured project without file list", () => { + 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"]); + checkNumberOfConfiguredProjects(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); + host.runQueuedTimeoutCallbacks(); + // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. + checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + }); + + 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); + + checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects[0]; + checkConfiguredProjectRootFiles(project, [commonFile1.path]); + checkNumberOfInferredProjects(projectService, 1); + }); + + it("handle recreated files correctly", () => { + 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); + + checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects[0]; + checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + + // 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