1namespace ts.projectSystem { 2 describe("unittests:: tsserver:: Projects", () => { 3 it("handles the missing files - that were added to program because they were added with ///<ref", () => { 4 const file1: File = { 5 path: "/a/b/commonFile1.ts", 6 content: `/// <reference path="commonFile2.ts"/> 7 let x = y` 8 }; 9 const host = createServerHost([file1, libFile]); 10 const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); 11 openFilesForSession([file1], session); 12 13 const getErrRequest = makeSessionRequest<server.protocol.SemanticDiagnosticsSyncRequestArgs>( 14 server.CommandNames.SemanticDiagnosticsSync, 15 { file: file1.path } 16 ); 17 18 // Two errors: CommonFile2 not found and cannot find name y 19 session.executeCommand(getErrRequest); 20 21 host.writeFile(commonFile2.path, commonFile2.content); 22 host.runQueuedTimeoutCallbacks(); 23 session.executeCommand(getErrRequest); 24 baselineTsserverLogs("projects", "handles the missing files added with tripleslash ref", session); 25 }); 26 27 it("should create new inferred projects for files excluded from a configured project", () => { 28 const configFile: File = { 29 path: "/a/b/tsconfig.json", 30 content: `{ 31 "compilerOptions": {}, 32 "files": ["${commonFile1.path}", "${commonFile2.path}"] 33 }` 34 }; 35 const files = [commonFile1, commonFile2, configFile]; 36 const host = createServerHost(files); 37 const projectService = createProjectService(host); 38 projectService.openClientFile(commonFile1.path); 39 40 const project = configuredProjectAt(projectService, 0); 41 checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); 42 configFile.content = `{ 43 "compilerOptions": {}, 44 "files": ["${commonFile1.path}"] 45 }`; 46 host.writeFile(configFile.path, configFile.content); 47 48 checkNumberOfConfiguredProjects(projectService, 1); 49 checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); 50 host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects 51 checkNumberOfConfiguredProjects(projectService, 1); 52 checkProjectRootFiles(project, [commonFile1.path]); 53 54 projectService.openClientFile(commonFile2.path); 55 checkNumberOfInferredProjects(projectService, 1); 56 }); 57 58 it("should disable features when the files are too large", () => { 59 const file1 = { 60 path: "/a/b/f1.js", 61 content: "let x =1;", 62 fileSize: 10 * 1024 * 1024 63 }; 64 const file2 = { 65 path: "/a/b/f2.js", 66 content: "let y =1;", 67 fileSize: 6 * 1024 * 1024 68 }; 69 const file3 = { 70 path: "/a/b/f3.js", 71 content: "let y =1;", 72 fileSize: 6 * 1024 * 1024 73 }; 74 75 const proj1name = "proj1", proj2name = "proj2", proj3name = "proj3"; 76 77 const host = createServerHost([file1, file2, file3]); 78 const projectService = createProjectService(host); 79 80 projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path]), options: {}, projectFileName: proj1name }); 81 const proj1 = projectService.findProject(proj1name)!; 82 assert.isTrue(proj1.languageServiceEnabled); 83 84 projectService.openExternalProject({ rootFiles: toExternalFiles([file2.path]), options: {}, projectFileName: proj2name }); 85 const proj2 = projectService.findProject(proj2name)!; 86 assert.isTrue(proj2.languageServiceEnabled); 87 88 projectService.openExternalProject({ rootFiles: toExternalFiles([file3.path]), options: {}, projectFileName: proj3name }); 89 const proj3 = projectService.findProject(proj3name)!; 90 assert.isFalse(proj3.languageServiceEnabled); 91 }); 92 93 it("should not crash when opening a file in a project with a disabled language service", () => { 94 const file1 = { 95 path: "/a/b/f1.js", 96 content: "let x =1;", 97 fileSize: 50 * 1024 * 1024 98 }; 99 const file2 = { 100 path: "/a/b/f2.js", 101 content: "let x =1;", 102 fileSize: 100 103 }; 104 105 const projName = "proj1"; 106 107 const host = createServerHost([file1, file2]); 108 const projectService = createProjectService(host, { useSingleInferredProject: true, eventHandler: noop }); 109 110 projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path, file2.path]), options: {}, projectFileName: projName }); 111 const proj1 = projectService.findProject(projName)!; 112 assert.isFalse(proj1.languageServiceEnabled); 113 114 assert.doesNotThrow(() => projectService.openClientFile(file2.path)); 115 }); 116 117 describe("ignoreConfigFiles", () => { 118 it("external project including config file", () => { 119 const file1 = { 120 path: "/a/b/f1.ts", 121 content: "let x =1;" 122 }; 123 const config1 = { 124 path: "/a/b/tsconfig.json", 125 content: JSON.stringify( 126 { 127 compilerOptions: {}, 128 files: ["f1.ts"] 129 } 130 ) 131 }; 132 133 const externalProjectName = "externalproject"; 134 const host = createServerHost([file1, config1]); 135 const projectService = createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true }); 136 projectService.openExternalProject({ 137 rootFiles: toExternalFiles([file1.path, config1.path]), 138 options: {}, 139 projectFileName: externalProjectName 140 }); 141 142 checkNumberOfProjects(projectService, { externalProjects: 1 }); 143 const proj = projectService.externalProjects[0]; 144 assert.isDefined(proj); 145 146 assert.isTrue(proj.fileExists(file1.path)); 147 }); 148 149 it("loose file included in config file (openClientFile)", () => { 150 const file1 = { 151 path: "/a/b/f1.ts", 152 content: "let x =1;" 153 }; 154 const config1 = { 155 path: "/a/b/tsconfig.json", 156 content: JSON.stringify( 157 { 158 compilerOptions: {}, 159 files: ["f1.ts"] 160 } 161 ) 162 }; 163 164 const host = createServerHost([file1, config1]); 165 const projectService = createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true }); 166 projectService.openClientFile(file1.path, file1.content); 167 168 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 169 const proj = projectService.inferredProjects[0]; 170 assert.isDefined(proj); 171 172 assert.isTrue(proj.fileExists(file1.path)); 173 }); 174 175 it("loose file included in config file (applyCodeChanges)", () => { 176 const file1 = { 177 path: "/a/b/f1.ts", 178 content: "let x =1;" 179 }; 180 const config1 = { 181 path: "/a/b/tsconfig.json", 182 content: JSON.stringify( 183 { 184 compilerOptions: {}, 185 files: ["f1.ts"] 186 } 187 ) 188 }; 189 190 const host = createServerHost([file1, config1]); 191 const projectService = createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true }); 192 projectService.applyChangesInOpenFiles(singleIterator({ fileName: file1.path, content: file1.content })); 193 194 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 195 const proj = projectService.inferredProjects[0]; 196 assert.isDefined(proj); 197 198 assert.isTrue(proj.fileExists(file1.path)); 199 }); 200 }); 201 202 it("reload regular file after closing", () => { 203 const f1 = { 204 path: "/a/b/app.ts", 205 content: "x." 206 }; 207 const f2 = { 208 path: "/a/b/lib.ts", 209 content: "let x: number;" 210 }; 211 212 const host = createServerHost([f1, f2, libFile]); 213 const service = createProjectService(host); 214 service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: toExternalFiles([f1.path, f2.path]), options: {} }); 215 216 service.openClientFile(f1.path); 217 service.openClientFile(f2.path, "let x: string"); 218 219 service.checkNumberOfProjects({ externalProjects: 1 }); 220 checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); 221 222 const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!; 223 // should contain completions for string 224 assert.isTrue(completions1.entries.some(e => e.name === "charAt"), "should contain 'charAt'"); 225 assert.isFalse(completions1.entries.some(e => e.name === "toExponential"), "should not contain 'toExponential'"); 226 227 service.closeClientFile(f2.path); 228 const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!; 229 // should contain completions for string 230 assert.isFalse(completions2.entries.some(e => e.name === "charAt"), "should not contain 'charAt'"); 231 assert.isTrue(completions2.entries.some(e => e.name === "toExponential"), "should contain 'toExponential'"); 232 }); 233 234 it("clear mixed content file after closing", () => { 235 const f1 = { 236 path: "/a/b/app.ts", 237 content: " " 238 }; 239 const f2 = { 240 path: "/a/b/lib.html", 241 content: "<html/>" 242 }; 243 244 const host = createServerHost([f1, f2, libFile]); 245 const service = createProjectService(host); 246 service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: [{ fileName: f1.path }, { fileName: f2.path, hasMixedContent: true }], options: {} }); 247 248 service.openClientFile(f1.path); 249 service.openClientFile(f2.path, "let somelongname: string"); 250 251 service.checkNumberOfProjects({ externalProjects: 1 }); 252 checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); 253 254 const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!; 255 assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'"); 256 257 service.closeClientFile(f2.path); 258 const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!; 259 assert.isFalse(completions2.entries.some(e => e.name === "somelongname"), "should not contain 'somelongname'"); 260 const sf2 = service.externalProjects[0].getLanguageService().getProgram()!.getSourceFile(f2.path)!; 261 assert.equal(sf2.text, ""); 262 }); 263 264 it("changes in closed files are reflected in project structure", () => { 265 const file1 = { 266 path: "/a/b/f1.ts", 267 content: `export * from "./f2"` 268 }; 269 const file2 = { 270 path: "/a/b/f2.ts", 271 content: `export let x = 1` 272 }; 273 const file3 = { 274 path: "/a/c/f3.ts", 275 content: `export let y = 1;` 276 }; 277 const host = createServerHost([file1, file2, file3]); 278 const projectService = createProjectService(host); 279 280 projectService.openClientFile(file1.path); 281 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 282 const inferredProject0 = projectService.inferredProjects[0]; 283 checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); 284 285 projectService.openClientFile(file3.path); 286 checkNumberOfProjects(projectService, { inferredProjects: 2 }); 287 assert.strictEqual(projectService.inferredProjects[0], inferredProject0); 288 checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); 289 const inferredProject1 = projectService.inferredProjects[1]; 290 checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); 291 292 host.writeFile(file2.path, `export * from "../c/f3"`); // now inferred project should inclule file3 293 host.checkTimeoutQueueLengthAndRun(2); 294 checkNumberOfProjects(projectService, { inferredProjects: 2 }); 295 assert.strictEqual(projectService.inferredProjects[0], inferredProject0); 296 checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); 297 assert.strictEqual(projectService.inferredProjects[1], inferredProject1); 298 assert.isTrue(inferredProject1.isOrphan()); 299 }); 300 301 it("deleted files affect project structure", () => { 302 const file1 = { 303 path: "/a/b/f1.ts", 304 content: `export * from "./f2"` 305 }; 306 const file2 = { 307 path: "/a/b/f2.ts", 308 content: `export * from "../c/f3"` 309 }; 310 const file3 = { 311 path: "/a/c/f3.ts", 312 content: `export let y = 1;` 313 }; 314 const host = createServerHost([file1, file2, file3]); 315 const projectService = createProjectService(host); 316 317 projectService.openClientFile(file1.path); 318 319 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 320 321 checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); 322 323 projectService.openClientFile(file3.path); 324 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 325 326 host.deleteFile(file2.path); 327 host.checkTimeoutQueueLengthAndRun(2); 328 329 checkNumberOfProjects(projectService, { inferredProjects: 2 }); 330 331 checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); 332 checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); 333 }); 334 335 it("ignores files excluded by a custom safe type list", () => { 336 const file1 = { 337 path: "/a/b/f1.js", 338 content: "export let x = 5" 339 }; 340 const office = { 341 path: "/lib/duckquack-3.min.js", 342 content: "whoa do @@ not parse me ok thanks!!!" 343 }; 344 const host = createServerHost([file1, office, customTypesMap]); 345 const projectService = createProjectService(host); 346 try { 347 projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, office.path]) }); 348 const proj = projectService.externalProjects[0]; 349 assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); 350 assert.deepEqual(proj.getTypeAcquisition().include, ["duck-types"]); 351 } 352 finally { 353 projectService.resetSafeList(); 354 } 355 }); 356 357 it("file with name constructor.js doesnt cause issue with typeAcquisition when safe type list", () => { 358 const file1 = { 359 path: "/a/b/f1.js", 360 content: `export let x = 5; import { s } from "s"` 361 }; 362 const constructorFile = { 363 path: "/a/b/constructor.js", 364 content: "const x = 10;" 365 }; 366 const bliss = { 367 path: "/a/b/bliss.js", 368 content: "export function is() { return true; }" 369 }; 370 const host = createServerHost([file1, libFile, constructorFile, bliss, customTypesMap]); 371 let request: string | undefined; 372 const cachePath = "/a/data"; 373 const typingsInstaller: server.ITypingsInstaller = { 374 isKnownTypesPackageName: returnFalse, 375 installPackage: notImplemented, 376 enqueueInstallTypingsRequest: (proj, typeAcquisition, unresolvedImports) => { 377 assert.isUndefined(request); 378 request = JSON.stringify(server.createInstallTypingsRequest(proj, typeAcquisition, unresolvedImports || server.emptyArray, cachePath)); 379 }, 380 attach: noop, 381 onProjectClosed: noop, 382 globalTypingsCacheLocation: cachePath 383 }; 384 385 const projectName = "project"; 386 const projectService = createProjectService(host, { typingsInstaller }); 387 projectService.openExternalProject({ projectFileName: projectName, options: {}, rootFiles: toExternalFiles([file1.path, constructorFile.path, bliss.path]) }); 388 assert.equal(request, JSON.stringify({ 389 projectName, 390 fileNames: [libFile.path, file1.path, constructorFile.path, bliss.path], 391 compilerOptions: { allowNonTsExtensions: true, noEmitForJsFiles: true }, 392 typeAcquisition: { include: ["blissfuljs"], exclude: [], enable: true }, 393 unresolvedImports: ["s"], 394 projectRootPath: "/", 395 cachePath, 396 kind: "discover" 397 })); 398 const response = JSON.parse(request!); 399 request = undefined; 400 projectService.updateTypingsForProject({ 401 kind: "action::set", 402 projectName: response.projectName, 403 typeAcquisition: response.typeAcquisition, 404 compilerOptions: response.compilerOptions, 405 typings: emptyArray, 406 unresolvedImports: response.unresolvedImports, 407 }); 408 409 host.checkTimeoutQueueLength(0); 410 assert.isUndefined(request); 411 }); 412 413 it("ignores files excluded by the default type list", () => { 414 const file1 = { 415 path: "/a/b/f1.js", 416 content: "export let x = 5" 417 }; 418 const minFile = { 419 path: "/c/moment.min.js", 420 content: "unspecified" 421 }; 422 const kendoFile1 = { 423 path: "/q/lib/kendo/kendo.all.min.js", 424 content: "unspecified" 425 }; 426 const kendoFile2 = { 427 path: "/q/lib/kendo/kendo.ui.min.js", 428 content: "unspecified" 429 }; 430 const kendoFile3 = { 431 path: "/q/lib/kendo-ui/kendo.all.js", 432 content: "unspecified" 433 }; 434 const officeFile1 = { 435 path: "/scripts/Office/1/excel-15.debug.js", 436 content: "unspecified" 437 }; 438 const officeFile2 = { 439 path: "/scripts/Office/1/powerpoint.js", 440 content: "unspecified" 441 }; 442 const files = [file1, minFile, kendoFile1, kendoFile2, kendoFile3, officeFile1, officeFile2]; 443 const host = createServerHost(files); 444 const projectService = createProjectService(host); 445 try { 446 projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles(files.map(f => f.path)) }); 447 const proj = projectService.externalProjects[0]; 448 assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); 449 assert.deepEqual(proj.getTypeAcquisition().include, ["kendo-ui", "office"]); 450 } 451 finally { 452 projectService.resetSafeList(); 453 } 454 }); 455 456 it("removes version numbers correctly", () => { 457 const testData: [string, string][] = [ 458 ["jquery-max", "jquery-max"], 459 ["jquery.min", "jquery"], 460 ["jquery-min.4.2.3", "jquery"], 461 ["jquery.min.4.2.1", "jquery"], 462 ["minimum", "minimum"], 463 ["min", "min"], 464 ["min.3.2", "min"], 465 ["jquery", "jquery"] 466 ]; 467 for (const t of testData) { 468 assert.equal(removeMinAndVersionNumbers(t[0]), t[1], t[0]); 469 } 470 }); 471 472 it("ignores files excluded by a legacy safe type list", () => { 473 const file1 = { 474 path: "/a/b/bliss.js", 475 content: "let x = 5" 476 }; 477 const file2 = { 478 path: "/a/b/foo.js", 479 content: "" 480 }; 481 const file3 = { 482 path: "/a/b/Bacon.js", 483 content: "let y = 5" 484 }; 485 const host = createServerHost([file1, file2, file3, customTypesMap]); 486 const projectService = createProjectService(host); 487 try { 488 projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]), typeAcquisition: { enable: true } }); 489 const proj = projectService.externalProjects[0]; 490 assert.deepEqual(proj.getFileNames(), [file2.path]); 491 } 492 finally { 493 projectService.resetSafeList(); 494 } 495 }); 496 497 it("correctly migrate files between projects", () => { 498 const file1 = { 499 path: "/a/b/f1.ts", 500 content: ` 501 export * from "../c/f2"; 502 export * from "../d/f3";` 503 }; 504 const file2 = { 505 path: "/a/c/f2.ts", 506 content: "export let x = 1;" 507 }; 508 const file3 = { 509 path: "/a/d/f3.ts", 510 content: "export let y = 1;" 511 }; 512 const host = createServerHost([file1, file2, file3]); 513 const projectService = createProjectService(host); 514 515 projectService.openClientFile(file2.path); 516 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 517 checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); 518 let inferredProjects = projectService.inferredProjects.slice(); 519 520 projectService.openClientFile(file3.path); 521 checkNumberOfProjects(projectService, { inferredProjects: 2 }); 522 assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); 523 checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); 524 checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); 525 inferredProjects = projectService.inferredProjects.slice(); 526 527 projectService.openClientFile(file1.path); 528 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 529 assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[0]); 530 assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[1]); 531 checkProjectRootFiles(projectService.inferredProjects[0], [file1.path]); 532 checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); 533 inferredProjects = projectService.inferredProjects.slice(); 534 535 projectService.closeClientFile(file1.path); 536 checkNumberOfProjects(projectService, { inferredProjects: 3 }); 537 assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); 538 assert.isTrue(projectService.inferredProjects[0].isOrphan()); 539 checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); 540 checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); 541 inferredProjects = projectService.inferredProjects.slice(); 542 543 projectService.closeClientFile(file3.path); 544 checkNumberOfProjects(projectService, { inferredProjects: 3 }); 545 assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); 546 assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); 547 assert.strictEqual(projectService.inferredProjects[2], inferredProjects[2]); 548 assert.isTrue(projectService.inferredProjects[0].isOrphan()); 549 checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); 550 assert.isTrue(projectService.inferredProjects[2].isOrphan()); 551 552 projectService.openClientFile(file3.path); 553 checkNumberOfProjects(projectService, { inferredProjects: 2 }); 554 assert.strictEqual(projectService.inferredProjects[0], inferredProjects[2]); 555 assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); 556 checkProjectActualFiles(projectService.inferredProjects[0], [file3.path]); 557 checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); 558 }); 559 560 it("regression test for crash in acquireOrUpdateDocument", () => { 561 const tsFile = { 562 fileName: "/a/b/file1.ts", 563 path: "/a/b/file1.ts", 564 content: "" 565 }; 566 const jsFile = { 567 path: "/a/b/file1.js", 568 content: "var x = 10;", 569 fileName: "/a/b/file1.js", 570 scriptKind: "JS" as const 571 }; 572 573 const host = createServerHost([]); 574 const projectService = createProjectService(host); 575 projectService.applyChangesInOpenFiles(singleIterator(tsFile)); 576 const projs = projectService.synchronizeProjectList([]); 577 projectService.findProject(projs[0].info!.projectName)!.getLanguageService().getNavigationBarItems(tsFile.fileName); 578 projectService.synchronizeProjectList([projs[0].info!]); 579 projectService.applyChangesInOpenFiles(singleIterator(jsFile)); 580 }); 581 582 it("config file is deleted", () => { 583 const file1 = { 584 path: "/a/b/f1.ts", 585 content: "let x = 1;" 586 }; 587 const file2 = { 588 path: "/a/b/f2.ts", 589 content: "let y = 2;" 590 }; 591 const config = { 592 path: "/a/b/tsconfig.json", 593 content: JSON.stringify({ compilerOptions: {} }) 594 }; 595 const host = createServerHost([file1, file2, config]); 596 const projectService = createProjectService(host); 597 598 projectService.openClientFile(file1.path); 599 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 600 checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); 601 602 projectService.openClientFile(file2.path); 603 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 604 checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); 605 606 host.deleteFile(config.path); 607 host.checkTimeoutQueueLengthAndRun(1); 608 checkNumberOfProjects(projectService, { inferredProjects: 2 }); 609 checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); 610 checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); 611 }); 612 613 it("loading files with correct priority", () => { 614 const f1 = { 615 path: "/a/main.ts", 616 content: "let x = 1" 617 }; 618 const f2 = { 619 path: "/a/main.js", 620 content: "var y = 1" 621 }; 622 const f3 = { 623 path: "/main.js", 624 content: "var y = 1" 625 }; 626 const config = { 627 path: "/a/tsconfig.json", 628 content: JSON.stringify({ 629 compilerOptions: { allowJs: true } 630 }) 631 }; 632 const host = createServerHost([f1, f2, f3, config]); 633 const projectService = createProjectService(host); 634 projectService.setHostConfiguration({ 635 extraFileExtensions: [ 636 { extension: ".js", isMixedContent: false }, 637 { extension: ".html", isMixedContent: true } 638 ] 639 }); 640 projectService.openClientFile(f1.path); 641 projectService.checkNumberOfProjects({ configuredProjects: 1 }); 642 checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]); 643 644 // Since f2 refers to config file as the default project, it needs to be kept alive 645 projectService.closeClientFile(f1.path); 646 projectService.openClientFile(f2.path); 647 projectService.checkNumberOfProjects({ inferredProjects: 1, configuredProjects: 1 }); 648 assert.isDefined(projectService.configuredProjects.get(config.path)); 649 checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]); 650 651 // Should close configured project with next file open 652 projectService.closeClientFile(f2.path); 653 projectService.openClientFile(f3.path); 654 projectService.checkNumberOfProjects({ inferredProjects: 1 }); 655 assert.isUndefined(projectService.configuredProjects.get(config.path)); 656 checkProjectActualFiles(projectService.inferredProjects[0], [f3.path]); 657 }); 658 659 it("tsconfig script block support", () => { 660 const file1 = { 661 path: "/a/b/f1.ts", 662 content: ` ` 663 }; 664 const file2 = { 665 path: "/a/b/f2.html", 666 content: `var hello = "hello";` 667 }; 668 const config = { 669 path: "/a/b/tsconfig.json", 670 content: JSON.stringify({ compilerOptions: { allowJs: true } }) 671 }; 672 const host = createServerHost([file1, file2, config]); 673 const session = createSession(host); 674 openFilesForSession([file1], session); 675 const projectService = session.getProjectService(); 676 677 // HTML file will not be included in any projects yet 678 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 679 const configuredProj = configuredProjectAt(projectService, 0); 680 checkProjectActualFiles(configuredProj, [file1.path, config.path]); 681 682 // Specify .html extension as mixed content 683 const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; 684 const configureHostRequest = makeSessionRequest<protocol.ConfigureRequestArguments>(CommandNames.Configure, { extraFileExtensions }); 685 session.executeCommand(configureHostRequest); 686 687 // The configured project should now be updated to include html file 688 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 689 assert.strictEqual(configuredProjectAt(projectService, 0), configuredProj, "Same configured project should be updated"); 690 checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); 691 692 // Open HTML file 693 projectService.applyChangesInOpenFiles(singleIterator({ 694 fileName: file2.path, 695 hasMixedContent: true, 696 scriptKind: ScriptKind.JS, 697 content: `var hello = "hello";` 698 })); 699 // Now HTML file is included in the project 700 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 701 checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); 702 703 // Check identifiers defined in HTML content are available in .ts file 704 const project = configuredProjectAt(projectService, 0); 705 let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, emptyOptions); 706 assert(completions && some(completions.entries, e => e.name === "hello"), `expected entry hello to be in completion list`); 707 708 // Close HTML file 709 projectService.applyChangesInOpenFiles( 710 /*openFiles*/ undefined, 711 /*changedFiles*/ undefined, 712 /*closedFiles*/[file2.path]); 713 714 // HTML file is still included in project 715 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 716 checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); 717 718 // Check identifiers defined in HTML content are not available in .ts file 719 completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, emptyOptions); 720 assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`); 721 }); 722 723 it("no tsconfig script block diagnostic errors", () => { 724 725 // #1. Ensure no diagnostic errors when allowJs is true 726 const file1 = { 727 path: "/a/b/f1.ts", 728 content: ` ` 729 }; 730 const file2 = { 731 path: "/a/b/f2.html", 732 content: `var hello = "hello";` 733 }; 734 const config1 = { 735 path: "/a/b/tsconfig.json", 736 content: JSON.stringify({ compilerOptions: { allowJs: true } }) 737 }; 738 739 let host = createServerHost([file1, file2, config1, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); 740 let session = createSession(host); 741 742 // Specify .html extension as mixed content in a configure host request 743 const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; 744 const configureHostRequest = makeSessionRequest<protocol.ConfigureRequestArguments>(CommandNames.Configure, { extraFileExtensions }); 745 session.executeCommand(configureHostRequest); 746 747 openFilesForSession([file1], session); 748 let projectService = session.getProjectService(); 749 750 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 751 752 let diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); 753 assert.deepEqual(diagnostics, []); 754 755 // #2. Ensure no errors when allowJs is false 756 const config2 = { 757 path: "/a/b/tsconfig.json", 758 content: JSON.stringify({ compilerOptions: { allowJs: false } }) 759 }; 760 761 host = createServerHost([file1, file2, config2, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); 762 session = createSession(host); 763 764 session.executeCommand(configureHostRequest); 765 766 openFilesForSession([file1], session); 767 projectService = session.getProjectService(); 768 769 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 770 771 diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); 772 assert.deepEqual(diagnostics, []); 773 774 // #3. Ensure no errors when compiler options aren't specified 775 const config3 = { 776 path: "/a/b/tsconfig.json", 777 content: JSON.stringify({}) 778 }; 779 780 host = createServerHost([file1, file2, config3, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); 781 session = createSession(host); 782 783 session.executeCommand(configureHostRequest); 784 785 openFilesForSession([file1], session); 786 projectService = session.getProjectService(); 787 788 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 789 790 diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); 791 assert.deepEqual(diagnostics, []); 792 793 // #4. Ensure no errors when files are explicitly specified in tsconfig 794 const config4 = { 795 path: "/a/b/tsconfig.json", 796 content: JSON.stringify({ compilerOptions: { allowJs: true }, files: [file1.path, file2.path] }) 797 }; 798 799 host = createServerHost([file1, file2, config4, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); 800 session = createSession(host); 801 802 session.executeCommand(configureHostRequest); 803 804 openFilesForSession([file1], session); 805 projectService = session.getProjectService(); 806 807 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 808 809 diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); 810 assert.deepEqual(diagnostics, []); 811 812 // #4. Ensure no errors when files are explicitly excluded in tsconfig 813 const config5 = { 814 path: "/a/b/tsconfig.json", 815 content: JSON.stringify({ compilerOptions: { allowJs: true }, exclude: [file2.path] }) 816 }; 817 818 host = createServerHost([file1, file2, config5, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); 819 session = createSession(host); 820 821 session.executeCommand(configureHostRequest); 822 823 openFilesForSession([file1], session); 824 projectService = session.getProjectService(); 825 826 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 827 828 diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); 829 assert.deepEqual(diagnostics, []); 830 }); 831 832 it("project structure update is deferred if files are not added\removed", () => { 833 const file1 = { 834 path: "/a/b/f1.ts", 835 content: `import {x} from "./f2"` 836 }; 837 const file2 = { 838 path: "/a/b/f2.ts", 839 content: "export let x = 1" 840 }; 841 const host = createServerHost([file1, file2]); 842 const projectService = createProjectService(host); 843 844 projectService.openClientFile(file1.path); 845 projectService.openClientFile(file2.path); 846 847 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 848 projectService.applyChangesInOpenFiles( 849 /*openFiles*/ undefined, 850 /*changedFiles*/singleIterator({ fileName: file1.path, changes: singleIterator({ span: createTextSpan(0, file1.path.length), newText: "let y = 1" }) }), 851 /*closedFiles*/ undefined); 852 853 checkNumberOfProjects(projectService, { inferredProjects: 1 }); 854 projectService.ensureInferredProjectsUpToDate_TestOnly(); 855 checkNumberOfProjects(projectService, { inferredProjects: 2 }); 856 }); 857 858 it("files with mixed content are handled correctly", () => { 859 const file1 = { 860 path: "/a/b/f1.html", 861 content: `<html><script language="javascript">var x = 1;</></html>` 862 }; 863 const host = createServerHost([file1]); 864 const projectService = createProjectService(host, { logger: createLoggerWithInMemoryLogs(host) }); 865 const projectFileName = "projectFileName"; 866 projectService.openExternalProject({ projectFileName, options: {}, rootFiles: [{ fileName: file1.path, scriptKind: ScriptKind.JS, hasMixedContent: true }] }); 867 868 869 const project = projectService.externalProjects[0]; 870 871 const scriptInfo = project.getScriptInfo(file1.path)!; 872 const snap = scriptInfo.getSnapshot(); 873 const actualText = getSnapshotText(snap); 874 projectService.logger.info(`Text of${file1.path}: ${actualText}`); 875 876 projectService.openClientFile(file1.path, `var x = 1;`); 877 project.updateGraph(); 878 879 const quickInfo = project.getLanguageService().getQuickInfoAtPosition(file1.path, 4)!; 880 projectService.logger.info(`QuickInfo : ${quickInfo.kind}`); 881 882 projectService.closeClientFile(file1.path); 883 884 const scriptInfo2 = project.getScriptInfo(file1.path)!; 885 const actualText2 = getSnapshotText(scriptInfo2.getSnapshot()); 886 projectService.logger.info(`Text of${file1.path}: ${actualText2}`); 887 baselineTsserverLogs("projects", "files with mixed content are handled correctly", projectService); 888 }); 889 890 it("syntax tree cache handles changes in project settings", () => { 891 const file1 = { 892 path: "/a/b/app.ts", 893 content: "{x: 1}" 894 }; 895 const host = createServerHost([file1]); 896 const projectService = createProjectService(host, { useSingleInferredProject: true }); 897 projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES5, allowJs: false }); 898 projectService.openClientFile(file1.path); 899 projectService.inferredProjects[0].getLanguageService(/*ensureSynchronized*/ false).getOutliningSpans(file1.path); 900 projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES5, allowJs: true }); 901 projectService.getScriptInfo(file1.path)!.editContent(0, 0, " "); 902 projectService.inferredProjects[0].getLanguageService(/*ensureSynchronized*/ false).getOutliningSpans(file1.path); 903 projectService.closeClientFile(file1.path); 904 }); 905 906 it("File in multiple projects at opened and closed correctly", () => { 907 const file1 = { 908 path: "/a/b/app.ts", 909 content: "let x = 1;" 910 }; 911 const file2 = { 912 path: "/a/c/f.ts", 913 content: `/// <reference path="../b/app.ts"/>` 914 }; 915 const tsconfig1 = { 916 path: "/a/c/tsconfig.json", 917 content: "{}" 918 }; 919 const tsconfig2 = { 920 path: "/a/b/tsconfig.json", 921 content: "{}" 922 }; 923 const host = createServerHost([file1, file2, tsconfig1, tsconfig2]); 924 const projectService = createProjectService(host); 925 926 projectService.openClientFile(file2.path); 927 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 928 const project1 = projectService.configuredProjects.get(tsconfig1.path)!; 929 assert.isTrue(project1.hasOpenRef(), "Has open ref count in project1 - 1"); // file2 930 assert.equal(project1.getScriptInfo(file2.path)!.containingProjects.length, 1, "containing projects count"); 931 assert.isFalse(project1.isClosed()); 932 933 projectService.openClientFile(file1.path); 934 checkNumberOfProjects(projectService, { configuredProjects: 2 }); 935 assert.isTrue(project1.hasOpenRef(), "Has open ref count in project1 - 2"); // file2 936 assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1); 937 assert.isFalse(project1.isClosed()); 938 939 const project2 = projectService.configuredProjects.get(tsconfig2.path)!; 940 assert.isTrue(project2.hasOpenRef(), "Has open ref count in project2 - 2"); // file1 941 assert.isFalse(project2.isClosed()); 942 943 assert.equal(project1.getScriptInfo(file1.path)!.containingProjects.length, 2, `${file1.path} containing projects count`); 944 assert.equal(project1.getScriptInfo(file2.path)!.containingProjects.length, 1, `${file2.path} containing projects count`); 945 946 projectService.closeClientFile(file2.path); 947 checkNumberOfProjects(projectService, { configuredProjects: 2 }); 948 assert.isFalse(project1.hasOpenRef(), "Has open ref count in project1 - 3"); // No files 949 assert.isTrue(project2.hasOpenRef(), "Has open ref count in project2 - 3"); // file1 950 assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1); 951 assert.strictEqual(projectService.configuredProjects.get(tsconfig2.path), project2); 952 assert.isFalse(project1.isClosed()); 953 assert.isFalse(project2.isClosed()); 954 955 projectService.closeClientFile(file1.path); 956 checkNumberOfProjects(projectService, { configuredProjects: 2 }); 957 assert.isFalse(project1.hasOpenRef(), "Has open ref count in project1 - 4"); // No files 958 assert.isFalse(project2.hasOpenRef(), "Has open ref count in project2 - 4"); // No files 959 assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1); 960 assert.strictEqual(projectService.configuredProjects.get(tsconfig2.path), project2); 961 assert.isFalse(project1.isClosed()); 962 assert.isFalse(project2.isClosed()); 963 964 projectService.openClientFile(file2.path); 965 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 966 assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1); 967 assert.isUndefined(projectService.configuredProjects.get(tsconfig2.path)); 968 assert.isTrue(project1.hasOpenRef(), "Has open ref count in project1 - 5"); // file2 969 assert.isFalse(project1.isClosed()); 970 assert.isTrue(project2.isClosed()); 971 }); 972 973 it("snapshot from different caches are incompatible", () => { 974 const f1 = { 975 path: "/a/b/app.ts", 976 content: "let x = 1;" 977 }; 978 const host = createServerHost([f1]); 979 const projectFileName = "/a/b/proj.csproj"; 980 const projectService = createProjectService(host); 981 projectService.openExternalProject({ 982 projectFileName, 983 rootFiles: [toExternalFile(f1.path)], 984 options: {} 985 }); 986 projectService.openClientFile(f1.path, "let x = 1;\nlet y = 2;"); 987 988 projectService.checkNumberOfProjects({ externalProjects: 1 }); 989 projectService.externalProjects[0].getLanguageService(/*ensureSynchronized*/ false).getNavigationBarItems(f1.path); 990 projectService.closeClientFile(f1.path); 991 992 projectService.openClientFile(f1.path); 993 projectService.checkNumberOfProjects({ externalProjects: 1 }); 994 const navbar = projectService.externalProjects[0].getLanguageService(/*ensureSynchronized*/ false).getNavigationBarItems(f1.path); 995 assert.equal(navbar[0].spans[0].length, f1.content.length); 996 }); 997 998 it("Getting errors from closed script info does not throw exception (because of getting project from orphan script info)", () => { 999 const f1 = { 1000 path: "/a/b/app.ts", 1001 content: "let x = 1;" 1002 }; 1003 const config = { 1004 path: "/a/b/tsconfig.json", 1005 content: JSON.stringify({ compilerOptions: {} }) 1006 }; 1007 const host = createServerHost([f1, libFile, config]); 1008 const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); 1009 session.executeCommandSeq({ 1010 command: server.CommandNames.Open, 1011 arguments: { 1012 file: f1.path 1013 } 1014 } as protocol.OpenRequest); 1015 session.executeCommandSeq({ 1016 command: server.CommandNames.Close, 1017 arguments: { 1018 file: f1.path 1019 } 1020 } as protocol.CloseRequest); 1021 session.executeCommandSeq({ 1022 command: server.CommandNames.Geterr, 1023 arguments: { 1024 delay: 0, 1025 files: [f1.path] 1026 } 1027 } as protocol.GeterrRequest); 1028 baselineTsserverLogs("projects", "getting errors from closed script info does not throw exception because of getting project from orphan script info", session); 1029 }); 1030 1031 it("Properly handle Windows-style outDir", () => { 1032 const configFile: File = { 1033 path: "C:\\a\\tsconfig.json", 1034 content: JSON.stringify({ 1035 compilerOptions: { 1036 outDir: `C:\\a\\b` 1037 }, 1038 include: ["*.ts"] 1039 }) 1040 }; 1041 const file1: File = { 1042 path: "C:\\a\\f1.ts", 1043 content: "let x = 1;" 1044 }; 1045 1046 const host = createServerHost([file1, configFile], { windowsStyleRoot: "c:/" }); 1047 const projectService = createProjectService(host); 1048 1049 projectService.openClientFile(file1.path); 1050 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 1051 const project = configuredProjectAt(projectService, 0); 1052 checkProjectActualFiles(project, [normalizePath(file1.path), normalizePath(configFile.path)]); 1053 const options = project.getCompilerOptions(); 1054 assert.equal(options.outDir, "C:/a/b", ""); 1055 }); 1056 1057 it("files opened and closed affecting multiple projects", () => { 1058 const file: File = { 1059 path: "/a/b/projects/config/file.ts", 1060 content: `import {a} from "../files/file1"; export let b = a;` 1061 }; 1062 const config: File = { 1063 path: "/a/b/projects/config/tsconfig.json", 1064 content: "" 1065 }; 1066 const filesFile1: File = { 1067 path: "/a/b/projects/files/file1.ts", 1068 content: "export let a = 10;" 1069 }; 1070 const filesFile2: File = { 1071 path: "/a/b/projects/files/file2.ts", 1072 content: "export let aa = 10;" 1073 }; 1074 1075 const files = [config, file, filesFile1, filesFile2, libFile]; 1076 const host = createServerHost(files); 1077 const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); 1078 // Create configured project 1079 session.executeCommandSeq<protocol.OpenRequest>({ 1080 command: protocol.CommandTypes.Open, 1081 arguments: { 1082 file: file.path 1083 } 1084 }); 1085 1086 // open files/file1 = should not create another project 1087 session.executeCommandSeq<protocol.OpenRequest>({ 1088 command: protocol.CommandTypes.Open, 1089 arguments: { 1090 file: filesFile1.path 1091 } 1092 }); 1093 1094 // Close the file = should still have project 1095 session.executeCommandSeq<protocol.CloseRequest>({ 1096 command: protocol.CommandTypes.Close, 1097 arguments: { 1098 file: file.path 1099 } 1100 }); 1101 1102 // Open files/file2 - should create inferred project and close configured project 1103 session.executeCommandSeq<protocol.OpenRequest>({ 1104 command: protocol.CommandTypes.Open, 1105 arguments: { 1106 file: filesFile2.path 1107 } 1108 }); 1109 1110 // Actions on file1 would result in assert 1111 session.executeCommandSeq<protocol.OccurrencesRequest>({ 1112 command: protocol.CommandTypes.Occurrences, 1113 arguments: { 1114 file: filesFile1.path, 1115 line: 1, 1116 offset: filesFile1.content.indexOf("a") 1117 } 1118 }); 1119 1120 baselineTsserverLogs("projects", "files opened and closed affecting multiple projects", session); 1121 }); 1122 1123 it("requests are done on file on pendingReload but has svc for previous version", () => { 1124 const file1: File = { 1125 path: `${tscWatch.projectRoot}/src/file1.ts`, 1126 content: `import { y } from "./file2"; let x = 10;` 1127 }; 1128 const file2: File = { 1129 path: `${tscWatch.projectRoot}/src/file2.ts`, 1130 content: "export let y = 10;" 1131 }; 1132 const config: File = { 1133 path: `${tscWatch.projectRoot}/tsconfig.json`, 1134 content: "{}" 1135 }; 1136 const files = [file1, file2, libFile, config]; 1137 const host = createServerHost(files); 1138 const session = createSession(host); 1139 session.executeCommandSeq<protocol.OpenRequest>({ 1140 command: protocol.CommandTypes.Open, 1141 arguments: { file: file2.path, fileContent: file2.content } 1142 }); 1143 session.executeCommandSeq<protocol.OpenRequest>({ 1144 command: protocol.CommandTypes.Open, 1145 arguments: { file: file1.path } 1146 }); 1147 session.executeCommandSeq<protocol.CloseRequest>({ 1148 command: protocol.CommandTypes.Close, 1149 arguments: { file: file2.path } 1150 }); 1151 1152 file2.content += "export let z = 10;"; 1153 host.writeFile(file2.path, file2.content); 1154 // Do not let the timeout runs, before executing command 1155 const startOffset = file2.content.indexOf("y") + 1; 1156 session.executeCommandSeq<protocol.GetApplicableRefactorsRequest>({ 1157 command: protocol.CommandTypes.GetApplicableRefactors, 1158 arguments: { file: file2.path, startLine: 1, startOffset, endLine: 1, endOffset: startOffset + 1 } 1159 }); 1160 }); 1161 1162 describe("includes deferred files in the project context", () => { 1163 function verifyDeferredContext(lazyConfiguredProjectsFromExternalProject: boolean) { 1164 const file1 = { 1165 path: "/a.deferred", 1166 content: "const a = 1;" 1167 }; 1168 // Deferred extensions should not affect JS files. 1169 const file2 = { 1170 path: "/b.js", 1171 content: "const b = 1;" 1172 }; 1173 const tsconfig = { 1174 path: "/tsconfig.json", 1175 content: "" 1176 }; 1177 1178 const host = createServerHost([file1, file2, tsconfig]); 1179 const session = createSession(host); 1180 const projectService = session.getProjectService(); 1181 session.executeCommandSeq<protocol.ConfigureRequest>({ 1182 command: protocol.CommandTypes.Configure, 1183 arguments: { preferences: { lazyConfiguredProjectsFromExternalProject } } 1184 }); 1185 1186 // Configure the deferred extension. 1187 const extraFileExtensions = [{ extension: ".deferred", scriptKind: ScriptKind.Deferred, isMixedContent: true }]; 1188 const configureHostRequest = makeSessionRequest<protocol.ConfigureRequestArguments>(CommandNames.Configure, { extraFileExtensions }); 1189 session.executeCommand(configureHostRequest); 1190 1191 // Open external project 1192 const projectName = "/proj1"; 1193 projectService.openExternalProject({ 1194 projectFileName: projectName, 1195 rootFiles: toExternalFiles([file1.path, file2.path, tsconfig.path]), 1196 options: {} 1197 }); 1198 1199 // Assert 1200 checkNumberOfProjects(projectService, { configuredProjects: 1 }); 1201 1202 const configuredProject = configuredProjectAt(projectService, 0); 1203 if (lazyConfiguredProjectsFromExternalProject) { 1204 // configured project is just created and not yet loaded 1205 checkProjectActualFiles(configuredProject, emptyArray); 1206 projectService.ensureInferredProjectsUpToDate_TestOnly(); 1207 } 1208 checkProjectActualFiles(configuredProject, [file1.path, tsconfig.path]); 1209 1210 // Allow allowNonTsExtensions will be set to true for deferred extensions. 1211 assert.isTrue(configuredProject.getCompilerOptions().allowNonTsExtensions); 1212 } 1213 1214 it("when lazyConfiguredProjectsFromExternalProject not set", () => { 1215 verifyDeferredContext(/*lazyConfiguredProjectsFromExternalProject*/ false); 1216 }); 1217 it("when lazyConfiguredProjectsFromExternalProject is set", () => { 1218 verifyDeferredContext(/*lazyConfiguredProjectsFromExternalProject*/ true); 1219 }); 1220 }); 1221 1222 it("Orphan source files are handled correctly on watch trigger", () => { 1223 const file1: File = { 1224 path: `${tscWatch.projectRoot}/src/file1.ts`, 1225 content: `export let x = 10;` 1226 }; 1227 const file2: File = { 1228 path: `${tscWatch.projectRoot}/src/file2.ts`, 1229 content: "export let y = 10;" 1230 }; 1231 const configContent1 = JSON.stringify({ 1232 files: ["src/file1.ts", "src/file2.ts"] 1233 }); 1234 const config: File = { 1235 path: `${tscWatch.projectRoot}/tsconfig.json`, 1236 content: configContent1 1237 }; 1238 const files = [file1, file2, libFile, config]; 1239 const host = createServerHost(files); 1240 const service = createProjectService(host); 1241 service.openClientFile(file1.path); 1242 checkProjectActualFiles(service.configuredProjects.get(config.path)!, [file1.path, file2.path, libFile.path, config.path]); 1243 1244 const configContent2 = JSON.stringify({ 1245 files: ["src/file1.ts"] 1246 }); 1247 config.content = configContent2; 1248 host.writeFile(config.path, config.content); 1249 host.runQueuedTimeoutCallbacks(); 1250 1251 checkProjectActualFiles(service.configuredProjects.get(config.path)!, [file1.path, libFile.path, config.path]); 1252 verifyFile2InfoIsOrphan(); 1253 1254 file2.content += "export let z = 10;"; 1255 host.writeFile(file2.path, file2.content); 1256 host.runQueuedTimeoutCallbacks(); 1257 1258 checkProjectActualFiles(service.configuredProjects.get(config.path)!, [file1.path, libFile.path, config.path]); 1259 verifyFile2InfoIsOrphan(); 1260 1261 function verifyFile2InfoIsOrphan() { 1262 const info = Debug.checkDefined(service.getScriptInfoForPath(file2.path as Path)); 1263 assert.equal(info.containingProjects.length, 0); 1264 } 1265 }); 1266 1267 it("no project structure update on directory watch invoke on open file save", () => { 1268 const projectRootPath = "/users/username/projects/project"; 1269 const file1: File = { 1270 path: `${projectRootPath}/a.ts`, 1271 content: "export const a = 10;" 1272 }; 1273 const config: File = { 1274 path: `${projectRootPath}/tsconfig.json`, 1275 content: "{}" 1276 }; 1277 const files = [file1, config]; 1278 const host = createServerHost(files); 1279 const service = createProjectService(host); 1280 service.openClientFile(file1.path); 1281 checkNumberOfProjects(service, { configuredProjects: 1 }); 1282 1283 host.modifyFile(file1.path, file1.content, { invokeFileDeleteCreateAsPartInsteadOfChange: true }); 1284 host.checkTimeoutQueueLength(0); 1285 }); 1286 1287 it("synchronizeProjectList provides redirect info when requested", () => { 1288 const projectRootPath = "/users/username/projects/project"; 1289 const fileA: File = { 1290 path: `${projectRootPath}/A/a.ts`, 1291 content: "export const foo: string = 5;" 1292 }; 1293 const configA: File = { 1294 path: `${projectRootPath}/A/tsconfig.json`, 1295 content: `{ 1296 "compilerOptions": { 1297 "composite": true, 1298 "declaration": true 1299 } 1300}` 1301 }; 1302 const fileB: File = { 1303 path: `${projectRootPath}/B/b.ts`, 1304 content: "import { foo } from \"../A/a\"; console.log(foo);" 1305 }; 1306 const configB: File = { 1307 path: `${projectRootPath}/B/tsconfig.json`, 1308 content: `{ 1309 "compilerOptions": { 1310 "composite": true, 1311 "declaration": true 1312 }, 1313 "references": [ 1314 { "path": "../A" } 1315 ] 1316}` 1317 }; 1318 const files = [fileA, fileB, configA, configB, libFile]; 1319 const host = createServerHost(files); 1320 const projectService = createProjectService(host); 1321 projectService.openClientFile(fileA.path); 1322 projectService.openClientFile(fileB.path); 1323 const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ true); 1324 assert.deepEqual(knownProjects[0].files, [ 1325 { 1326 fileName: libFile.path, 1327 isSourceOfProjectReferenceRedirect: false 1328 }, 1329 { 1330 fileName: fileA.path, 1331 isSourceOfProjectReferenceRedirect: false 1332 }, 1333 { 1334 fileName: configA.path, 1335 isSourceOfProjectReferenceRedirect: false 1336 } 1337 ]); 1338 assert.deepEqual(knownProjects[1].files, [ 1339 { 1340 fileName: libFile.path, 1341 isSourceOfProjectReferenceRedirect: false 1342 }, 1343 { 1344 fileName: fileA.path, 1345 isSourceOfProjectReferenceRedirect: true, 1346 }, 1347 { 1348 fileName: fileB.path, 1349 isSourceOfProjectReferenceRedirect: false, 1350 }, 1351 { 1352 fileName: configB.path, 1353 isSourceOfProjectReferenceRedirect: false 1354 } 1355 ]); 1356 }); 1357 1358 it("synchronizeProjectList provides updates to redirect info when requested", () => { 1359 const projectRootPath = "/users/username/projects/project"; 1360 const fileA: File = { 1361 path: `${projectRootPath}/A/a.ts`, 1362 content: "export const foo: string = 5;" 1363 }; 1364 const configA: File = { 1365 path: `${projectRootPath}/A/tsconfig.json`, 1366 content: `{ 1367 "compilerOptions": { 1368 "composite": true, 1369 "declaration": true 1370 } 1371}` 1372 }; 1373 const fileB: File = { 1374 path: `${projectRootPath}/B/b.ts`, 1375 content: "import { foo } from \"../B/b2\"; console.log(foo);" 1376 }; 1377 const fileB2: File = { 1378 path: `${projectRootPath}/B/b2.ts`, 1379 content: "export const foo: string = 5;" 1380 }; 1381 const configB: File = { 1382 path: `${projectRootPath}/B/tsconfig.json`, 1383 content: `{ 1384 "compilerOptions": { 1385 "composite": true, 1386 "declaration": true 1387 }, 1388 "references": [ 1389 { "path": "../A" } 1390 ] 1391}` 1392 }; 1393 const files = [fileA, fileB, fileB2, configA, configB, libFile]; 1394 const host = createServerHost(files); 1395 const projectService = createProjectService(host); 1396 projectService.openClientFile(fileA.path); 1397 projectService.openClientFile(fileB.path); 1398 const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ true); 1399 assert.deepEqual(knownProjects[0].files, [ 1400 { 1401 fileName: libFile.path, 1402 isSourceOfProjectReferenceRedirect: false 1403 }, 1404 { 1405 fileName: fileA.path, 1406 isSourceOfProjectReferenceRedirect: false 1407 }, 1408 { 1409 fileName: configA.path, 1410 isSourceOfProjectReferenceRedirect: false 1411 } 1412 ]); 1413 assert.deepEqual(knownProjects[1].files, [ 1414 { 1415 fileName: libFile.path, 1416 isSourceOfProjectReferenceRedirect: false 1417 }, 1418 { 1419 fileName: fileB2.path, 1420 isSourceOfProjectReferenceRedirect: false, 1421 }, 1422 { 1423 fileName: fileB.path, 1424 isSourceOfProjectReferenceRedirect: false, 1425 }, 1426 { 1427 fileName: configB.path, 1428 isSourceOfProjectReferenceRedirect: false 1429 } 1430 ]); 1431 1432 host.modifyFile(configA.path, `{ 1433 "compilerOptions": { 1434 "composite": true, 1435 "declaration": true 1436 }, 1437 "include": [ 1438 "**/*", 1439 "../B/b2.ts" 1440 ] 1441}`); 1442 const newKnownProjects = projectService.synchronizeProjectList(knownProjects.map(proj => proj.info!), /*includeProjectReferenceRedirectInfo*/ true); 1443 assert.deepEqual(newKnownProjects[0].changes?.added, [ 1444 { 1445 fileName: fileB2.path, 1446 isSourceOfProjectReferenceRedirect: false 1447 } 1448 ]); 1449 assert.deepEqual(newKnownProjects[1].changes?.updatedRedirects, [ 1450 { 1451 fileName: fileB2.path, 1452 isSourceOfProjectReferenceRedirect: true 1453 } 1454 ]); 1455 }); 1456 1457 it("synchronizeProjectList returns correct information when base configuration file cannot be resolved", () => { 1458 const file: File = { 1459 path: `${tscWatch.projectRoot}/index.ts`, 1460 content: "export const foo = 5;" 1461 }; 1462 const config: File = { 1463 path: `${tscWatch.projectRoot}/tsconfig.json`, 1464 content: JSON.stringify({ extends: "./tsconfig_base.json" }) 1465 }; 1466 const host = createServerHost([file, config, libFile]); 1467 const projectService = createProjectService(host); 1468 projectService.openClientFile(file.path); 1469 const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ false); 1470 assert.deepEqual(knownProjects[0].files, [ 1471 libFile.path, 1472 file.path, 1473 config.path, 1474 `${tscWatch.projectRoot}/tsconfig_base.json`, 1475 ]); 1476 }); 1477 1478 it("synchronizeProjectList returns correct information when base configuration file cannot be resolved and redirect info is requested", () => { 1479 const file: File = { 1480 path: `${tscWatch.projectRoot}/index.ts`, 1481 content: "export const foo = 5;" 1482 }; 1483 const config: File = { 1484 path: `${tscWatch.projectRoot}/tsconfig.json`, 1485 content: JSON.stringify({ extends: "./tsconfig_base.json" }) 1486 }; 1487 const host = createServerHost([file, config, libFile]); 1488 const projectService = createProjectService(host); 1489 projectService.openClientFile(file.path); 1490 const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ true); 1491 assert.deepEqual(knownProjects[0].files, [ 1492 { fileName: libFile.path, isSourceOfProjectReferenceRedirect: false }, 1493 { fileName: file.path, isSourceOfProjectReferenceRedirect: false }, 1494 { fileName: config.path, isSourceOfProjectReferenceRedirect: false }, 1495 { fileName: `${tscWatch.projectRoot}/tsconfig_base.json`, isSourceOfProjectReferenceRedirect: false }, 1496 ]); 1497 }); 1498 1499 it("handles delayed directory watch invoke on file creation", () => { 1500 const projectRootPath = "/users/username/projects/project"; 1501 const fileB: File = { 1502 path: `${projectRootPath}/b.ts`, 1503 content: "export const b = 10;" 1504 }; 1505 const fileA: File = { 1506 path: `${projectRootPath}/a.ts`, 1507 content: "export const a = 10;" 1508 }; 1509 const fileSubA: File = { 1510 path: `${projectRootPath}/sub/a.ts`, 1511 content: fileA.content 1512 }; 1513 const config: File = { 1514 path: `${projectRootPath}/tsconfig.json`, 1515 content: "{}" 1516 }; 1517 const files = [fileSubA, fileB, config, libFile]; 1518 const host = createServerHost(files); 1519 const session = createSession(host, { canUseEvents: true, noGetErrOnBackgroundUpdate: true, logger: createLoggerWithInMemoryLogs(host) }); 1520 openFile(fileB); 1521 openFile(fileSubA); 1522 1523 host.checkTimeoutQueueLengthAndRun(0); 1524 1525 // This should schedule 2 timeouts for ensuring project structure and ensuring projects for open file 1526 host.deleteFile(fileSubA.path); 1527 host.deleteFolder(getDirectoryPath(fileSubA.path)); 1528 host.writeFile(fileA.path, fileA.content); 1529 host.checkTimeoutQueueLength(2); 1530 1531 closeFilesForSession([fileSubA], session); 1532 // This should cancel existing updates and schedule new ones 1533 host.checkTimeoutQueueLength(2); 1534 1535 // Open the fileA (as if rename) 1536 // config project is updated to check if fileA is present in it 1537 openFile(fileA); 1538 1539 // Run the timeout for updating configured project and ensuring projects for open file 1540 host.checkTimeoutQueueLengthAndRun(2); 1541 1542 // file is deleted but watches are not yet invoked 1543 const originalFileExists = host.fileExists; 1544 host.fileExists = s => s === fileA.path ? false : originalFileExists.call(host, s); 1545 closeFilesForSession([fileA], session); 1546 host.checkTimeoutQueueLength(2); // Update configured project and projects for open file 1547 1548 // This should create inferred project since fileSubA not on the disk 1549 openFile(fileSubA); 1550 1551 host.checkTimeoutQueueLengthAndRun(2); // Update configured project and projects for open file 1552 host.fileExists = originalFileExists; 1553 1554 // Actually trigger the file move 1555 host.deleteFile(fileA.path); 1556 host.ensureFileOrFolder(fileSubA); 1557 host.checkTimeoutQueueLength(2); 1558 1559 verifyGetErrRequest({ session, host, files: [fileB, fileSubA], existingTimeouts: 2 }); 1560 baselineTsserverLogs("projects", "handles delayed directory watch invoke on file creation", session); 1561 1562 function openFile(file: File) { 1563 openFilesForSession([{ file, projectRootPath }], session); 1564 } 1565 }); 1566 1567 it("assert when removing project", () => { 1568 const host = createServerHost([commonFile1, commonFile2, libFile]); 1569 const service = createProjectService(host); 1570 service.openClientFile(commonFile1.path); 1571 const project = service.inferredProjects[0]; 1572 checkProjectActualFiles(project, [commonFile1.path, libFile.path]); 1573 // Intentionally create scriptinfo and attach it to project 1574 const info = service.getOrCreateScriptInfoForNormalizedPath(commonFile2.path as server.NormalizedPath, /*openedByClient*/ false)!; 1575 info.attachToProject(project); 1576 try { 1577 service.applyChangesInOpenFiles(/*openFiles*/ undefined, /*changedFiles*/ undefined, [commonFile1.path]); 1578 } 1579 catch (e) { 1580 assert.isTrue(e.message.indexOf("Debug Failure. False expression: Found script Info still attached to project") === 0); 1581 } 1582 }); 1583 it("does not look beyond node_modules folders for default configured projects", () => { 1584 const rootFilePath = server.asNormalizedPath("/project/index.ts"); 1585 const rootProjectPath = server.asNormalizedPath("/project/tsconfig.json"); 1586 const nodeModulesFilePath1 = server.asNormalizedPath("/project/node_modules/@types/a/index.d.ts"); 1587 const nodeModulesProjectPath1 = server.asNormalizedPath("/project/node_modules/@types/a/tsconfig.json"); 1588 const nodeModulesFilePath2 = server.asNormalizedPath("/project/node_modules/@types/b/index.d.ts"); 1589 const serverHost = createServerHost([ 1590 { path: rootFilePath, content: "import 'a'; import 'b';" }, 1591 { path: rootProjectPath, content: "{}" }, 1592 { path: nodeModulesFilePath1, content: "{}" }, 1593 { path: nodeModulesProjectPath1, content: "{}" }, 1594 { path: nodeModulesFilePath2, content: "{}" }, 1595 ]); 1596 const projectService = createProjectService(serverHost, { useSingleInferredProject: true }); 1597 1598 const openRootFileResult = projectService.openClientFile(rootFilePath); 1599 assert.strictEqual(openRootFileResult.configFileName?.toString(), rootProjectPath); 1600 1601 const openNodeModulesFileResult1 = projectService.openClientFile(nodeModulesFilePath1); 1602 assert.strictEqual(openNodeModulesFileResult1.configFileName?.toString(), nodeModulesProjectPath1); 1603 1604 const openNodeModulesFileResult2 = projectService.openClientFile(nodeModulesFilePath2); 1605 assert.isUndefined(openNodeModulesFileResult2.configFileName); 1606 1607 const rootProject = projectService.findProject(rootProjectPath)!; 1608 checkProjectActualFiles(rootProject, [rootProjectPath, rootFilePath, nodeModulesFilePath1, nodeModulesFilePath2]); 1609 1610 checkNumberOfInferredProjects(projectService, 0); 1611 }); 1612 1613 describe("file opened is in configured project that will be removed", () => { 1614 function runOnTs<T extends server.protocol.Request>(scenario: string, getRequest: (innerFile: File) => Partial<T>) { 1615 it(scenario, () => { 1616 const testsConfig: File = { 1617 path: `${tscWatch.projectRoot}/playground/tsconfig.json`, 1618 content: "{}" 1619 }; 1620 const testsFile: File = { 1621 path: `${tscWatch.projectRoot}/playground/tests.ts`, 1622 content: `export function foo() {}` 1623 }; 1624 const innerFile: File = { 1625 path: `${tscWatch.projectRoot}/playground/tsconfig-json/tests/spec.ts`, 1626 content: `export function bar() { }` 1627 }; 1628 const innerConfig: File = { 1629 path: `${tscWatch.projectRoot}/playground/tsconfig-json/tsconfig.json`, 1630 content: JSON.stringify({ 1631 include: ["./src"] 1632 }) 1633 }; 1634 const innerSrcFile: File = { 1635 path: `${tscWatch.projectRoot}/playground/tsconfig-json/src/src.ts`, 1636 content: `export function foobar() { }` 1637 }; 1638 const host = createServerHost([testsConfig, testsFile, innerFile, innerConfig, innerSrcFile, libFile]); 1639 const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); 1640 openFilesForSession([testsFile], session); 1641 closeFilesForSession([testsFile], session); 1642 openFilesForSession([innerFile], session); 1643 session.executeCommandSeq(getRequest(innerFile)); 1644 baselineTsserverLogs("projects", scenario, session); 1645 }); 1646 } 1647 runOnTs<protocol.OutliningSpansRequest>( 1648 "file opened is in configured project that will be removed", 1649 innerFile => ({ 1650 command: protocol.CommandTypes.GetOutliningSpans, 1651 arguments: { file: innerFile.path } 1652 }) 1653 ); 1654 1655 runOnTs<protocol.ReferencesRequest>( 1656 "references on file opened is in configured project that will be removed", 1657 innerFile => ({ 1658 command: protocol.CommandTypes.References, 1659 arguments: protocolFileLocationFromSubstring(innerFile, "bar") 1660 }) 1661 ); 1662 1663 it("js file opened is in configured project that will be removed", () => { 1664 const rootConfig: File = { 1665 path: `${tscWatch.projectRoot}/tsconfig.json`, 1666 content: JSON.stringify({ compilerOptions: { allowJs: true } }) 1667 }; 1668 const mocksFile: File = { 1669 path: `${tscWatch.projectRoot}/mocks/cssMock.js`, 1670 content: `function foo() { }` 1671 }; 1672 const innerFile: File = { 1673 path: `${tscWatch.projectRoot}/apps/editor/scripts/createConfigVariable.js`, 1674 content: `function bar() { }` 1675 }; 1676 const innerConfig: File = { 1677 path: `${tscWatch.projectRoot}/apps/editor/tsconfig.json`, 1678 content: JSON.stringify({ 1679 extends: "../../tsconfig.json", 1680 include: ["./src"], 1681 }) 1682 }; 1683 const innerSrcFile: File = { 1684 path: `${tscWatch.projectRoot}/apps/editor/src/src.js`, 1685 content: `function fooBar() { }` 1686 }; 1687 const host = createServerHost([rootConfig, mocksFile, innerFile, innerConfig, innerSrcFile, libFile]); 1688 const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }); 1689 openFilesForSession([mocksFile], session); 1690 closeFilesForSession([mocksFile], session); 1691 openFilesForSession([innerFile], session); 1692 baselineTsserverLogs("projects", "js file opened is in configured project that will be removed", session); 1693 }); 1694 }); 1695 }); 1696} 1697