1namespace ts.projectSystem { 2 describe("unittests:: tsserver:: ConfiguredProjects", () => { 3 it("create configured project without file list", () => { 4 const configFile: File = { 5 path: "/a/b/tsconfig.json", 6 content: ` 7 { 8 "compilerOptions": {}, 9 "exclude": [ 10 "e" 11 ] 12 }` 13 }; 14 const file1: File = { 15 path: "/a/b/c/f1.ts", 16 content: "let x = 1" 17 }; 18 const file2: File = { 19 path: "/a/b/d/f2.ts", 20 content: "let y = 1" 21 }; 22 const file3: File = { 23 path: "/a/b/e/f3.ts", 24 content: "let z = 1" 25 }; 26 27 const host = createServerHost([configFile, libFile, file1, file2, file3]); 28 const projectService = createProjectService(host); 29 const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); 30 31 assert(configFileName, "should find config file"); 32 assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); 33 checkNumberOfInferredProjects(projectService, 0); 34 checkNumberOfConfiguredProjects(projectService, 1); 35 36 const project = configuredProjectAt(projectService, 0); 37 checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); 38 checkProjectRootFiles(project, [file1.path, file2.path]); 39 // watching all files except one that was open 40 checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); 41 const configFileDirectory = getDirectoryPath(configFile.path); 42 checkWatchedDirectories(host, [configFileDirectory, combinePaths(configFileDirectory, nodeModulesAtTypes)], /*recursive*/ true); 43 }); 44 45 it("create configured project with the file list", () => { 46 const configFile: File = { 47 path: "/a/b/tsconfig.json", 48 content: ` 49 { 50 "compilerOptions": {}, 51 "include": ["*.ts"] 52 }` 53 }; 54 const file1: File = { 55 path: "/a/b/f1.ts", 56 content: "let x = 1" 57 }; 58 const file2: File = { 59 path: "/a/b/f2.ts", 60 content: "let y = 1" 61 }; 62 const file3: File = { 63 path: "/a/b/c/f3.ts", 64 content: "let z = 1" 65 }; 66 67 const host = createServerHost([configFile, libFile, file1, file2, file3]); 68 const projectService = createProjectService(host); 69 const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); 70 71 assert(configFileName, "should find config file"); 72 assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); 73 checkNumberOfInferredProjects(projectService, 0); 74 checkNumberOfConfiguredProjects(projectService, 1); 75 76 const project = configuredProjectAt(projectService, 0); 77 checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); 78 checkProjectRootFiles(project, [file1.path, file2.path]); 79 // watching all files except one that was open 80 checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); 81 checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ false); 82 }); 83 84 it("add and then remove a config file in a folder with loose files", () => { 85 const configFile: File = { 86 path: `${tscWatch.projectRoot}/tsconfig.json`, 87 content: `{ 88 "files": ["commonFile1.ts"] 89 }` 90 }; 91 const commonFile1: File = { 92 path: `${tscWatch.projectRoot}/commonFile1.ts`, 93 content: "let x = 1" 94 }; 95 const commonFile2: File = { 96 path: `${tscWatch.projectRoot}/commonFile2.ts`, 97 content: "let y = 1" 98 }; 99 100 const host = createServerHost([libFile, commonFile1, commonFile2]); 101 102 const projectService = createProjectService(host); 103 projectService.openClientFile(commonFile1.path); 104 projectService.openClientFile(commonFile2.path); 105 106 projectService.checkNumberOfProjects({ inferredProjects: 2 }); 107 checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, libFile.path]); 108 checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); 109 110 const watchedFiles = getConfigFilesToWatch(tscWatch.projectRoot).concat(libFile.path); 111 checkWatchedFiles(host, watchedFiles); 112 113 // Add a tsconfig file 114 host.writeFile(configFile.path, configFile.content); 115 host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles 116 117 projectService.checkNumberOfProjects({ inferredProjects: 2, configuredProjects: 1 }); 118 assert.isTrue(projectService.inferredProjects[0].isOrphan()); 119 checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); 120 checkProjectActualFiles(projectService.configuredProjects.get(configFile.path)!, [libFile.path, commonFile1.path, configFile.path]); 121 122 checkWatchedFiles(host, watchedFiles); 123 124 // remove the tsconfig file 125 host.deleteFile(configFile.path); 126 127 projectService.checkNumberOfProjects({ inferredProjects: 2 }); 128 assert.isTrue(projectService.inferredProjects[0].isOrphan()); 129 checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); 130 131 host.checkTimeoutQueueLengthAndRun(1); // Refresh inferred projects 132 133 projectService.checkNumberOfProjects({ inferredProjects: 2 }); 134 checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, libFile.path]); 135 checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); 136 checkWatchedFiles(host, watchedFiles); 137 }); 138 139 it("add new files to a configured project without file list", () => { 140 const configFile: File = { 141 path: "/a/b/tsconfig.json", 142 content: `{}` 143 }; 144 const host = createServerHost([commonFile1, libFile, configFile]); 145 const projectService = createProjectService(host); 146 projectService.openClientFile(commonFile1.path); 147 const configFileDir = getDirectoryPath(configFile.path); 148 checkWatchedDirectories(host, [configFileDir, combinePaths(configFileDir, nodeModulesAtTypes)], /*recursive*/ true); 149 checkNumberOfConfiguredProjects(projectService, 1); 150 151 const project = configuredProjectAt(projectService, 0); 152 checkProjectRootFiles(project, [commonFile1.path]); 153 154 // add a new ts file 155 host.writeFile(commonFile2.path, commonFile2.content); 156 host.checkTimeoutQueueLengthAndRun(2); 157 // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. 158 checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); 159 }); 160 161 it("should ignore non-existing files specified in the config file", () => { 162 const configFile: File = { 163 path: "/a/b/tsconfig.json", 164 content: `{ 165 "compilerOptions": {}, 166 "files": [ 167 "commonFile1.ts", 168 "commonFile3.ts" 169 ] 170 }` 171 }; 172 const host = createServerHost([commonFile1, commonFile2, configFile]); 173 const projectService = createProjectService(host); 174 projectService.openClientFile(commonFile1.path); 175 projectService.openClientFile(commonFile2.path); 176 177 checkNumberOfConfiguredProjects(projectService, 1); 178 const project = configuredProjectAt(projectService, 0); 179 checkProjectRootFiles(project, [commonFile1.path]); 180 checkNumberOfInferredProjects(projectService, 1); 181 }); 182 183 it("handle recreated files correctly", () => { 184 const configFile: File = { 185 path: "/a/b/tsconfig.json", 186 content: `{}` 187 }; 188 const host = createServerHost([commonFile1, commonFile2, configFile]); 189 const projectService = createProjectService(host); 190 projectService.openClientFile(commonFile1.path); 191 192 checkNumberOfConfiguredProjects(projectService, 1); 193 const project = configuredProjectAt(projectService, 0); 194 checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); 195 196 // delete commonFile2 197 host.deleteFile(commonFile2.path); 198 host.checkTimeoutQueueLengthAndRun(2); 199 checkProjectRootFiles(project, [commonFile1.path]); 200 201 // re-add commonFile2 202 host.writeFile(commonFile2.path, commonFile2.content); 203 host.checkTimeoutQueueLengthAndRun(2); 204 checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); 205 }); 206 207 it("files explicitly excluded in config file", () => { 208 const configFile: File = { 209 path: "/a/b/tsconfig.json", 210 content: `{ 211 "compilerOptions": {}, 212 "exclude": ["/a/c"] 213 }` 214 }; 215 const excludedFile1: File = { 216 path: "/a/c/excluedFile1.ts", 217 content: `let t = 1;` 218 }; 219 220 const host = createServerHost([commonFile1, commonFile2, excludedFile1, configFile]); 221 const projectService = createProjectService(host); 222 223 projectService.openClientFile(commonFile1.path); 224 checkNumberOfConfiguredProjects(projectService, 1); 225 const project = configuredProjectAt(projectService, 0); 226 checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); 227 projectService.openClientFile(excludedFile1.path); 228 checkNumberOfInferredProjects(projectService, 1); 229 }); 230 231 it("should properly handle module resolution changes in config file", () => { 232 const file1: File = { 233 path: "/a/b/file1.ts", 234 content: `import { T } from "module1";` 235 }; 236 const nodeModuleFile: File = { 237 path: "/a/b/node_modules/module1.ts", 238 content: `export interface T {}` 239 }; 240 const classicModuleFile: File = { 241 path: "/a/module1.ts", 242 content: `export interface T {}` 243 }; 244 const randomFile: File = { 245 path: "/a/file1.ts", 246 content: `export interface T {}` 247 }; 248 const configFile: File = { 249 path: "/a/b/tsconfig.json", 250 content: `{ 251 "compilerOptions": { 252 "moduleResolution": "node" 253 }, 254 "files": ["${file1.path}"] 255 }` 256 }; 257 const files = [file1, nodeModuleFile, classicModuleFile, configFile, randomFile]; 258 const host = createServerHost(files); 259 const projectService = createProjectService(host); 260 projectService.openClientFile(file1.path); 261 projectService.openClientFile(nodeModuleFile.path); 262 projectService.openClientFile(classicModuleFile.path); 263 264 checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); 265 const project = configuredProjectAt(projectService, 0); 266 const inferredProject0 = projectService.inferredProjects[0]; 267 checkProjectActualFiles(project, [file1.path, nodeModuleFile.path, configFile.path]); 268 checkProjectActualFiles(projectService.inferredProjects[0], [classicModuleFile.path]); 269 270 host.writeFile(configFile.path, `{ 271 "compilerOptions": { 272 "moduleResolution": "classic" 273 }, 274 "files": ["${file1.path}"] 275 }`); 276 host.checkTimeoutQueueLengthAndRun(2); 277 278 checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); // will not remove project 1 279 checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); 280 assert.strictEqual(projectService.inferredProjects[0], inferredProject0); 281 assert.isTrue(projectService.inferredProjects[0].isOrphan()); 282 const inferredProject1 = projectService.inferredProjects[1]; 283 checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); 284 285 // Open random file and it will reuse first inferred project 286 projectService.openClientFile(randomFile.path); 287 checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); 288 checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); 289 assert.strictEqual(projectService.inferredProjects[0], inferredProject0); 290 checkProjectActualFiles(projectService.inferredProjects[0], [randomFile.path]); // Reuses first inferred project 291 assert.strictEqual(projectService.inferredProjects[1], inferredProject1); 292 checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); 293 }); 294 295 it("should keep the configured project when the opened file is referenced by the project but not its root", () => { 296 const file1: File = { 297 path: "/a/b/main.ts", 298 content: "import { objA } from './obj-a';" 299 }; 300 const file2: File = { 301 path: "/a/b/obj-a.ts", 302 content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` 303 }; 304 const configFile: File = { 305 path: "/a/b/tsconfig.json", 306 content: `{ 307 "compilerOptions": { 308 "target": "es6" 309 }, 310 "files": [ "main.ts" ] 311 }` 312 }; 313 const host = createServerHost([file1, file2, configFile]); 314 const projectService = createProjectService(host); 315 projectService.openClientFile(file1.path); 316 projectService.closeClientFile(file1.path); 317 projectService.openClientFile(file2.path); 318 checkNumberOfConfiguredProjects(projectService, 1); 319 checkNumberOfInferredProjects(projectService, 0); 320 }); 321 322 it("should keep the configured project when the opened file is referenced by the project but not its root", () => { 323 const file1: File = { 324 path: "/a/b/main.ts", 325 content: "import { objA } from './obj-a';" 326 }; 327 const file2: File = { 328 path: "/a/b/obj-a.ts", 329 content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` 330 }; 331 const configFile: File = { 332 path: "/a/b/tsconfig.json", 333 content: `{ 334 "compilerOptions": { 335 "target": "es6" 336 }, 337 "files": [ "main.ts" ] 338 }` 339 }; 340 const host = createServerHost([file1, file2, configFile]); 341 const projectService = createProjectService(host); 342 projectService.openClientFile(file1.path); 343 projectService.closeClientFile(file1.path); 344 projectService.openClientFile(file2.path); 345 checkNumberOfConfiguredProjects(projectService, 1); 346 checkNumberOfInferredProjects(projectService, 0); 347 }); 348 349 it("should tolerate config file errors and still try to build a project", () => { 350 const configFile: File = { 351 path: "/a/b/tsconfig.json", 352 content: `{ 353 "compilerOptions": { 354 "target": "es6", 355 "allowAnything": true 356 }, 357 "someOtherProperty": {} 358 }` 359 }; 360 const host = createServerHost([commonFile1, commonFile2, libFile, configFile]); 361 const projectService = createProjectService(host); 362 projectService.openClientFile(commonFile1.path); 363 checkNumberOfConfiguredProjects(projectService, 1); 364 checkProjectRootFiles(configuredProjectAt(projectService, 0), [commonFile1.path, commonFile2.path]); 365 }); 366 367 it("should reuse same project if file is opened from the configured project that has no open files", () => { 368 const file1 = { 369 path: "/a/b/main.ts", 370 content: "let x =1;" 371 }; 372 const file2 = { 373 path: "/a/b/main2.ts", 374 content: "let y =1;" 375 }; 376 const configFile: File = { 377 path: "/a/b/tsconfig.json", 378 content: `{ 379 "compilerOptions": { 380 "target": "es6" 381 }, 382 "files": [ "main.ts", "main2.ts" ] 383 }` 384 }; 385 const host = createServerHost([file1, file2, configFile, libFile]); 386 const projectService = createProjectService(host, { useSingleInferredProject: true }); 387 projectService.openClientFile(file1.path); 388 checkNumberOfConfiguredProjects(projectService, 1); 389 const project = projectService.configuredProjects.get(configFile.path)!; 390 assert.isTrue(project.hasOpenRef()); // file1 391 392 projectService.closeClientFile(file1.path); 393 checkNumberOfConfiguredProjects(projectService, 1); 394 assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); 395 assert.isFalse(project.hasOpenRef()); // No open files 396 assert.isFalse(project.isClosed()); 397 398 projectService.openClientFile(file2.path); 399 checkNumberOfConfiguredProjects(projectService, 1); 400 assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); 401 assert.isTrue(project.hasOpenRef()); // file2 402 assert.isFalse(project.isClosed()); 403 }); 404 405 it("should not close configured project after closing last open file, but should be closed on next file open if its not the file from same project", () => { 406 const file1 = { 407 path: "/a/b/main.ts", 408 content: "let x =1;" 409 }; 410 const configFile: File = { 411 path: "/a/b/tsconfig.json", 412 content: `{ 413 "compilerOptions": { 414 "target": "es6" 415 }, 416 "files": [ "main.ts" ] 417 }` 418 }; 419 const host = createServerHost([file1, configFile, libFile]); 420 const projectService = createProjectService(host, { useSingleInferredProject: true }); 421 projectService.openClientFile(file1.path); 422 checkNumberOfConfiguredProjects(projectService, 1); 423 const project = projectService.configuredProjects.get(configFile.path)!; 424 assert.isTrue(project.hasOpenRef()); // file1 425 426 projectService.closeClientFile(file1.path); 427 checkNumberOfConfiguredProjects(projectService, 1); 428 assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); 429 assert.isFalse(project.hasOpenRef()); // No files 430 assert.isFalse(project.isClosed()); 431 432 projectService.openClientFile(libFile.path); 433 checkNumberOfConfiguredProjects(projectService, 0); 434 assert.isFalse(project.hasOpenRef()); // No files + project closed 435 assert.isTrue(project.isClosed()); 436 }); 437 438 it("open file become a part of configured project if it is referenced from root file", () => { 439 const file1 = { 440 path: `${tscWatch.projectRoot}/a/b/f1.ts`, 441 content: "export let x = 5" 442 }; 443 const file2 = { 444 path: `${tscWatch.projectRoot}/a/c/f2.ts`, 445 content: `import {x} from "../b/f1"` 446 }; 447 const file3 = { 448 path: `${tscWatch.projectRoot}/a/c/f3.ts`, 449 content: "export let y = 1" 450 }; 451 const configFile = { 452 path: `${tscWatch.projectRoot}/a/c/tsconfig.json`, 453 content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) 454 }; 455 456 const host = createServerHost([file1, file2, file3]); 457 const projectService = createProjectService(host); 458 459 projectService.openClientFile(file1.path); 460 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 461 checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); 462 463 projectService.openClientFile(file3.path); 464 checkNumberOfProjects(projectService, { inferredProjects: 2 }); 465 checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); 466 checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); 467 468 host.writeFile(configFile.path, configFile.content); 469 host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles 470 checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); 471 checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, file3.path, configFile.path]); 472 assert.isTrue(projectService.inferredProjects[0].isOrphan()); 473 assert.isTrue(projectService.inferredProjects[1].isOrphan()); 474 }); 475 476 it("can correctly update configured project when set of root files has changed (new file on disk)", () => { 477 const file1 = { 478 path: "/a/b/f1.ts", 479 content: "let x = 1" 480 }; 481 const file2 = { 482 path: "/a/b/f2.ts", 483 content: "let y = 1" 484 }; 485 const configFile = { 486 path: "/a/b/tsconfig.json", 487 content: JSON.stringify({ compilerOptions: {} }) 488 }; 489 490 const host = createServerHost([file1, configFile]); 491 const projectService = createProjectService(host); 492 493 projectService.openClientFile(file1.path); 494 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 495 checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); 496 497 host.writeFile(file2.path, file2.content); 498 499 host.checkTimeoutQueueLengthAndRun(2); 500 501 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 502 checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); 503 }); 504 505 it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { 506 const file1 = { 507 path: "/a/b/f1.ts", 508 content: "let x = 1" 509 }; 510 const file2 = { 511 path: "/a/b/f2.ts", 512 content: "let y = 1" 513 }; 514 const configFile = { 515 path: "/a/b/tsconfig.json", 516 content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) 517 }; 518 519 const host = createServerHost([file1, file2, configFile]); 520 const projectService = createProjectService(host); 521 522 projectService.openClientFile(file1.path); 523 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 524 checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); 525 526 host.writeFile(configFile.path, JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] })); 527 528 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 529 host.checkTimeoutQueueLengthAndRun(2); 530 checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); 531 }); 532 533 it("can update configured project when set of root files was not changed", () => { 534 const file1 = { 535 path: "/a/b/f1.ts", 536 content: "let x = 1" 537 }; 538 const file2 = { 539 path: "/a/b/f2.ts", 540 content: "let y = 1" 541 }; 542 const configFile = { 543 path: "/a/b/tsconfig.json", 544 content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) 545 }; 546 547 const host = createServerHost([file1, file2, configFile]); 548 const projectService = createProjectService(host); 549 550 projectService.openClientFile(file1.path); 551 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 552 checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, configFile.path]); 553 554 host.writeFile(configFile.path, JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] })); 555 556 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 557 checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); 558 }); 559 560 it("Open ref of configured project when open file gets added to the project as part of configured file update", () => { 561 const file1: File = { 562 path: "/a/b/src/file1.ts", 563 content: "let x = 1;" 564 }; 565 const file2: File = { 566 path: "/a/b/src/file2.ts", 567 content: "let y = 1;" 568 }; 569 const file3: File = { 570 path: "/a/b/file3.ts", 571 content: "let z = 1;" 572 }; 573 const file4: File = { 574 path: "/a/file4.ts", 575 content: "let z = 1;" 576 }; 577 const configFile = { 578 path: "/a/b/tsconfig.json", 579 content: JSON.stringify({ files: ["src/file1.ts", "file3.ts"] }) 580 }; 581 582 const files = [file1, file2, file3, file4]; 583 const host = createServerHost(files.concat(configFile)); 584 const projectService = createProjectService(host); 585 586 projectService.openClientFile(file1.path); 587 projectService.openClientFile(file2.path); 588 projectService.openClientFile(file3.path); 589 projectService.openClientFile(file4.path); 590 591 const infos = files.map(file => projectService.getScriptInfoForPath(file.path as Path)!); 592 checkOpenFiles(projectService, files); 593 checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); 594 const configProject1 = projectService.configuredProjects.get(configFile.path)!; 595 assert.isTrue(configProject1.hasOpenRef()); // file1 and file3 596 checkProjectActualFiles(configProject1, [file1.path, file3.path, configFile.path]); 597 const inferredProject1 = projectService.inferredProjects[0]; 598 checkProjectActualFiles(inferredProject1, [file2.path]); 599 const inferredProject2 = projectService.inferredProjects[1]; 600 checkProjectActualFiles(inferredProject2, [file4.path]); 601 602 host.writeFile(configFile.path, "{}"); 603 host.runQueuedTimeoutCallbacks(); 604 605 verifyScriptInfos(); 606 checkOpenFiles(projectService, files); 607 verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file1, file2, file3 608 assert.isTrue(projectService.inferredProjects[0].isOrphan()); 609 const inferredProject3 = projectService.inferredProjects[1]; 610 checkProjectActualFiles(inferredProject3, [file4.path]); 611 assert.strictEqual(inferredProject3, inferredProject2); 612 613 projectService.closeClientFile(file1.path); 614 projectService.closeClientFile(file2.path); 615 projectService.closeClientFile(file4.path); 616 617 verifyScriptInfos(); 618 checkOpenFiles(projectService, [file3]); 619 verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file3 620 assert.isTrue(projectService.inferredProjects[0].isOrphan()); 621 assert.isTrue(projectService.inferredProjects[1].isOrphan()); 622 623 projectService.openClientFile(file4.path); 624 verifyScriptInfos(); 625 checkOpenFiles(projectService, [file3, file4]); 626 verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 1); // file3 627 const inferredProject4 = projectService.inferredProjects[0]; 628 checkProjectActualFiles(inferredProject4, [file4.path]); 629 630 projectService.closeClientFile(file3.path); 631 verifyScriptInfos(); 632 checkOpenFiles(projectService, [file4]); 633 verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ false, 1); // No open files 634 const inferredProject5 = projectService.inferredProjects[0]; 635 checkProjectActualFiles(inferredProject4, [file4.path]); 636 assert.strictEqual(inferredProject5, inferredProject4); 637 638 const file5: File = { 639 path: "/file5.ts", 640 content: "let zz = 1;" 641 }; 642 host.writeFile(file5.path, file5.content); 643 projectService.openClientFile(file5.path); 644 verifyScriptInfosAreUndefined([file1, file2, file3]); 645 assert.strictEqual(projectService.getScriptInfoForPath(file4.path as Path), find(infos, info => info.path === file4.path)); 646 assert.isDefined(projectService.getScriptInfoForPath(file5.path as Path)); 647 checkOpenFiles(projectService, [file4, file5]); 648 checkNumberOfProjects(projectService, { inferredProjects: 2 }); 649 checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); 650 checkProjectActualFiles(projectService.inferredProjects[1], [file5.path]); 651 652 function verifyScriptInfos() { 653 infos.forEach(info => assert.strictEqual(projectService.getScriptInfoForPath(info.path), info)); 654 } 655 656 function verifyScriptInfosAreUndefined(files: File[]) { 657 for (const file of files) { 658 assert.isUndefined(projectService.getScriptInfoForPath(file.path as Path)); 659 } 660 } 661 662 function verifyConfiguredProjectStateAfterUpdate(hasOpenRef: boolean, inferredProjects: number) { 663 checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects }); 664 const configProject2 = projectService.configuredProjects.get(configFile.path)!; 665 assert.strictEqual(configProject2, configProject1); 666 checkProjectActualFiles(configProject2, [file1.path, file2.path, file3.path, configFile.path]); 667 assert.equal(configProject2.hasOpenRef(), hasOpenRef); 668 } 669 }); 670 671 it("Open ref of configured project when open file gets added to the project as part of configured file update buts its open file references are all closed when the update happens", () => { 672 const file1: File = { 673 path: "/a/b/src/file1.ts", 674 content: "let x = 1;" 675 }; 676 const file2: File = { 677 path: "/a/b/src/file2.ts", 678 content: "let y = 1;" 679 }; 680 const file3: File = { 681 path: "/a/b/file3.ts", 682 content: "let z = 1;" 683 }; 684 const file4: File = { 685 path: "/a/file4.ts", 686 content: "let z = 1;" 687 }; 688 const configFile = { 689 path: "/a/b/tsconfig.json", 690 content: JSON.stringify({ files: ["src/file1.ts", "file3.ts"] }) 691 }; 692 693 const files = [file1, file2, file3]; 694 const hostFiles = files.concat(file4, configFile); 695 const host = createServerHost(hostFiles); 696 const projectService = createProjectService(host); 697 698 projectService.openClientFile(file1.path); 699 projectService.openClientFile(file2.path); 700 projectService.openClientFile(file3.path); 701 702 checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); 703 const configuredProject = projectService.configuredProjects.get(configFile.path)!; 704 assert.isTrue(configuredProject.hasOpenRef()); // file1 and file3 705 checkProjectActualFiles(configuredProject, [file1.path, file3.path, configFile.path]); 706 const inferredProject1 = projectService.inferredProjects[0]; 707 checkProjectActualFiles(inferredProject1, [file2.path]); 708 709 projectService.closeClientFile(file1.path); 710 projectService.closeClientFile(file3.path); 711 assert.isFalse(configuredProject.hasOpenRef()); // No files 712 713 host.writeFile(configFile.path, "{}"); 714 // Time out is not yet run so there is project update pending 715 assert.isTrue(configuredProject.hasOpenRef()); // Pending update and file2 might get into the project 716 717 projectService.openClientFile(file4.path); 718 719 checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); 720 assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject); 721 assert.isTrue(configuredProject.hasOpenRef()); // Pending update and F2 might get into the project 722 assert.strictEqual(projectService.inferredProjects[0], inferredProject1); 723 const inferredProject2 = projectService.inferredProjects[1]; 724 checkProjectActualFiles(inferredProject2, [file4.path]); 725 726 host.runQueuedTimeoutCallbacks(); 727 checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); 728 assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject); 729 assert.isTrue(configuredProject.hasOpenRef()); // file2 730 checkProjectActualFiles(configuredProject, [file1.path, file2.path, file3.path, configFile.path]); 731 assert.strictEqual(projectService.inferredProjects[0], inferredProject1); 732 assert.isTrue(inferredProject1.isOrphan()); 733 assert.strictEqual(projectService.inferredProjects[1], inferredProject2); 734 checkProjectActualFiles(inferredProject2, [file4.path]); 735 }); 736 737 it("files are properly detached when language service is disabled", () => { 738 const f1 = { 739 path: "/a/app.js", 740 content: "var x = 1" 741 }; 742 const f2 = { 743 path: "/a/largefile.js", 744 content: "" 745 }; 746 const f3 = { 747 path: "/a/lib.js", 748 content: "var x = 1" 749 }; 750 const config = { 751 path: "/a/tsconfig.json", 752 content: JSON.stringify({ compilerOptions: { allowJs: true } }) 753 }; 754 const host = createServerHost([f1, f2, f3, config]); 755 const originalGetFileSize = host.getFileSize; 756 host.getFileSize = (filePath: string) => 757 filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); 758 759 const projectService = createProjectService(host); 760 projectService.openClientFile(f1.path); 761 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 762 const project = projectService.configuredProjects.get(config.path)!; 763 assert.isTrue(project.hasOpenRef()); // f1 764 assert.isFalse(project.isClosed()); 765 766 projectService.closeClientFile(f1.path); 767 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 768 assert.strictEqual(projectService.configuredProjects.get(config.path), project); 769 assert.isFalse(project.hasOpenRef()); // No files 770 assert.isFalse(project.isClosed()); 771 772 for (const f of [f1, f2, f3]) { 773 // All the script infos should be present and contain the project since it is still alive. 774 const scriptInfo = projectService.getScriptInfoForNormalizedPath(server.toNormalizedPath(f.path))!; 775 assert.equal(scriptInfo.containingProjects.length, 1, `expect 1 containing projects for '${f.path}'`); 776 assert.equal(scriptInfo.containingProjects[0], project, `expect configured project to be the only containing project for '${f.path}'`); 777 } 778 779 const f4 = { 780 path: "/aa.js", 781 content: "var x = 1" 782 }; 783 host.writeFile(f4.path, f4.content); 784 projectService.openClientFile(f4.path); 785 projectService.checkNumberOfProjects({ inferredProjects: 1 }); 786 assert.isFalse(project.hasOpenRef()); // No files 787 assert.isTrue(project.isClosed()); 788 789 for (const f of [f1, f2, f3]) { 790 // All the script infos should not be present since the project is closed and orphan script infos are collected 791 assert.isUndefined(projectService.getScriptInfoForNormalizedPath(server.toNormalizedPath(f.path))); 792 } 793 }); 794 795 it("syntactic features work even if language service is disabled", () => { 796 const f1 = { 797 path: "/a/app.js", 798 content: "let x = 1;" 799 }; 800 const f2 = { 801 path: "/a/largefile.js", 802 content: "" 803 }; 804 const config = { 805 path: "/a/jsconfig.json", 806 content: "{}" 807 }; 808 const host = createServerHost([f1, f2, config]); 809 const originalGetFileSize = host.getFileSize; 810 host.getFileSize = (filePath: string) => 811 filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); 812 const { session, events } = createSessionWithEventTracking<server.ProjectLanguageServiceStateEvent>(host, server.ProjectLanguageServiceStateEvent); 813 session.executeCommand(<protocol.OpenRequest>{ 814 seq: 0, 815 type: "request", 816 command: "open", 817 arguments: { file: f1.path } 818 }); 819 820 const projectService = session.getProjectService(); 821 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 822 const project = configuredProjectAt(projectService, 0); 823 assert.isFalse(project.languageServiceEnabled, "Language service enabled"); 824 assert.equal(events.length, 1, "should receive event"); 825 assert.equal(events[0].data.project, project, "project name"); 826 assert.isFalse(events[0].data.languageServiceEnabled, "Language service state"); 827 828 const options = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath); 829 const edits = project.getLanguageService().getFormattingEditsForDocument(f1.path, options); 830 assert.deepEqual(edits, [{ span: createTextSpan(/*start*/ 7, /*length*/ 3), newText: " " }]); 831 }); 832 833 it("when multiple projects are open, detects correct default project", () => { 834 const barConfig: File = { 835 path: `${tscWatch.projectRoot}/bar/tsconfig.json`, 836 content: JSON.stringify({ 837 include: ["index.ts"], 838 compilerOptions: { 839 lib: ["dom", "es2017"] 840 } 841 }) 842 }; 843 const barIndex: File = { 844 path: `${tscWatch.projectRoot}/bar/index.ts`, 845 content: ` 846export function bar() { 847 console.log("hello world"); 848}` 849 }; 850 const fooConfig: File = { 851 path: `${tscWatch.projectRoot}/foo/tsconfig.json`, 852 content: JSON.stringify({ 853 include: ["index.ts"], 854 compilerOptions: { 855 lib: ["es2017"] 856 } 857 }) 858 }; 859 const fooIndex: File = { 860 path: `${tscWatch.projectRoot}/foo/index.ts`, 861 content: ` 862import { bar } from "bar"; 863bar();` 864 }; 865 const barSymLink: SymLink = { 866 path: `${tscWatch.projectRoot}/foo/node_modules/bar`, 867 symLink: `${tscWatch.projectRoot}/bar` 868 }; 869 870 const lib2017: File = { 871 path: `${getDirectoryPath(libFile.path)}/lib.es2017.d.ts`, 872 content: libFile.content 873 }; 874 const libDom: File = { 875 path: `${getDirectoryPath(libFile.path)}/lib.dom.d.ts`, 876 content: ` 877declare var console: { 878 log(...args: any[]): void; 879};` 880 }; 881 const host = createServerHost([barConfig, barIndex, fooConfig, fooIndex, barSymLink, lib2017, libDom]); 882 const session = createSession(host, { canUseEvents: true, }); 883 openFilesForSession([fooIndex, barIndex], session); 884 verifyGetErrRequestNoErrors({ session, host, files: [barIndex, fooIndex] }); 885 }); 886 887 it("when file name starts with ^", () => { 888 const file: File = { 889 path: `${tscWatch.projectRoot}/file.ts`, 890 content: "const x = 10;" 891 }; 892 const app: File = { 893 path: `${tscWatch.projectRoot}/^app.ts`, 894 content: "const y = 10;" 895 }; 896 const tsconfig: File = { 897 path: `${tscWatch.projectRoot}/tsconfig.json`, 898 content: "{}" 899 }; 900 const host = createServerHost([file, app, tsconfig, libFile]); 901 const service = createProjectService(host); 902 service.openClientFile(file.path); 903 }); 904 905 describe("when creating new file", () => { 906 const foo: File = { 907 path: `${tscWatch.projectRoot}/src/foo.ts`, 908 content: "export function foo() { }" 909 }; 910 const bar: File = { 911 path: `${tscWatch.projectRoot}/src/bar.ts`, 912 content: "export function bar() { }" 913 }; 914 const config: File = { 915 path: `${tscWatch.projectRoot}/tsconfig.json`, 916 content: JSON.stringify({ 917 include: ["./src"] 918 }) 919 }; 920 const fooBar: File = { 921 path: `${tscWatch.projectRoot}/src/sub/fooBar.ts`, 922 content: "export function fooBar() { }" 923 }; 924 function verifySessionWorker({ withExclude, openFileBeforeCreating, checkProjectBeforeError, checkProjectAfterError, }: VerifySession, errorOnNewFileBeforeOldFile: boolean) { 925 const host = createServerHost([ 926 foo, bar, libFile, { path: `${tscWatch.projectRoot}/src/sub` }, 927 withExclude ? 928 { 929 path: config.path, 930 content: JSON.stringify({ 931 include: ["./src"], 932 exclude: ["./src/sub"] 933 }) 934 } : 935 config 936 ]); 937 const session = createSession(host, { 938 canUseEvents: true 939 }); 940 session.executeCommandSeq<protocol.OpenRequest>({ 941 command: protocol.CommandTypes.Open, 942 arguments: { 943 file: foo.path, 944 fileContent: foo.content, 945 projectRootPath: tscWatch.projectRoot 946 } 947 }); 948 if (!openFileBeforeCreating) { 949 host.writeFile(fooBar.path, fooBar.content); 950 } 951 session.executeCommandSeq<protocol.OpenRequest>({ 952 command: protocol.CommandTypes.Open, 953 arguments: { 954 file: fooBar.path, 955 fileContent: fooBar.content, 956 projectRootPath: tscWatch.projectRoot 957 } 958 }); 959 if (openFileBeforeCreating) { 960 host.writeFile(fooBar.path, fooBar.content); 961 } 962 const service = session.getProjectService(); 963 checkProjectBeforeError(service); 964 verifyGetErrRequestNoErrors({ 965 session, 966 host, 967 files: errorOnNewFileBeforeOldFile ? 968 [fooBar, foo] : 969 [foo, fooBar], 970 existingTimeouts: withExclude ? 0 : 2 971 }); 972 checkProjectAfterError(service); 973 } 974 interface VerifySession { 975 withExclude?: boolean; 976 openFileBeforeCreating: boolean; 977 checkProjectBeforeError: (service: server.ProjectService) => void; 978 checkProjectAfterError: (service: server.ProjectService) => void; 979 } 980 function verifySession(input: VerifySession) { 981 it("when error on new file are asked before old one", () => { 982 verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ true); 983 }); 984 985 it("when error on new file are asked after old one", () => { 986 verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ false); 987 }); 988 } 989 function checkFooBarInInferredProject(service: server.ProjectService) { 990 checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); 991 checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, libFile.path, config.path]); 992 checkProjectActualFiles(service.inferredProjects[0], [fooBar.path, libFile.path]); 993 } 994 function checkFooBarInConfiguredProject(service: server.ProjectService) { 995 checkNumberOfProjects(service, { configuredProjects: 1 }); 996 checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, fooBar.path, libFile.path, config.path]); 997 } 998 describe("when new file creation directory watcher is invoked before file is opened in editor", () => { 999 verifySession({ 1000 openFileBeforeCreating: false, 1001 checkProjectBeforeError: checkFooBarInConfiguredProject, 1002 checkProjectAfterError: checkFooBarInConfiguredProject 1003 }); 1004 describe("when new file is excluded from config", () => { 1005 verifySession({ 1006 withExclude: true, 1007 openFileBeforeCreating: false, 1008 checkProjectBeforeError: checkFooBarInInferredProject, 1009 checkProjectAfterError: checkFooBarInInferredProject 1010 }); 1011 }); 1012 }); 1013 1014 describe("when new file creation directory watcher is invoked after file is opened in editor", () => { 1015 verifySession({ 1016 openFileBeforeCreating: true, 1017 checkProjectBeforeError: checkFooBarInInferredProject, 1018 checkProjectAfterError: service => { 1019 // Both projects exist but fooBar is in configured project after the update 1020 // Inferred project is yet to be updated so still has fooBar 1021 checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); 1022 checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, fooBar.path, libFile.path, config.path]); 1023 checkProjectActualFiles(service.inferredProjects[0], [fooBar.path, libFile.path]); 1024 assert.isTrue(service.inferredProjects[0].dirty); 1025 assert.equal(service.inferredProjects[0].getRootFilesMap().size, 0); 1026 } 1027 }); 1028 describe("when new file is excluded from config", () => { 1029 verifySession({ 1030 withExclude: true, 1031 openFileBeforeCreating: true, 1032 checkProjectBeforeError: checkFooBarInInferredProject, 1033 checkProjectAfterError: checkFooBarInInferredProject 1034 }); 1035 }); 1036 }); 1037 }); 1038 1039 it("when default configured project does not contain the file", () => { 1040 const barConfig: File = { 1041 path: `${tscWatch.projectRoot}/bar/tsconfig.json`, 1042 content: "{}" 1043 }; 1044 const barIndex: File = { 1045 path: `${tscWatch.projectRoot}/bar/index.ts`, 1046 content: `import {foo} from "../foo/lib"; 1047foo();` 1048 }; 1049 const fooBarConfig: File = { 1050 path: `${tscWatch.projectRoot}/foobar/tsconfig.json`, 1051 content: barConfig.path 1052 }; 1053 const fooBarIndex: File = { 1054 path: `${tscWatch.projectRoot}/foobar/index.ts`, 1055 content: barIndex.content 1056 }; 1057 const fooConfig: File = { 1058 path: `${tscWatch.projectRoot}/foo/tsconfig.json`, 1059 content: JSON.stringify({ 1060 include: ["index.ts"], 1061 compilerOptions: { 1062 declaration: true, 1063 outDir: "lib" 1064 } 1065 }) 1066 }; 1067 const fooIndex: File = { 1068 path: `${tscWatch.projectRoot}/foo/index.ts`, 1069 content: `export function foo() {}` 1070 }; 1071 const host = createServerHost([barConfig, barIndex, fooBarConfig, fooBarIndex, fooConfig, fooIndex, libFile]); 1072 tscWatch.ensureErrorFreeBuild(host, [fooConfig.path]); 1073 const fooDts = `${tscWatch.projectRoot}/foo/lib/index.d.ts`; 1074 assert.isTrue(host.fileExists(fooDts)); 1075 const session = createSession(host); 1076 const service = session.getProjectService(); 1077 service.openClientFile(barIndex.path); 1078 checkProjectActualFiles(service.configuredProjects.get(barConfig.path)!, [barIndex.path, fooDts, libFile.path, barConfig.path]); 1079 service.openClientFile(fooBarIndex.path); 1080 checkProjectActualFiles(service.configuredProjects.get(fooBarConfig.path)!, [fooBarIndex.path, fooDts, libFile.path, fooBarConfig.path]); 1081 service.openClientFile(fooIndex.path); 1082 checkProjectActualFiles(service.configuredProjects.get(fooConfig.path)!, [fooIndex.path, libFile.path, fooConfig.path]); 1083 service.openClientFile(fooDts); 1084 session.executeCommandSeq<protocol.GetApplicableRefactorsRequest>({ 1085 command: protocol.CommandTypes.GetApplicableRefactors, 1086 arguments: { 1087 file: fooDts, 1088 startLine: 1, 1089 startOffset: 1, 1090 endLine: 1, 1091 endOffset: 1 1092 } 1093 }); 1094 assert.equal(service.tryGetDefaultProjectForFile(server.toNormalizedPath(fooDts)), service.configuredProjects.get(barConfig.path)); 1095 }); 1096 1097 describe("watches extended config files", () => { 1098 function getService(additionalFiles?: File[]) { 1099 const alphaExtendedConfig: File = { 1100 path: `${tscWatch.projectRoot}/extended/alpha.tsconfig.json`, 1101 content: "{}" 1102 }; 1103 const bravoExtendedConfig: File = { 1104 path: `${tscWatch.projectRoot}/extended/bravo.tsconfig.json`, 1105 content: JSON.stringify({ 1106 extends: "./alpha.tsconfig.json" 1107 }) 1108 }; 1109 const aConfig: File = { 1110 path: `${tscWatch.projectRoot}/a/tsconfig.json`, 1111 content: JSON.stringify({ 1112 extends: "../extended/alpha.tsconfig.json", 1113 files: ["a.ts"] 1114 }) 1115 }; 1116 const aFile: File = { 1117 path: `${tscWatch.projectRoot}/a/a.ts`, 1118 content: `let a = 1;` 1119 }; 1120 const bConfig: File = { 1121 path: `${tscWatch.projectRoot}/b/tsconfig.json`, 1122 content: JSON.stringify({ 1123 extends: "../extended/bravo.tsconfig.json", 1124 files: ["b.ts"] 1125 }) 1126 }; 1127 const bFile: File = { 1128 path: `${tscWatch.projectRoot}/b/b.ts`, 1129 content: `let b = 1;` 1130 }; 1131 1132 const host = createServerHost([alphaExtendedConfig, aConfig, aFile, bravoExtendedConfig, bConfig, bFile, ...(additionalFiles || emptyArray)]); 1133 const projectService = createProjectService(host); 1134 return { host, projectService, aFile, bFile, aConfig, bConfig, alphaExtendedConfig, bravoExtendedConfig }; 1135 } 1136 1137 it("should watch the extended configs of multiple projects", () => { 1138 const { host, projectService, aFile, bFile, aConfig, bConfig, alphaExtendedConfig, bravoExtendedConfig } = getService(); 1139 1140 projectService.openClientFile(aFile.path); 1141 projectService.openClientFile(bFile.path); 1142 checkNumberOfConfiguredProjects(projectService, 2); 1143 const aProject = projectService.configuredProjects.get(aConfig.path)!; 1144 const bProject = projectService.configuredProjects.get(bConfig.path)!; 1145 checkProjectActualFiles(aProject, [aFile.path, aConfig.path, alphaExtendedConfig.path]); 1146 checkProjectActualFiles(bProject, [bFile.path, bConfig.path, bravoExtendedConfig.path, alphaExtendedConfig.path]); 1147 assert.isUndefined(aProject.getCompilerOptions().strict); 1148 assert.isUndefined(bProject.getCompilerOptions().strict); 1149 checkWatchedFiles(host, [aConfig.path, bConfig.path, libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path]); 1150 1151 host.writeFile(alphaExtendedConfig.path, JSON.stringify({ 1152 compilerOptions: { 1153 strict: true 1154 } 1155 })); 1156 assert.isTrue(projectService.hasPendingProjectUpdate(aProject)); 1157 assert.isTrue(projectService.hasPendingProjectUpdate(bProject)); 1158 host.checkTimeoutQueueLengthAndRun(3); 1159 assert.isTrue(aProject.getCompilerOptions().strict); 1160 assert.isTrue(bProject.getCompilerOptions().strict); 1161 checkWatchedFiles(host, [aConfig.path, bConfig.path, libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path]); 1162 1163 host.writeFile(bravoExtendedConfig.path, JSON.stringify({ 1164 extends: "./alpha.tsconfig.json", 1165 compilerOptions: { 1166 strict: false 1167 } 1168 })); 1169 assert.isFalse(projectService.hasPendingProjectUpdate(aProject)); 1170 assert.isTrue(projectService.hasPendingProjectUpdate(bProject)); 1171 host.checkTimeoutQueueLengthAndRun(2); 1172 assert.isTrue(aProject.getCompilerOptions().strict); 1173 assert.isFalse(bProject.getCompilerOptions().strict); 1174 checkWatchedFiles(host, [aConfig.path, bConfig.path, libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path]); 1175 1176 host.writeFile(bConfig.path, JSON.stringify({ 1177 extends: "../extended/alpha.tsconfig.json", 1178 })); 1179 assert.isFalse(projectService.hasPendingProjectUpdate(aProject)); 1180 assert.isTrue(projectService.hasPendingProjectUpdate(bProject)); 1181 host.checkTimeoutQueueLengthAndRun(2); 1182 assert.isTrue(aProject.getCompilerOptions().strict); 1183 assert.isTrue(bProject.getCompilerOptions().strict); 1184 checkWatchedFiles(host, [aConfig.path, bConfig.path, libFile.path, alphaExtendedConfig.path]); 1185 1186 host.writeFile(alphaExtendedConfig.path, "{}"); 1187 assert.isTrue(projectService.hasPendingProjectUpdate(aProject)); 1188 assert.isTrue(projectService.hasPendingProjectUpdate(bProject)); 1189 host.checkTimeoutQueueLengthAndRun(3); 1190 assert.isUndefined(aProject.getCompilerOptions().strict); 1191 assert.isUndefined(bProject.getCompilerOptions().strict); 1192 checkWatchedFiles(host, [aConfig.path, bConfig.path, libFile.path, alphaExtendedConfig.path]); 1193 }); 1194 1195 it("should stop watching the extended configs of closed projects", () => { 1196 const dummy: File = { 1197 path: `${tscWatch.projectRoot}/dummy/dummy.ts`, 1198 content: `let dummy = 1;` 1199 }; 1200 const dummyConfig: File = { 1201 path: `${tscWatch.projectRoot}/dummy/tsconfig.json`, 1202 content: "{}" 1203 }; 1204 const { host, projectService, aFile, bFile, aConfig, bConfig, alphaExtendedConfig, bravoExtendedConfig } = getService([dummy, dummyConfig]); 1205 1206 projectService.openClientFile(aFile.path); 1207 projectService.openClientFile(bFile.path); 1208 projectService.openClientFile(dummy.path); 1209 checkNumberOfConfiguredProjects(projectService, 3); 1210 checkWatchedFiles(host, [aConfig.path, bConfig.path, libFile.path, bravoExtendedConfig.path, alphaExtendedConfig.path, dummyConfig.path]); 1211 1212 projectService.closeClientFile(bFile.path); 1213 projectService.closeClientFile(dummy.path); 1214 projectService.openClientFile(dummy.path); 1215 1216 checkNumberOfConfiguredProjects(projectService, 2); 1217 checkWatchedFiles(host, [aConfig.path, libFile.path, alphaExtendedConfig.path, dummyConfig.path]); 1218 1219 projectService.closeClientFile(aFile.path); 1220 projectService.closeClientFile(dummy.path); 1221 projectService.openClientFile(dummy.path); 1222 1223 checkNumberOfConfiguredProjects(projectService, 1); 1224 checkWatchedFiles(host, [libFile.path, dummyConfig.path]); 1225 }); 1226 }); 1227 }); 1228 1229 describe("unittests:: tsserver:: ConfiguredProjects:: non-existing directories listed in config file input array", () => { 1230 it("should be tolerated without crashing the server", () => { 1231 const configFile = { 1232 path: "/a/b/tsconfig.json", 1233 content: `{ 1234 "compilerOptions": {}, 1235 "include": ["app/*", "test/**/*", "something"] 1236 }` 1237 }; 1238 const file1 = { 1239 path: "/a/b/file1.ts", 1240 content: "let t = 10;" 1241 }; 1242 1243 const host = createServerHost([file1, configFile]); 1244 const projectService = createProjectService(host); 1245 projectService.openClientFile(file1.path); 1246 host.runQueuedTimeoutCallbacks(); 1247 1248 // Since file1 refers to config file as the default project, it needs to be kept alive 1249 checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); 1250 const inferredProject = projectService.inferredProjects[0]; 1251 assert.isTrue(inferredProject.containsFile(<server.NormalizedPath>file1.path)); 1252 assert.isFalse(projectService.configuredProjects.get(configFile.path)!.containsFile(<server.NormalizedPath>file1.path)); 1253 }); 1254 1255 it("should be able to handle @types if input file list is empty", () => { 1256 const f = { 1257 path: "/a/app.ts", 1258 content: "let x = 1" 1259 }; 1260 const config = { 1261 path: "/a/tsconfig.json", 1262 content: JSON.stringify({ 1263 compiler: {}, 1264 files: [] 1265 }) 1266 }; 1267 const t1 = { 1268 path: "/a/node_modules/@types/typings/index.d.ts", 1269 content: `export * from "./lib"` 1270 }; 1271 const t2 = { 1272 path: "/a/node_modules/@types/typings/lib.d.ts", 1273 content: `export const x: number` 1274 }; 1275 const host = createServerHost([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) }); 1276 const projectService = createProjectService(host); 1277 1278 projectService.openClientFile(f.path); 1279 // Since f refers to config file as the default project, it needs to be kept alive 1280 projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); 1281 }); 1282 1283 it("should tolerate invalid include files that start in subDirectory", () => { 1284 const f = { 1285 path: `${tscWatch.projectRoot}/src/server/index.ts`, 1286 content: "let x = 1" 1287 }; 1288 const config = { 1289 path: `${tscWatch.projectRoot}/src/server/tsconfig.json`, 1290 content: JSON.stringify({ 1291 compiler: { 1292 module: "commonjs", 1293 outDir: "../../build" 1294 }, 1295 include: [ 1296 "../src/**/*.ts" 1297 ] 1298 }) 1299 }; 1300 const host = createServerHost([f, config, libFile], { useCaseSensitiveFileNames: true }); 1301 const projectService = createProjectService(host); 1302 1303 projectService.openClientFile(f.path); 1304 // Since f refers to config file as the default project, it needs to be kept alive 1305 projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); 1306 }); 1307 1308 it("Changed module resolution reflected when specifying files list", () => { 1309 const file1: File = { 1310 path: "/a/b/file1.ts", 1311 content: 'import classc from "file2"' 1312 }; 1313 const file2a: File = { 1314 path: "/a/file2.ts", 1315 content: "export classc { method2a() { return 10; } }" 1316 }; 1317 const file2: File = { 1318 path: "/a/b/file2.ts", 1319 content: "export classc { method2() { return 10; } }" 1320 }; 1321 const configFile: File = { 1322 path: "/a/b/tsconfig.json", 1323 content: JSON.stringify({ files: [file1.path], compilerOptions: { module: "amd" } }) 1324 }; 1325 const files = [file1, file2a, configFile, libFile]; 1326 const host = createServerHost(files); 1327 const projectService = createProjectService(host); 1328 projectService.openClientFile(file1.path); 1329 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 1330 const project = projectService.configuredProjects.get(configFile.path)!; 1331 assert.isDefined(project); 1332 checkProjectActualFiles(project, map(files, file => file.path)); 1333 checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); 1334 checkWatchedDirectoriesDetailed(host, ["/a/b"], 1, /*recursive*/ false); 1335 checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); 1336 1337 files.push(file2); 1338 host.writeFile(file2.path, file2.content); 1339 host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions 1340 host.runQueuedTimeoutCallbacks(); // Actual update 1341 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 1342 assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); 1343 checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); 1344 checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); 1345 checkWatchedDirectories(host, emptyArray, /*recursive*/ false); 1346 checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); 1347 1348 // On next file open the files file2a should be closed and not watched any more 1349 projectService.openClientFile(file2.path); 1350 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 1351 assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); 1352 checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); 1353 checkWatchedFiles(host, [libFile.path, configFile.path]); 1354 checkWatchedDirectories(host, emptyArray, /*recursive*/ false); 1355 checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); 1356 }); 1357 1358 it("Failed lookup locations uses parent most node_modules directory", () => { 1359 const root = "/user/username/rootfolder"; 1360 const file1: File = { 1361 path: "/a/b/src/file1.ts", 1362 content: 'import { classc } from "module1"' 1363 }; 1364 const module1: File = { 1365 path: "/a/b/node_modules/module1/index.d.ts", 1366 content: `import { class2 } from "module2"; 1367 export classc { method2a(): class2; }` 1368 }; 1369 const module2: File = { 1370 path: "/a/b/node_modules/module2/index.d.ts", 1371 content: "export class2 { method2() { return 10; } }" 1372 }; 1373 const module3: File = { 1374 path: "/a/b/node_modules/module/node_modules/module3/index.d.ts", 1375 content: "export class3 { method2() { return 10; } }" 1376 }; 1377 const configFile: File = { 1378 path: "/a/b/src/tsconfig.json", 1379 content: JSON.stringify({ files: ["file1.ts"] }) 1380 }; 1381 const nonLibFiles = [file1, module1, module2, module3, configFile]; 1382 nonLibFiles.forEach(f => f.path = root + f.path); 1383 const files = nonLibFiles.concat(libFile); 1384 const host = createServerHost(files); 1385 const projectService = createProjectService(host); 1386 projectService.openClientFile(file1.path); 1387 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 1388 const project = projectService.configuredProjects.get(configFile.path)!; 1389 assert.isDefined(project); 1390 checkProjectActualFiles(project, [file1.path, libFile.path, module1.path, module2.path, configFile.path]); 1391 checkWatchedFiles(host, [libFile.path, configFile.path]); 1392 checkWatchedDirectories(host, [], /*recursive*/ false); 1393 const watchedRecursiveDirectories = getTypeRootsFromLocation(root + "/a/b/src"); 1394 watchedRecursiveDirectories.push(`${root}/a/b/src/node_modules`, `${root}/a/b/node_modules`); 1395 checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); 1396 }); 1397 }); 1398 1399 describe("unittests:: tsserver:: ConfiguredProjects:: when reading tsconfig file fails", () => { 1400 it("should be tolerated without crashing the server", () => { 1401 const configFile = { 1402 path: `${tscWatch.projectRoot}/tsconfig.json`, 1403 content: "" 1404 }; 1405 const file1 = { 1406 path: `${tscWatch.projectRoot}/file1.ts`, 1407 content: "let t = 10;" 1408 }; 1409 1410 const host = createServerHost([file1, libFile, configFile]); 1411 const { session, events } = createSessionWithEventTracking<server.ConfigFileDiagEvent>(host, server.ConfigFileDiagEvent); 1412 const originalReadFile = host.readFile; 1413 host.readFile = f => { 1414 return f === configFile.path ? 1415 undefined : 1416 originalReadFile.call(host, f); 1417 }; 1418 openFilesForSession([file1], session); 1419 1420 assert.deepEqual(events, [{ 1421 eventName: server.ConfigFileDiagEvent, 1422 data: { 1423 triggerFile: file1.path, 1424 configFileName: configFile.path, 1425 diagnostics: [ 1426 createCompilerDiagnostic(Diagnostics.Cannot_read_file_0, configFile.path) 1427 ] 1428 } 1429 }]); 1430 }); 1431 }); 1432} 1433