1namespace ts.projectSystem { 2 describe("unittests:: tsserver:: Inferred projects", () => { 3 it("create inferred project", () => { 4 const appFile: File = { 5 path: `${tscWatch.projectRoot}/app.ts`, 6 content: ` 7 import {f} from "./module" 8 console.log(f) 9 ` 10 }; 11 12 const moduleFile: File = { 13 path: `${tscWatch.projectRoot}/module.d.ts`, 14 content: `export let x: number` 15 }; 16 const host = createServerHost([appFile, moduleFile, libFile]); 17 const projectService = createProjectService(host); 18 const { configFileName } = projectService.openClientFile(appFile.path); 19 20 assert(!configFileName, `should not find config, got: '${configFileName}`); 21 checkNumberOfConfiguredProjects(projectService, 0); 22 checkNumberOfInferredProjects(projectService, 1); 23 24 const project = projectService.inferredProjects[0]; 25 26 checkArray("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); 27 checkWatchedFiles(host, getConfigFilesToWatch(tscWatch.projectRoot).concat(libFile.path, moduleFile.path)); 28 checkWatchedDirectories(host, [tscWatch.projectRoot], /*recursive*/ false); 29 checkWatchedDirectories(host, [combinePaths(tscWatch.projectRoot, nodeModulesAtTypes)], /*recursive*/ true); 30 }); 31 32 it("should use only one inferred project if 'useOneInferredProject' is set", () => { 33 const file1 = { 34 path: `${tscWatch.projectRoot}/a/b/main.ts`, 35 content: "let x =1;" 36 }; 37 const configFile: File = { 38 path: `${tscWatch.projectRoot}/a/b/tsconfig.json`, 39 content: `{ 40 "compilerOptions": { 41 "target": "es6" 42 }, 43 "files": [ "main.ts" ] 44 }` 45 }; 46 const file2 = { 47 path: `${tscWatch.projectRoot}/a/c/main.ts`, 48 content: "let x =1;" 49 }; 50 51 const file3 = { 52 path: `${tscWatch.projectRoot}/a/d/main.ts`, 53 content: "let x =1;" 54 }; 55 56 const host = createServerHost([file1, file2, file3, libFile]); 57 const projectService = createProjectService(host, { useSingleInferredProject: true }); 58 projectService.openClientFile(file1.path); 59 projectService.openClientFile(file2.path); 60 projectService.openClientFile(file3.path); 61 62 checkNumberOfConfiguredProjects(projectService, 0); 63 checkNumberOfInferredProjects(projectService, 1); 64 checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path, libFile.path]); 65 66 67 host.writeFile(configFile.path, configFile.content); 68 host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles 69 checkNumberOfConfiguredProjects(projectService, 1); 70 checkNumberOfInferredProjects(projectService, 1); 71 checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); 72 }); 73 74 it("disable inferred project", () => { 75 const file1 = { 76 path: "/a/b/f1.ts", 77 content: "let x =1;" 78 }; 79 80 const host = createServerHost([file1]); 81 const projectService = createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true }); 82 83 projectService.openClientFile(file1.path, file1.content); 84 85 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 86 const proj = projectService.inferredProjects[0]; 87 assert.isDefined(proj); 88 89 assert.isFalse(proj.languageServiceEnabled); 90 }); 91 92 it("project settings for inferred projects", () => { 93 const file1 = { 94 path: "/a/b/app.ts", 95 content: `import {x} from "mod"` 96 }; 97 const modFile = { 98 path: "/a/mod.ts", 99 content: "export let x: number" 100 }; 101 const host = createServerHost([file1, modFile]); 102 const projectService = createProjectService(host); 103 104 projectService.openClientFile(file1.path); 105 projectService.openClientFile(modFile.path); 106 107 checkNumberOfProjects(projectService, { inferredProjects: 2 }); 108 const inferredProjects = projectService.inferredProjects.slice(); 109 checkProjectActualFiles(inferredProjects[0], [file1.path]); 110 checkProjectActualFiles(inferredProjects[1], [modFile.path]); 111 112 projectService.setCompilerOptionsForInferredProjects({ moduleResolution: ModuleResolutionKind.Classic }); 113 host.checkTimeoutQueueLengthAndRun(3); 114 checkNumberOfProjects(projectService, { inferredProjects: 2 }); 115 assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); 116 assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); 117 checkProjectActualFiles(inferredProjects[0], [file1.path, modFile.path]); 118 assert.isTrue(inferredProjects[1].isOrphan()); 119 }); 120 121 it("should support files without extensions", () => { 122 const f = { 123 path: "/a/compile", 124 content: "let x = 1" 125 }; 126 const host = createServerHost([f]); 127 const session = createSession(host); 128 session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{ 129 seq: 1, 130 type: "request", 131 command: "compilerOptionsForInferredProjects", 132 arguments: { 133 options: { 134 allowJs: true 135 } 136 } 137 }); 138 session.executeCommand(<server.protocol.OpenRequest>{ 139 seq: 2, 140 type: "request", 141 command: "open", 142 arguments: { 143 file: f.path, 144 fileContent: f.content, 145 scriptKindName: "JS" 146 } 147 }); 148 const projectService = session.getProjectService(); 149 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 150 checkProjectActualFiles(projectService.inferredProjects[0], [f.path]); 151 }); 152 153 it("inferred projects per project root", () => { 154 const file1 = { path: "/a/file1.ts", content: "let x = 1;", projectRootPath: "/a" }; 155 const file2 = { path: "/a/file2.ts", content: "let y = 2;", projectRootPath: "/a" }; 156 const file3 = { path: "/b/file2.ts", content: "let x = 3;", projectRootPath: "/b" }; 157 const file4 = { path: "/c/file3.ts", content: "let z = 4;" }; 158 const host = createServerHost([file1, file2, file3, file4]); 159 const session = createSession(host, { 160 useSingleInferredProject: true, 161 useInferredProjectPerProjectRoot: true 162 }); 163 session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{ 164 seq: 1, 165 type: "request", 166 command: CommandNames.CompilerOptionsForInferredProjects, 167 arguments: { 168 options: { 169 allowJs: true, 170 target: ScriptTarget.ESNext 171 } 172 } 173 }); 174 session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{ 175 seq: 2, 176 type: "request", 177 command: CommandNames.CompilerOptionsForInferredProjects, 178 arguments: { 179 options: { 180 allowJs: true, 181 target: ScriptTarget.ES2015 182 }, 183 projectRootPath: "/b" 184 } 185 }); 186 session.executeCommand(<server.protocol.OpenRequest>{ 187 seq: 3, 188 type: "request", 189 command: CommandNames.Open, 190 arguments: { 191 file: file1.path, 192 fileContent: file1.content, 193 scriptKindName: "JS", 194 projectRootPath: file1.projectRootPath 195 } 196 }); 197 session.executeCommand(<server.protocol.OpenRequest>{ 198 seq: 4, 199 type: "request", 200 command: CommandNames.Open, 201 arguments: { 202 file: file2.path, 203 fileContent: file2.content, 204 scriptKindName: "JS", 205 projectRootPath: file2.projectRootPath 206 } 207 }); 208 session.executeCommand(<server.protocol.OpenRequest>{ 209 seq: 5, 210 type: "request", 211 command: CommandNames.Open, 212 arguments: { 213 file: file3.path, 214 fileContent: file3.content, 215 scriptKindName: "JS", 216 projectRootPath: file3.projectRootPath 217 } 218 }); 219 session.executeCommand(<server.protocol.OpenRequest>{ 220 seq: 6, 221 type: "request", 222 command: CommandNames.Open, 223 arguments: { 224 file: file4.path, 225 fileContent: file4.content, 226 scriptKindName: "JS" 227 } 228 }); 229 230 const projectService = session.getProjectService(); 231 checkNumberOfProjects(projectService, { inferredProjects: 3 }); 232 checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); 233 checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]); 234 checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); 235 assert.equal(projectService.inferredProjects[0].getCompilationSettings().target, ScriptTarget.ESNext); 236 assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ScriptTarget.ESNext); 237 assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ScriptTarget.ES2015); 238 }); 239 240 function checkInferredProject(inferredProject: server.InferredProject, actualFiles: File[], target: ScriptTarget) { 241 checkProjectActualFiles(inferredProject, actualFiles.map(f => f.path)); 242 assert.equal(inferredProject.getCompilationSettings().target, target); 243 } 244 245 function verifyProjectRootWithCaseSensitivity(useCaseSensitiveFileNames: boolean) { 246 const files: [File, File, File, File] = [ 247 { path: "/a/file1.ts", content: "let x = 1;" }, 248 { path: "/A/file2.ts", content: "let y = 2;" }, 249 { path: "/b/file2.ts", content: "let x = 3;" }, 250 { path: "/c/file3.ts", content: "let z = 4;" } 251 ]; 252 const host = createServerHost(files, { useCaseSensitiveFileNames }); 253 const projectService = createProjectService(host, { useSingleInferredProject: true, useInferredProjectPerProjectRoot: true }); 254 projectService.setCompilerOptionsForInferredProjects({ 255 allowJs: true, 256 target: ScriptTarget.ESNext 257 }); 258 projectService.setCompilerOptionsForInferredProjects({ 259 allowJs: true, 260 target: ScriptTarget.ES2015 261 }, "/a"); 262 263 openClientFiles(["/a", "/a", "/b", undefined]); 264 verifyInferredProjectsState([ 265 [[files[3]], ScriptTarget.ESNext], 266 [[files[0], files[1]], ScriptTarget.ES2015], 267 [[files[2]], ScriptTarget.ESNext] 268 ]); 269 closeClientFiles(); 270 271 openClientFiles(["/a", "/A", "/b", undefined]); 272 if (useCaseSensitiveFileNames) { 273 verifyInferredProjectsState([ 274 [[files[3]], ScriptTarget.ESNext], 275 [[files[0]], ScriptTarget.ES2015], 276 [[files[1]], ScriptTarget.ESNext], 277 [[files[2]], ScriptTarget.ESNext] 278 ]); 279 } 280 else { 281 verifyInferredProjectsState([ 282 [[files[3]], ScriptTarget.ESNext], 283 [[files[0], files[1]], ScriptTarget.ES2015], 284 [[files[2]], ScriptTarget.ESNext] 285 ]); 286 } 287 closeClientFiles(); 288 289 projectService.setCompilerOptionsForInferredProjects({ 290 allowJs: true, 291 target: ScriptTarget.ES2017 292 }, "/A"); 293 294 openClientFiles(["/a", "/a", "/b", undefined]); 295 verifyInferredProjectsState([ 296 [[files[3]], ScriptTarget.ESNext], 297 [[files[0], files[1]], useCaseSensitiveFileNames ? ScriptTarget.ES2015 : ScriptTarget.ES2017], 298 [[files[2]], ScriptTarget.ESNext] 299 ]); 300 closeClientFiles(); 301 302 openClientFiles(["/a", "/A", "/b", undefined]); 303 if (useCaseSensitiveFileNames) { 304 verifyInferredProjectsState([ 305 [[files[3]], ScriptTarget.ESNext], 306 [[files[0]], ScriptTarget.ES2015], 307 [[files[1]], ScriptTarget.ES2017], 308 [[files[2]], ScriptTarget.ESNext] 309 ]); 310 } 311 else { 312 verifyInferredProjectsState([ 313 [[files[3]], ScriptTarget.ESNext], 314 [[files[0], files[1]], ScriptTarget.ES2017], 315 [[files[2]], ScriptTarget.ESNext] 316 ]); 317 } 318 closeClientFiles(); 319 320 function openClientFiles(projectRoots: [string | undefined, string | undefined, string | undefined, string | undefined]) { 321 files.forEach((file, index) => { 322 projectService.openClientFile(file.path, file.content, ScriptKind.JS, projectRoots[index]); 323 }); 324 } 325 326 function closeClientFiles() { 327 files.forEach(file => projectService.closeClientFile(file.path)); 328 } 329 330 function verifyInferredProjectsState(expected: [File[], ScriptTarget][]) { 331 checkNumberOfProjects(projectService, { inferredProjects: expected.length }); 332 projectService.inferredProjects.forEach((p, index) => { 333 const [actualFiles, target] = expected[index]; 334 checkInferredProject(p, actualFiles, target); 335 }); 336 } 337 } 338 339 it("inferred projects per project root with case sensitive system", () => { 340 verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ true); 341 }); 342 343 it("inferred projects per project root with case insensitive system", () => { 344 verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ false); 345 }); 346 347 it("should still retain configured project created while opening the file", () => { 348 const appFile: File = { 349 path: `${tscWatch.projectRoot}/app.ts`, 350 content: `const app = 20;` 351 }; 352 const config: File = { 353 path: `${tscWatch.projectRoot}/tsconfig.json`, 354 content: "{}" 355 }; 356 const jsFile1: File = { 357 path: `${tscWatch.projectRoot}/jsFile1.js`, 358 content: `const jsFile1 = 10;` 359 }; 360 const jsFile2: File = { 361 path: `${tscWatch.projectRoot}/jsFile2.js`, 362 content: `const jsFile2 = 10;` 363 }; 364 const host = createServerHost([appFile, libFile, config, jsFile1, jsFile2]); 365 const projectService = createProjectService(host); 366 const originalSet = projectService.configuredProjects.set; 367 const originalDelete = projectService.configuredProjects.delete; 368 const configuredCreated = new Map<string, true>(); 369 const configuredRemoved = new Map<string, true>(); 370 projectService.configuredProjects.set = (key, value) => { 371 assert.isFalse(configuredCreated.has(key)); 372 configuredCreated.set(key, true); 373 return originalSet.call(projectService.configuredProjects, key, value); 374 }; 375 projectService.configuredProjects.delete = key => { 376 assert.isFalse(configuredRemoved.has(key)); 377 configuredRemoved.set(key, true); 378 return originalDelete.call(projectService.configuredProjects, key); 379 }; 380 381 // Do not remove config project when opening jsFile that is not present as part of config project 382 projectService.openClientFile(jsFile1.path); 383 checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); 384 checkProjectActualFiles(projectService.inferredProjects[0], [jsFile1.path, libFile.path]); 385 const project = projectService.configuredProjects.get(config.path)!; 386 checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]); 387 checkConfiguredProjectCreatedAndNotDeleted(); 388 389 // Do not remove config project when opening jsFile that is not present as part of config project 390 projectService.closeClientFile(jsFile1.path); 391 checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); 392 projectService.openClientFile(jsFile2.path); 393 checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); 394 checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]); 395 checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]); 396 checkConfiguredProjectNotCreatedAndNotDeleted(); 397 398 // Do not remove config project when opening jsFile that is not present as part of config project 399 projectService.openClientFile(jsFile1.path); 400 checkNumberOfProjects(projectService, { inferredProjects: 2, configuredProjects: 1 }); 401 checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]); 402 checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, libFile.path]); 403 checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]); 404 checkConfiguredProjectNotCreatedAndNotDeleted(); 405 406 // When opening file that doesnt fall back to the config file, we remove the config project 407 projectService.openClientFile(libFile.path); 408 checkNumberOfProjects(projectService, { inferredProjects: 2 }); 409 checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]); 410 checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, libFile.path]); 411 checkConfiguredProjectNotCreatedButDeleted(); 412 413 function checkConfiguredProjectCreatedAndNotDeleted() { 414 assert.equal(configuredCreated.size, 1); 415 assert.isTrue(configuredCreated.has(config.path)); 416 assert.equal(configuredRemoved.size, 0); 417 configuredCreated.clear(); 418 } 419 420 function checkConfiguredProjectNotCreatedAndNotDeleted() { 421 assert.equal(configuredCreated.size, 0); 422 assert.equal(configuredRemoved.size, 0); 423 } 424 425 function checkConfiguredProjectNotCreatedButDeleted() { 426 assert.equal(configuredCreated.size, 0); 427 assert.equal(configuredRemoved.size, 1); 428 assert.isTrue(configuredRemoved.has(config.path)); 429 configuredRemoved.clear(); 430 } 431 }); 432 433 it("regression test - should infer typeAcquisition for inferred projects when set undefined", () => { 434 const file1 = { path: "/a/file1.js", content: "" }; 435 const host = createServerHost([file1]); 436 437 const projectService = createProjectService(host); 438 439 projectService.openClientFile(file1.path); 440 441 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 442 const inferredProject = projectService.inferredProjects[0]; 443 checkProjectActualFiles(inferredProject, [file1.path]); 444 inferredProject.setTypeAcquisition(undefined); 445 446 const expected = { 447 enable: true, 448 include: [], 449 exclude: [] 450 }; 451 assert.deepEqual(inferredProject.getTypeAcquisition(), expected, "typeAcquisition should be inferred for inferred projects"); 452 }); 453 454 it("Setting compiler options for inferred projects when there are no open files should not schedule any refresh", () => { 455 const host = createServerHost([commonFile1, libFile]); 456 const projectService = createProjectService(host); 457 projectService.setCompilerOptionsForInferredProjects({ 458 allowJs: true, 459 target: ScriptTarget.ES2015 460 }); 461 host.checkTimeoutQueueLength(0); 462 }); 463 }); 464} 465