1namespace ts.projectSystem { 2 describe("unittests:: tsserver:: ExternalProjects", () => { 3 describe("can handle tsconfig file name with difference casing", () => { 4 function verifyConfigFileCasing(lazyConfiguredProjectsFromExternalProject: boolean) { 5 const f1 = { 6 path: "/a/b/app.ts", 7 content: "let x = 1" 8 }; 9 const config = { 10 path: "/a/b/tsconfig.json", 11 content: JSON.stringify({ 12 include: [] 13 }) 14 }; 15 16 const host = createServerHost([f1, config], { useCaseSensitiveFileNames: false }); 17 const service = createProjectService(host); 18 service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); 19 const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); 20 service.openExternalProject({ 21 projectFileName: "/a/b/project.csproj", 22 rootFiles: toExternalFiles([f1.path, upperCaseConfigFilePath]), 23 options: {} 24 } as protocol.ExternalProject); 25 service.checkNumberOfProjects({ configuredProjects: 1 }); 26 const project = service.configuredProjects.get(config.path)!; 27 if (lazyConfiguredProjectsFromExternalProject) { 28 assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded 29 checkProjectActualFiles(project, emptyArray); 30 } 31 else { 32 assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded 33 checkProjectActualFiles(project, [upperCaseConfigFilePath]); 34 } 35 36 service.openClientFile(f1.path); 37 service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); 38 39 assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project is updated 40 checkProjectActualFiles(project, [upperCaseConfigFilePath]); 41 checkProjectActualFiles(service.inferredProjects[0], [f1.path]); 42 } 43 44 it("when lazyConfiguredProjectsFromExternalProject not set", () => { 45 verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ false); 46 }); 47 48 it("when lazyConfiguredProjectsFromExternalProject is set", () => { 49 verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ true); 50 }); 51 }); 52 53 it("load global plugins", () => { 54 const f1 = { 55 path: "/a/file1.ts", 56 content: "let x = [1, 2];" 57 }; 58 const p1 = { projectFileName: "/a/proj1.csproj", rootFiles: [toExternalFile(f1.path)], options: {} }; 59 60 const host = createServerHost([f1]); 61 host.require = (_initialPath, moduleName) => { 62 assert.equal(moduleName, "myplugin"); 63 return { 64 module: () => ({ 65 create(info: server.PluginCreateInfo) { 66 const proxy = Harness.LanguageService.makeDefaultProxy(info); 67 proxy.getSemanticDiagnostics = filename => { 68 const prev = info.languageService.getSemanticDiagnostics(filename); 69 const sourceFile: SourceFile = info.project.getSourceFile(toPath(filename, /*basePath*/ undefined, createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; 70 prev.push({ 71 category: DiagnosticCategory.Warning, 72 file: sourceFile, 73 code: 9999, 74 length: 3, 75 messageText: `Plugin diagnostic`, 76 start: 0 77 }); 78 return prev; 79 }; 80 return proxy; 81 } 82 }), 83 error: undefined 84 }; 85 }; 86 const session = createSession(host, { globalPlugins: ["myplugin"] }); 87 88 session.executeCommand({ 89 seq: 1, 90 type: "request", 91 command: "openExternalProjects", 92 arguments: { projects: [p1] } 93 } as protocol.OpenExternalProjectsRequest); 94 95 const projectService = session.getProjectService(); 96 checkNumberOfProjects(projectService, { externalProjects: 1 }); 97 assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); 98 99 const handlerResponse = session.executeCommand({ 100 seq: 2, 101 type: "request", 102 command: "semanticDiagnosticsSync", 103 arguments: { 104 file: f1.path, 105 projectFileName: p1.projectFileName 106 } 107 } as protocol.SemanticDiagnosticsSyncRequest); 108 109 assert.isDefined(handlerResponse.response); 110 const response = handlerResponse.response as protocol.Diagnostic[]; 111 assert.equal(response.length, 1); 112 assert.equal(response[0].text, "Plugin diagnostic"); 113 }); 114 115 it("remove not-listed external projects", () => { 116 const f1 = { 117 path: "/a/app.ts", 118 content: "let x = 1" 119 }; 120 const f2 = { 121 path: "/b/app.ts", 122 content: "let x = 1" 123 }; 124 const f3 = { 125 path: "/c/app.ts", 126 content: "let x = 1" 127 }; 128 const makeProject = (f: File) => ({ projectFileName: f.path + ".csproj", rootFiles: [toExternalFile(f.path)], options: {} }); 129 const p1 = makeProject(f1); 130 const p2 = makeProject(f2); 131 const p3 = makeProject(f3); 132 133 const host = createServerHost([f1, f2, f3]); 134 const session = createSession(host); 135 136 session.executeCommand({ 137 seq: 1, 138 type: "request", 139 command: "openExternalProjects", 140 arguments: { projects: [p1, p2] } 141 } as protocol.OpenExternalProjectsRequest); 142 143 const projectService = session.getProjectService(); 144 checkNumberOfProjects(projectService, { externalProjects: 2 }); 145 assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); 146 assert.equal(projectService.externalProjects[1].getProjectName(), p2.projectFileName); 147 148 session.executeCommand({ 149 seq: 2, 150 type: "request", 151 command: "openExternalProjects", 152 arguments: { projects: [p1, p3] } 153 } as protocol.OpenExternalProjectsRequest); 154 checkNumberOfProjects(projectService, { externalProjects: 2 }); 155 assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); 156 assert.equal(projectService.externalProjects[1].getProjectName(), p3.projectFileName); 157 158 session.executeCommand({ 159 seq: 3, 160 type: "request", 161 command: "openExternalProjects", 162 arguments: { projects: [] } 163 } as protocol.OpenExternalProjectsRequest); 164 checkNumberOfProjects(projectService, { externalProjects: 0 }); 165 166 session.executeCommand({ 167 seq: 3, 168 type: "request", 169 command: "openExternalProjects", 170 arguments: { projects: [p2] } 171 } as protocol.OpenExternalProjectsRequest); 172 assert.equal(projectService.externalProjects[0].getProjectName(), p2.projectFileName); 173 }); 174 175 it("should not close external project with no open files", () => { 176 const file1 = { 177 path: "/a/b/f1.ts", 178 content: "let x =1;" 179 }; 180 const file2 = { 181 path: "/a/b/f2.ts", 182 content: "let y =1;" 183 }; 184 const externalProjectName = "externalproject"; 185 const host = createServerHost([file1, file2]); 186 const projectService = createProjectService(host); 187 projectService.openExternalProject({ 188 rootFiles: toExternalFiles([file1.path, file2.path]), 189 options: {}, 190 projectFileName: externalProjectName 191 }); 192 193 checkNumberOfExternalProjects(projectService, 1); 194 checkNumberOfInferredProjects(projectService, 0); 195 196 // open client file - should not lead to creation of inferred project 197 projectService.openClientFile(file1.path, file1.content); 198 checkNumberOfExternalProjects(projectService, 1); 199 checkNumberOfInferredProjects(projectService, 0); 200 201 // close client file - external project should still exists 202 projectService.closeClientFile(file1.path); 203 checkNumberOfExternalProjects(projectService, 1); 204 checkNumberOfInferredProjects(projectService, 0); 205 206 projectService.closeExternalProject(externalProjectName); 207 checkNumberOfExternalProjects(projectService, 0); 208 checkNumberOfInferredProjects(projectService, 0); 209 }); 210 211 it("external project for dynamic file", () => { 212 const externalProjectName = "^ScriptDocument1 file1.ts"; 213 const externalFiles = toExternalFiles(["^ScriptDocument1 file1.ts"]); 214 const host = createServerHost([]); 215 const projectService = createProjectService(host); 216 projectService.openExternalProject({ 217 rootFiles: externalFiles, 218 options: {}, 219 projectFileName: externalProjectName 220 }); 221 222 checkNumberOfExternalProjects(projectService, 1); 223 checkNumberOfInferredProjects(projectService, 0); 224 verifyDynamic(projectService, "/^scriptdocument1 file1.ts"); 225 226 externalFiles[0].content = "let x =1;"; 227 projectService.applyChangesInOpenFiles(arrayIterator(externalFiles)); 228 }); 229 230 it("when file name starts with ^", () => { 231 const file: File = { 232 path: `${tscWatch.projectRoot}/file.ts`, 233 content: "const x = 10;" 234 }; 235 const app: File = { 236 path: `${tscWatch.projectRoot}/^app.ts`, 237 content: "const y = 10;" 238 }; 239 const host = createServerHost([file, app, libFile]); 240 const service = createProjectService(host); 241 service.openExternalProjects([{ 242 projectFileName: `${tscWatch.projectRoot}/myproject.njsproj`, 243 rootFiles: [ 244 toExternalFile(file.path), 245 toExternalFile(app.path) 246 ], 247 options: { }, 248 }]); 249 }); 250 251 it("external project that included config files", () => { 252 const file1 = { 253 path: "/a/b/f1.ts", 254 content: "let x =1;" 255 }; 256 const config1 = { 257 path: "/a/b/tsconfig.json", 258 content: JSON.stringify( 259 { 260 compilerOptions: {}, 261 files: ["f1.ts"] 262 } 263 ) 264 }; 265 const file2 = { 266 path: "/a/c/f2.ts", 267 content: "let y =1;" 268 }; 269 const config2 = { 270 path: "/a/c/tsconfig.json", 271 content: JSON.stringify( 272 { 273 compilerOptions: {}, 274 files: ["f2.ts"] 275 } 276 ) 277 }; 278 const file3 = { 279 path: "/a/d/f3.ts", 280 content: "let z =1;" 281 }; 282 const externalProjectName = "externalproject"; 283 const host = createServerHost([file1, file2, file3, config1, config2]); 284 const projectService = createProjectService(host); 285 projectService.openExternalProject({ 286 rootFiles: toExternalFiles([config1.path, config2.path, file3.path]), 287 options: {}, 288 projectFileName: externalProjectName 289 }); 290 291 checkNumberOfProjects(projectService, { configuredProjects: 2 }); 292 const proj1 = projectService.configuredProjects.get(config1.path); 293 const proj2 = projectService.configuredProjects.get(config2.path); 294 assert.isDefined(proj1); 295 assert.isDefined(proj2); 296 297 // open client file - should not lead to creation of inferred project 298 projectService.openClientFile(file1.path, file1.content); 299 checkNumberOfProjects(projectService, { configuredProjects: 2 }); 300 assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); 301 assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); 302 303 projectService.openClientFile(file3.path, file3.content); 304 checkNumberOfProjects(projectService, { configuredProjects: 2, inferredProjects: 1 }); 305 assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); 306 assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); 307 308 projectService.closeExternalProject(externalProjectName); 309 // open file 'file1' from configured project keeps project alive 310 checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); 311 assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); 312 assert.isUndefined(projectService.configuredProjects.get(config2.path)); 313 314 projectService.closeClientFile(file3.path); 315 checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); 316 assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); 317 assert.isUndefined(projectService.configuredProjects.get(config2.path)); 318 assert.isTrue(projectService.inferredProjects[0].isOrphan()); 319 320 projectService.closeClientFile(file1.path); 321 checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); 322 assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); 323 assert.isUndefined(projectService.configuredProjects.get(config2.path)); 324 assert.isTrue(projectService.inferredProjects[0].isOrphan()); 325 326 projectService.openClientFile(file2.path, file2.content); 327 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 328 assert.isUndefined(projectService.configuredProjects.get(config1.path)); 329 assert.isDefined(projectService.configuredProjects.get(config2.path)); 330 }); 331 332 it("external project with included config file opened after configured project", () => { 333 const file1 = { 334 path: "/a/b/f1.ts", 335 content: "let x = 1" 336 }; 337 const configFile = { 338 path: "/a/b/tsconfig.json", 339 content: JSON.stringify({ compilerOptions: {} }) 340 }; 341 const externalProjectName = "externalproject"; 342 const host = createServerHost([file1, configFile]); 343 const projectService = createProjectService(host); 344 345 projectService.openClientFile(file1.path); 346 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 347 348 projectService.openExternalProject({ 349 rootFiles: toExternalFiles([configFile.path]), 350 options: {}, 351 projectFileName: externalProjectName 352 }); 353 354 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 355 356 projectService.closeClientFile(file1.path); 357 // configured project is alive since it is opened as part of external project 358 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 359 360 projectService.closeExternalProject(externalProjectName); 361 checkNumberOfProjects(projectService, { configuredProjects: 0 }); 362 }); 363 364 it("external project with included config file opened after configured project and then closed", () => { 365 const file1 = { 366 path: "/a/b/f1.ts", 367 content: "let x = 1" 368 }; 369 const file2 = { 370 path: "/a/f2.ts", 371 content: "let x = 1" 372 }; 373 const configFile = { 374 path: "/a/b/tsconfig.json", 375 content: JSON.stringify({ compilerOptions: {} }) 376 }; 377 const externalProjectName = "externalproject"; 378 const host = createServerHost([file1, file2, libFile, configFile]); 379 const projectService = createProjectService(host); 380 381 projectService.openClientFile(file1.path); 382 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 383 const project = projectService.configuredProjects.get(configFile.path); 384 385 projectService.openExternalProject({ 386 rootFiles: toExternalFiles([configFile.path]), 387 options: {}, 388 projectFileName: externalProjectName 389 }); 390 391 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 392 assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); 393 394 projectService.closeExternalProject(externalProjectName); 395 // configured project is alive since file is still open 396 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 397 assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); 398 399 projectService.closeClientFile(file1.path); 400 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 401 assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); 402 403 projectService.openClientFile(file2.path); 404 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 405 assert.isUndefined(projectService.configuredProjects.get(configFile.path)); 406 }); 407 408 it("can correctly update external project when set of root files has changed", () => { 409 const file1 = { 410 path: "/a/b/f1.ts", 411 content: "let x = 1" 412 }; 413 const file2 = { 414 path: "/a/b/f2.ts", 415 content: "let y = 1" 416 }; 417 const host = createServerHost([file1, file2]); 418 const projectService = createProjectService(host); 419 420 projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path]) }); 421 checkNumberOfProjects(projectService, { externalProjects: 1 }); 422 checkProjectActualFiles(projectService.externalProjects[0], [file1.path]); 423 424 projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]) }); 425 checkNumberOfProjects(projectService, { externalProjects: 1 }); 426 checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); 427 }); 428 429 it("can update external project when set of root files was not changed", () => { 430 const file1 = { 431 path: "/a/b/f1.ts", 432 content: `export * from "m"` 433 }; 434 const file2 = { 435 path: "/a/b/f2.ts", 436 content: "export let y = 1" 437 }; 438 const file3 = { 439 path: "/a/m.ts", 440 content: "export let y = 1" 441 }; 442 443 const host = createServerHost([file1, file2, file3]); 444 const projectService = createProjectService(host); 445 446 projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.NodeJs }, rootFiles: toExternalFiles([file1.path, file2.path]) }); 447 checkNumberOfProjects(projectService, { externalProjects: 1 }); 448 checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); 449 checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path]); 450 451 projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.Classic }, rootFiles: toExternalFiles([file1.path, file2.path]) }); 452 checkNumberOfProjects(projectService, { externalProjects: 1 }); 453 checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); 454 checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path, file3.path]); 455 }); 456 457 it("language service disabled state is updated in external projects", () => { 458 const f1 = { 459 path: "/a/app.js", 460 content: "var x = 1" 461 }; 462 const f2 = { 463 path: "/a/largefile.js", 464 content: "" 465 }; 466 const host = createServerHost([f1, f2]); 467 const originalGetFileSize = host.getFileSize; 468 host.getFileSize = (filePath: string) => 469 filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); 470 471 const service = createProjectService(host); 472 const projectFileName = "/a/proj.csproj"; 473 474 service.openExternalProject({ 475 projectFileName, 476 rootFiles: toExternalFiles([f1.path, f2.path]), 477 options: {} 478 }); 479 service.checkNumberOfProjects({ externalProjects: 1 }); 480 assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 1"); 481 482 service.openExternalProject({ 483 projectFileName, 484 rootFiles: toExternalFiles([f1.path]), 485 options: {} 486 }); 487 service.checkNumberOfProjects({ externalProjects: 1 }); 488 assert.isTrue(service.externalProjects[0].languageServiceEnabled, "language service should be enabled"); 489 490 service.openExternalProject({ 491 projectFileName, 492 rootFiles: toExternalFiles([f1.path, f2.path]), 493 options: {} 494 }); 495 service.checkNumberOfProjects({ externalProjects: 1 }); 496 assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 2"); 497 }); 498 499 describe("deleting config file opened from the external project works", () => { 500 function verifyDeletingConfigFile(lazyConfiguredProjectsFromExternalProject: boolean) { 501 const site = { 502 path: "/user/someuser/project/js/site.js", 503 content: "" 504 }; 505 const configFile = { 506 path: "/user/someuser/project/tsconfig.json", 507 content: "{}" 508 }; 509 const projectFileName = "/user/someuser/project/WebApplication6.csproj"; 510 const host = createServerHost([libFile, site, configFile]); 511 const projectService = createProjectService(host); 512 projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); 513 514 const externalProject: protocol.ExternalProject = { 515 projectFileName, 516 rootFiles: [toExternalFile(site.path), toExternalFile(configFile.path)], 517 options: { allowJs: false }, 518 typeAcquisition: { include: [] } 519 }; 520 521 projectService.openExternalProjects([externalProject]); 522 523 let knownProjects = projectService.synchronizeProjectList([]); 524 checkNumberOfProjects(projectService, { configuredProjects: 1, externalProjects: 0, inferredProjects: 0 }); 525 526 const configProject = configuredProjectAt(projectService, 0); 527 checkProjectActualFiles(configProject, lazyConfiguredProjectsFromExternalProject ? 528 emptyArray : // Since no files opened from this project, its not loaded 529 [configFile.path]); 530 531 host.deleteFile(configFile.path); 532 533 knownProjects = projectService.synchronizeProjectList(map(knownProjects, proj => proj.info!)); // TODO: GH#18217 GH#20039 534 checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 0, inferredProjects: 0 }); 535 536 externalProject.rootFiles.length = 1; 537 projectService.openExternalProjects([externalProject]); 538 539 checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 1, inferredProjects: 0 }); 540 checkProjectActualFiles(projectService.externalProjects[0], [site.path, libFile.path]); 541 } 542 it("when lazyConfiguredProjectsFromExternalProject not set", () => { 543 verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ false); 544 }); 545 it("when lazyConfiguredProjectsFromExternalProject is set", () => { 546 verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ true); 547 }); 548 }); 549 550 describe("correctly handling add/remove tsconfig - 1", () => { 551 function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { 552 const f1 = { 553 path: "/a/b/app.ts", 554 content: "let x = 1;" 555 }; 556 const f2 = { 557 path: "/a/b/lib.ts", 558 content: "" 559 }; 560 const tsconfig = { 561 path: "/a/b/tsconfig.json", 562 content: "" 563 }; 564 const host = createServerHost([f1, f2]); 565 const projectService = createProjectService(host); 566 projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); 567 568 // open external project 569 const projectName = "/a/b/proj1"; 570 projectService.openExternalProject({ 571 projectFileName: projectName, 572 rootFiles: toExternalFiles([f1.path, f2.path]), 573 options: {} 574 }); 575 projectService.openClientFile(f1.path); 576 projectService.checkNumberOfProjects({ externalProjects: 1 }); 577 checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); 578 579 // rename lib.ts to tsconfig.json 580 host.renameFile(f2.path, tsconfig.path); 581 projectService.openExternalProject({ 582 projectFileName: projectName, 583 rootFiles: toExternalFiles([f1.path, tsconfig.path]), 584 options: {} 585 }); 586 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 587 if (lazyConfiguredProjectsFromExternalProject) { 588 checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed 589 projectService.ensureInferredProjectsUpToDate_TestOnly(); 590 } 591 checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]); 592 593 // rename tsconfig.json back to lib.ts 594 host.renameFile(tsconfig.path, f2.path); 595 projectService.openExternalProject({ 596 projectFileName: projectName, 597 rootFiles: toExternalFiles([f1.path, f2.path]), 598 options: {} 599 }); 600 601 projectService.checkNumberOfProjects({ externalProjects: 1 }); 602 checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); 603 } 604 it("when lazyConfiguredProjectsFromExternalProject not set", () => { 605 verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); 606 }); 607 it("when lazyConfiguredProjectsFromExternalProject is set", () => { 608 verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); 609 }); 610 }); 611 612 describe("correctly handling add/remove tsconfig - 2", () => { 613 function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { 614 const f1 = { 615 path: "/a/b/app.ts", 616 content: "let x = 1;" 617 }; 618 const cLib = { 619 path: "/a/b/c/lib.ts", 620 content: "" 621 }; 622 const cTsconfig = { 623 path: "/a/b/c/tsconfig.json", 624 content: "{}" 625 }; 626 const dLib = { 627 path: "/a/b/d/lib.ts", 628 content: "" 629 }; 630 const dTsconfig = { 631 path: "/a/b/d/tsconfig.json", 632 content: "{}" 633 }; 634 const host = createServerHost([f1, cLib, cTsconfig, dLib, dTsconfig]); 635 const projectService = createProjectService(host); 636 projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); 637 638 // open external project 639 const projectName = "/a/b/proj1"; 640 projectService.openExternalProject({ 641 projectFileName: projectName, 642 rootFiles: toExternalFiles([f1.path]), 643 options: {} 644 }); 645 646 projectService.checkNumberOfProjects({ externalProjects: 1 }); 647 checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); 648 649 // add two config file as root files 650 projectService.openExternalProject({ 651 projectFileName: projectName, 652 rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), 653 options: {} 654 }); 655 projectService.checkNumberOfProjects({ configuredProjects: 2 }); 656 if (lazyConfiguredProjectsFromExternalProject) { 657 checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed 658 checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed 659 projectService.ensureInferredProjectsUpToDate_TestOnly(); 660 } 661 checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); 662 checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); 663 664 // remove one config file 665 projectService.openExternalProject({ 666 projectFileName: projectName, 667 rootFiles: toExternalFiles([f1.path, dTsconfig.path]), 668 options: {} 669 }); 670 671 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 672 checkProjectActualFiles(configuredProjectAt(projectService, 0), [dLib.path, dTsconfig.path]); 673 674 // remove second config file 675 projectService.openExternalProject({ 676 projectFileName: projectName, 677 rootFiles: toExternalFiles([f1.path]), 678 options: {} 679 }); 680 681 projectService.checkNumberOfProjects({ externalProjects: 1 }); 682 checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); 683 684 // open two config files 685 // add two config file as root files 686 projectService.openExternalProject({ 687 projectFileName: projectName, 688 rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), 689 options: {} 690 }); 691 projectService.checkNumberOfProjects({ configuredProjects: 2 }); 692 if (lazyConfiguredProjectsFromExternalProject) { 693 checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed 694 checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed 695 projectService.ensureInferredProjectsUpToDate_TestOnly(); 696 } 697 checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); 698 checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); 699 700 // close all projects - no projects should be opened 701 projectService.closeExternalProject(projectName); 702 projectService.checkNumberOfProjects({}); 703 } 704 705 it("when lazyConfiguredProjectsFromExternalProject not set", () => { 706 verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); 707 }); 708 it("when lazyConfiguredProjectsFromExternalProject is set", () => { 709 verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); 710 }); 711 }); 712 713 it("correctly handles changes in lib section of config file", () => { 714 const libES5 = { 715 path: "/compiler/lib.es5.d.ts", 716 content: "declare const eval: any" 717 }; 718 const libES2015Promise = { 719 path: "/compiler/lib.es2015.promise.d.ts", 720 content: "declare class Promise<T> {}" 721 }; 722 const app = { 723 path: "/src/app.ts", 724 content: "var x: Promise<string>;" 725 }; 726 const config1 = { 727 path: "/src/tsconfig.json", 728 content: JSON.stringify( 729 { 730 compilerOptions: { 731 module: "commonjs", 732 target: "es5", 733 noImplicitAny: true, 734 sourceMap: false, 735 lib: [ 736 "es5" 737 ] 738 } 739 }) 740 }; 741 const config2 = { 742 path: config1.path, 743 content: JSON.stringify( 744 { 745 compilerOptions: { 746 module: "commonjs", 747 target: "es5", 748 noImplicitAny: true, 749 sourceMap: false, 750 lib: [ 751 "es5", 752 "es2015.promise" 753 ] 754 } 755 }) 756 }; 757 const host = createServerHost([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); 758 const projectService = createProjectService(host); 759 projectService.openClientFile(app.path); 760 761 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 762 checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, app.path, config1.path]); 763 764 host.writeFile(config2.path, config2.content); 765 host.checkTimeoutQueueLengthAndRun(2); 766 767 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 768 checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, libES2015Promise.path, app.path, config2.path]); 769 }); 770 771 it("should handle non-existing directories in config file", () => { 772 const f = { 773 path: "/a/src/app.ts", 774 content: "let x = 1;" 775 }; 776 const config = { 777 path: "/a/tsconfig.json", 778 content: JSON.stringify({ 779 compilerOptions: {}, 780 include: [ 781 "src/**/*", 782 "notexistingfolder/*" 783 ] 784 }) 785 }; 786 const host = createServerHost([f, config]); 787 const projectService = createProjectService(host); 788 projectService.openClientFile(f.path); 789 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 790 const project = projectService.configuredProjects.get(config.path)!; 791 assert.isTrue(project.hasOpenRef()); // f 792 793 projectService.closeClientFile(f.path); 794 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 795 assert.strictEqual(projectService.configuredProjects.get(config.path), project); 796 assert.isFalse(project.hasOpenRef()); // No files 797 assert.isFalse(project.isClosed()); 798 799 projectService.openClientFile(f.path); 800 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 801 assert.strictEqual(projectService.configuredProjects.get(config.path), project); 802 assert.isTrue(project.hasOpenRef()); // f 803 assert.isFalse(project.isClosed()); 804 }); 805 806 it("handles loads existing configured projects of external projects when lazyConfiguredProjectsFromExternalProject is disabled", () => { 807 const f1 = { 808 path: "/a/b/app.ts", 809 content: "let x = 1" 810 }; 811 const config = { 812 path: "/a/b/tsconfig.json", 813 content: JSON.stringify({}) 814 }; 815 const projectFileName = "/a/b/project.csproj"; 816 const host = createServerHost([f1, config]); 817 const service = createProjectService(host); 818 service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: true } }); 819 service.openExternalProject({ 820 projectFileName, 821 rootFiles: toExternalFiles([f1.path, config.path]), 822 options: {} 823 } as protocol.ExternalProject); 824 service.checkNumberOfProjects({ configuredProjects: 1 }); 825 const project = service.configuredProjects.get(config.path)!; 826 assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded 827 checkProjectActualFiles(project, emptyArray); 828 829 service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); 830 assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded 831 checkProjectActualFiles(project, [config.path, f1.path]); 832 833 service.closeExternalProject(projectFileName); 834 service.checkNumberOfProjects({}); 835 836 service.openExternalProject({ 837 projectFileName, 838 rootFiles: toExternalFiles([f1.path, config.path]), 839 options: {} 840 } as protocol.ExternalProject); 841 service.checkNumberOfProjects({ configuredProjects: 1 }); 842 const project2 = service.configuredProjects.get(config.path)!; 843 assert.equal(project2.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded 844 checkProjectActualFiles(project2, [config.path, f1.path]); 845 }); 846 847 it("handles creation of external project with jsconfig before jsconfig creation watcher is invoked", () => { 848 const projectFileName = `${tscWatch.projectRoot}/WebApplication36.csproj`; 849 const tsconfig: File = { 850 path: `${tscWatch.projectRoot}/tsconfig.json`, 851 content: "{}" 852 }; 853 const files = [libFile, tsconfig]; 854 const host = createServerHost(files); 855 const service = createProjectService(host); 856 857 // Create external project 858 service.openExternalProjects([{ 859 projectFileName, 860 rootFiles: [{ fileName: tsconfig.path }], 861 options: { allowJs: false } 862 }]); 863 checkNumberOfProjects(service, { configuredProjects: 1 }); 864 const configProject = service.configuredProjects.get(tsconfig.path.toLowerCase())!; 865 checkProjectActualFiles(configProject, [tsconfig.path]); 866 867 // write js file, open external project and open it for edit 868 const jsFilePath = `${tscWatch.projectRoot}/javascript.js`; 869 host.writeFile(jsFilePath, ""); 870 service.openExternalProjects([{ 871 projectFileName, 872 rootFiles: [{ fileName: tsconfig.path }, { fileName: jsFilePath }], 873 options: { allowJs: false } 874 }]); 875 service.applyChangesInOpenFiles(singleIterator({ fileName: jsFilePath, scriptKind: ScriptKind.JS, content: "" })); 876 checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); 877 checkProjectActualFiles(configProject, [tsconfig.path]); 878 const inferredProject = service.inferredProjects[0]; 879 checkProjectActualFiles(inferredProject, [libFile.path, jsFilePath]); 880 881 // write jsconfig file 882 const jsConfig: File = { 883 path: `${tscWatch.projectRoot}/jsconfig.json`, 884 content: "{}" 885 }; 886 // Dont invoke file creation watchers as the repro suggests 887 host.ensureFileOrFolder(jsConfig, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); 888 889 // Open external project 890 service.openExternalProjects([{ 891 projectFileName, 892 rootFiles: [{ fileName: jsConfig.path }, { fileName: tsconfig.path }, { fileName: jsFilePath }], 893 options: { allowJs: false } 894 }]); 895 checkNumberOfProjects(service, { configuredProjects: 2, inferredProjects: 1 }); 896 checkProjectActualFiles(configProject, [tsconfig.path]); 897 assert.isTrue(inferredProject.isOrphan()); 898 const jsConfigProject = service.configuredProjects.get(jsConfig.path.toLowerCase())!; 899 checkProjectActualFiles(jsConfigProject, [jsConfig.path, jsFilePath, libFile.path]); 900 }); 901 902 it("does not crash if external file does not exist", () => { 903 const f1 = { 904 path: "/a/file1.ts", 905 content: "let x = [1, 2];", 906 }; 907 const p1 = { 908 projectFileName: "/a/proj1.csproj", 909 rootFiles: [toExternalFile(f1.path)], 910 options: {}, 911 }; 912 913 const host = createServerHost([f1]); 914 host.require = (_initialPath, moduleName) => { 915 assert.equal(moduleName, "myplugin"); 916 return { 917 module: () => ({ 918 create(info: server.PluginCreateInfo) { 919 return Harness.LanguageService.makeDefaultProxy(info); 920 }, 921 getExternalFiles() { 922 return ["/does/not/exist"]; 923 }, 924 }), 925 error: undefined, 926 }; 927 }; 928 const session = createSession(host, { 929 globalPlugins: ["myplugin"], 930 }); 931 const projectService = session.getProjectService(); 932 // When the external project is opened, the graph will be updated, 933 // and in the process getExternalFiles() above will be called. 934 // Since the external file does not exist, there will not be a script 935 // info for it. If tsserver does not handle this case, the following 936 // method call will crash. 937 projectService.openExternalProject(p1); 938 checkNumberOfProjects(projectService, { externalProjects: 1 }); 939 }); 940 }); 941} 942