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