1namespace ts.tscWatch { 2 import projectsLocation = TestFSWithWatch.tsbuildProjectsLocation; 3 import getFilePathInProject = TestFSWithWatch.getTsBuildProjectFilePath; 4 import getFileFromProject = TestFSWithWatch.getTsBuildProjectFile; 5 type TsBuildWatchSystem = TestFSWithWatch.TestServerHostTrackingWrittenFiles; 6 7 function createTsBuildWatchSystem(fileOrFolderList: readonly TestFSWithWatch.FileOrFolderOrSymLink[], params?: TestFSWithWatch.TestServerHostCreationParameters) { 8 return TestFSWithWatch.changeToHostTrackingWrittenFiles( 9 createWatchedSystem(fileOrFolderList, params) 10 ); 11 } 12 13 export function createSolutionBuilder(system: WatchedSystem, rootNames: readonly string[], defaultOptions?: BuildOptions) { 14 const host = createSolutionBuilderHost(system); 15 return ts.createSolutionBuilder(host, rootNames, defaultOptions || {}); 16 } 17 18 export function ensureErrorFreeBuild(host: WatchedSystem, rootNames: readonly string[]) { 19 // ts build should succeed 20 const solutionBuilder = createSolutionBuilder(host, rootNames, {}); 21 solutionBuilder.build(); 22 assert.equal(host.getOutput().length, 0, JSON.stringify(host.getOutput(), /*replacer*/ undefined, " ")); 23 } 24 25 type OutputFileStamp = [string, Date | undefined, boolean]; 26 function transformOutputToOutputFileStamp(f: string, host: TsBuildWatchSystem): OutputFileStamp { 27 return [f, host.getModifiedTime(f), host.writtenFiles.has(host.toFullPath(f))] as OutputFileStamp; 28 } 29 30 describe("unittests:: tsbuild:: watchMode:: program updates", () => { 31 const scenario = "programUpdates"; 32 const project = "sample1"; 33 const enum SubProject { 34 core = "core", 35 logic = "logic", 36 tests = "tests", 37 ui = "ui" 38 } 39 type ReadonlyFile = Readonly<File>; 40 /** [tsconfig, index] | [tsconfig, index, anotherModule, someDecl] */ 41 type SubProjectFiles = [ReadonlyFile, ReadonlyFile] | [ReadonlyFile, ReadonlyFile, ReadonlyFile, ReadonlyFile]; 42 function getProjectPath(project: string) { 43 return `${projectsLocation}/${project}`; 44 } 45 46 function projectPath(subProject: SubProject) { 47 return getFilePathInProject(project, subProject); 48 } 49 50 function projectFilePath(subProject: SubProject, baseFileName: string) { 51 return `${projectPath(subProject)}/${baseFileName.toLowerCase()}`; 52 } 53 54 function projectFileName(subProject: SubProject, baseFileName: string) { 55 return `${projectPath(subProject)}/${baseFileName}`; 56 } 57 58 function projectFile(subProject: SubProject, baseFileName: string): File { 59 return getFileFromProject(project, `${subProject}/${baseFileName}`); 60 } 61 62 function subProjectFiles(subProject: SubProject, anotherModuleAndSomeDecl?: true): SubProjectFiles { 63 const tsconfig = projectFile(subProject, "tsconfig.json"); 64 const index = projectFile(subProject, "index.ts"); 65 if (!anotherModuleAndSomeDecl) { 66 return [tsconfig, index]; 67 } 68 const anotherModule = projectFile(SubProject.core, "anotherModule.ts"); 69 const someDecl = projectFile(SubProject.core, "some_decl.ts"); 70 return [tsconfig, index, anotherModule, someDecl]; 71 } 72 73 function getOutputFileNames(subProject: SubProject, baseFileNameWithoutExtension: string) { 74 const file = projectFilePath(subProject, baseFileNameWithoutExtension); 75 return [`${file}.js`, `${file}.d.ts`]; 76 } 77 78 function getOutputStamps(host: TsBuildWatchSystem, subProject: SubProject, baseFileNameWithoutExtension: string): OutputFileStamp[] { 79 return getOutputFileNames(subProject, baseFileNameWithoutExtension).map(f => transformOutputToOutputFileStamp(f, host)); 80 } 81 82 function getOutputFileStamps(host: TsBuildWatchSystem, additionalFiles?: readonly [SubProject, string][]): OutputFileStamp[] { 83 const result = [ 84 ...getOutputStamps(host, SubProject.core, "anotherModule"), 85 ...getOutputStamps(host, SubProject.core, "index"), 86 ...getOutputStamps(host, SubProject.logic, "index"), 87 ...getOutputStamps(host, SubProject.tests, "index"), 88 ]; 89 if (additionalFiles) { 90 additionalFiles.forEach(([subProject, baseFileNameWithoutExtension]) => result.push(...getOutputStamps(host, subProject, baseFileNameWithoutExtension))); 91 } 92 host.writtenFiles.clear(); 93 return result; 94 } 95 96 function changeFile(fileName: string | (() => string), content: string | (() => string), caption: string): TscWatchCompileChange { 97 return { 98 caption, 99 change: sys => sys.writeFile(isString(fileName) ? fileName : fileName(), isString(content) ? content : content()), 100 timeouts: checkSingleTimeoutQueueLengthAndRun, // Builds core 101 }; 102 } 103 104 function changeCore(content: () => string, caption: string) { 105 return changeFile(() => core[1].path, content, caption); 106 } 107 108 let core: SubProjectFiles; 109 let logic: SubProjectFiles; 110 let tests: SubProjectFiles; 111 let ui: SubProjectFiles; 112 let allFiles: readonly File[]; 113 let testProjectExpectedWatchedFiles: string[]; 114 let testProjectExpectedWatchedDirectoriesRecursive: string[]; 115 116 before(() => { 117 core = subProjectFiles(SubProject.core, /*anotherModuleAndSomeDecl*/ true); 118 logic = subProjectFiles(SubProject.logic); 119 tests = subProjectFiles(SubProject.tests); 120 ui = subProjectFiles(SubProject.ui); 121 allFiles = [libFile, ...core, ...logic, ...tests, ...ui]; 122 testProjectExpectedWatchedFiles = [core[0], core[1], core[2]!, ...logic, ...tests].map(f => f.path.toLowerCase()); 123 testProjectExpectedWatchedDirectoriesRecursive = [projectPath(SubProject.core), projectPath(SubProject.logic)]; 124 }); 125 126 after(() => { 127 core = undefined!; 128 logic = undefined!; 129 tests = undefined!; 130 ui = undefined!; 131 allFiles = undefined!; 132 testProjectExpectedWatchedFiles = undefined!; 133 testProjectExpectedWatchedDirectoriesRecursive = undefined!; 134 }); 135 136 verifyTscWatch({ 137 scenario, 138 subScenario: "creates solution in watch mode", 139 commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], 140 sys: () => createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), 141 changes: emptyArray 142 }); 143 144 it("verify building references watches only those projects", () => { 145 const system = createTsBuildWatchSystem(allFiles, { currentDirectory: projectsLocation }); 146 const host = createSolutionBuilderWithWatchHost(system); 147 const solutionBuilder = createSolutionBuilderWithWatch(host, [`${project}/${SubProject.tests}`], { watch: true }); 148 solutionBuilder.buildReferences(`${project}/${SubProject.tests}`); 149 150 checkWatchedFiles(system, testProjectExpectedWatchedFiles.slice(0, testProjectExpectedWatchedFiles.length - tests.length)); 151 checkWatchedDirectories(system, emptyArray, /*recursive*/ false); 152 checkWatchedDirectories(system, testProjectExpectedWatchedDirectoriesRecursive, /*recursive*/ true); 153 154 checkOutputErrorsInitial(system, emptyArray); 155 const testOutput = getOutputStamps(system, SubProject.tests, "index"); 156 const outputFileStamps = getOutputFileStamps(system); 157 for (const stamp of outputFileStamps.slice(0, outputFileStamps.length - testOutput.length)) { 158 assert.isDefined(stamp[1], `${stamp[0]} expected to be present`); 159 } 160 for (const stamp of testOutput) { 161 assert.isUndefined(stamp[1], `${stamp[0]} expected to be missing`); 162 } 163 return system; 164 }); 165 166 const buildTests: TscWatchCompileChange = { 167 caption: "Build Tests", 168 change: noop, 169 // Build tests 170 timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, 171 }; 172 173 describe("validates the changes and watched files", () => { 174 const newFileWithoutExtension = "newFile"; 175 const newFile: File = { 176 path: projectFilePath(SubProject.core, `${newFileWithoutExtension}.ts`), 177 content: `export const newFileConst = 30;` 178 }; 179 180 function verifyProjectChanges(subScenario: string, allFilesGetter: () => readonly File[]) { 181 const buildLogicOrUpdateTimeStamps: TscWatchCompileChange = { 182 caption: "Build logic or update time stamps", 183 change: noop, 184 timeouts: checkSingleTimeoutQueueLengthAndRun, // Builds logic or updates timestamps 185 }; 186 187 verifyTscWatch({ 188 scenario, 189 subScenario: `${subScenario}/change builds changes and reports found errors message`, 190 commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], 191 sys: () => createWatchedSystem( 192 allFilesGetter(), 193 { currentDirectory: projectsLocation } 194 ), 195 changes: [ 196 changeCore(() => `${core[1].content} 197export class someClass { }`, "Make change to core"), 198 buildLogicOrUpdateTimeStamps, 199 buildTests, 200 // Another change requeues and builds it 201 changeCore(() => core[1].content, "Revert core file"), 202 buildLogicOrUpdateTimeStamps, 203 buildTests, 204 { 205 caption: "Make two changes", 206 change: sys => { 207 const change1 = `${core[1].content} 208export class someClass { }`; 209 sys.writeFile(core[1].path, change1); 210 assert.equal(sys.writtenFiles.size, 1); 211 sys.writtenFiles.clear(); 212 sys.writeFile(core[1].path, `${change1} 213export class someClass2 { }`); 214 }, 215 timeouts: checkSingleTimeoutQueueLengthAndRun, // Builds core 216 }, 217 buildLogicOrUpdateTimeStamps, 218 buildTests, 219 ] 220 }); 221 222 verifyTscWatch({ 223 scenario, 224 subScenario: `${subScenario}/non local change does not start build of referencing projects`, 225 commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], 226 sys: () => createWatchedSystem( 227 allFilesGetter(), 228 { currentDirectory: projectsLocation } 229 ), 230 changes: [ 231 changeCore(() => `${core[1].content} 232function foo() { }`, "Make local change to core"), 233 buildLogicOrUpdateTimeStamps, 234 buildTests 235 ] 236 }); 237 238 function changeNewFile(newFileContent: string) { 239 return changeFile(newFile.path, newFileContent, "Change to new File and build core"); 240 } 241 verifyTscWatch({ 242 scenario, 243 subScenario: `${subScenario}/builds when new file is added, and its subsequent updates`, 244 commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], 245 sys: () => createWatchedSystem( 246 allFilesGetter(), 247 { currentDirectory: projectsLocation } 248 ), 249 changes: [ 250 changeNewFile(newFile.content), 251 buildLogicOrUpdateTimeStamps, 252 buildTests, 253 changeNewFile(`${newFile.content} 254export class someClass2 { }`), 255 buildLogicOrUpdateTimeStamps, 256 buildTests 257 ] 258 }); 259 } 260 261 describe("with simple project reference graph", () => { 262 verifyProjectChanges( 263 "with simple project reference graph", 264 () => allFiles 265 ); 266 }); 267 268 describe("with circular project reference", () => { 269 verifyProjectChanges( 270 "with circular project reference", 271 () => { 272 const [coreTsconfig, ...otherCoreFiles] = core; 273 const circularCoreConfig: File = { 274 path: coreTsconfig.path, 275 content: JSON.stringify({ 276 compilerOptions: { composite: true, declaration: true }, 277 references: [{ path: "../tests", circular: true }] 278 }) 279 }; 280 return [libFile, circularCoreConfig, ...otherCoreFiles, ...logic, ...tests]; 281 } 282 ); 283 }); 284 }); 285 286 verifyTscWatch({ 287 scenario, 288 subScenario: "watches config files that are not present", 289 commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], 290 sys: () => createWatchedSystem( 291 [libFile, ...core, logic[1], ...tests], 292 { currentDirectory: projectsLocation } 293 ), 294 changes: [ 295 { 296 caption: "Write logic tsconfig and build logic", 297 change: sys => sys.writeFile(logic[0].path, logic[0].content), 298 timeouts: checkSingleTimeoutQueueLengthAndRun, // Builds logic 299 }, 300 buildTests 301 ] 302 }); 303 304 describe("when referenced using prepend, builds referencing project even for non local change", () => { 305 let coreIndex: File; 306 before(() => { 307 coreIndex = { 308 path: core[1].path, 309 content: `function foo() { return 10; }` 310 }; 311 }); 312 after(() => { 313 coreIndex = undefined!; 314 }); 315 const buildLogic: TscWatchCompileChange = { 316 caption: "Build logic", 317 change: noop, 318 // Builds logic 319 timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, 320 }; 321 verifyTscWatch({ 322 scenario, 323 subScenario: "when referenced using prepend builds referencing project even for non local change", 324 commandLineArgs: ["-b", "-w", `${project}/${SubProject.logic}`], 325 sys: () => { 326 const coreTsConfig: File = { 327 path: core[0].path, 328 content: JSON.stringify({ 329 compilerOptions: { composite: true, declaration: true, outFile: "index.js" } 330 }) 331 }; 332 const logicTsConfig: File = { 333 path: logic[0].path, 334 content: JSON.stringify({ 335 compilerOptions: { composite: true, declaration: true, outFile: "index.js" }, 336 references: [{ path: "../core", prepend: true }] 337 }) 338 }; 339 const logicIndex: File = { 340 path: logic[1].path, 341 content: `function bar() { return foo() + 1 };` 342 }; 343 return createWatchedSystem([libFile, coreTsConfig, coreIndex, logicTsConfig, logicIndex], { currentDirectory: projectsLocation }); 344 }, 345 changes: [ 346 changeCore(() => `${coreIndex.content} 347function myFunc() { return 10; }`, "Make non local change and build core"), 348 buildLogic, 349 changeCore(() => `${coreIndex.content} 350function myFunc() { return 100; }`, "Make local change and build core"), 351 buildLogic, 352 ] 353 }); 354 }); 355 356 describe("when referenced project change introduces error in the down stream project and then fixes it", () => { 357 const subProjectLibrary = `${projectsLocation}/${project}/Library`; 358 const libraryTs: File = { 359 path: `${subProjectLibrary}/library.ts`, 360 content: ` 361interface SomeObject 362{ 363 message: string; 364} 365 366export function createSomeObject(): SomeObject 367{ 368 return { 369 message: "new Object" 370 }; 371}` 372 }; 373 verifyTscWatch({ 374 scenario, 375 subScenario: "when referenced project change introduces error in the down stream project and then fixes it", 376 commandLineArgs: ["-b", "-w", "App"], 377 sys: () => { 378 const libraryTsconfig: File = { 379 path: `${subProjectLibrary}/tsconfig.json`, 380 content: JSON.stringify({ compilerOptions: { composite: true } }) 381 }; 382 const subProjectApp = `${projectsLocation}/${project}/App`; 383 const appTs: File = { 384 path: `${subProjectApp}/app.ts`, 385 content: `import { createSomeObject } from "../Library/library"; 386createSomeObject().message;` 387 }; 388 const appTsconfig: File = { 389 path: `${subProjectApp}/tsconfig.json`, 390 content: JSON.stringify({ references: [{ path: "../Library" }] }) 391 }; 392 393 const files = [libFile, libraryTs, libraryTsconfig, appTs, appTsconfig]; 394 return createWatchedSystem(files, { currentDirectory: `${projectsLocation}/${project}` }); 395 }, 396 changes: [ 397 { 398 caption: "Introduce error", 399 // Change message in library to message2 400 change: sys => sys.writeFile(libraryTs.path, libraryTs.content.replace(/message/g, "message2")), 401 timeouts: sys => { 402 sys.checkTimeoutQueueLengthAndRun(1); // Build library 403 sys.checkTimeoutQueueLengthAndRun(1); // Build App 404 }, 405 }, 406 { 407 caption: "Fix error", 408 // Revert library changes 409 change: sys => sys.writeFile(libraryTs.path, libraryTs.content), 410 timeouts: sys => { 411 sys.checkTimeoutQueueLengthAndRun(1); // Build library 412 sys.checkTimeoutQueueLengthAndRun(1); // Build App 413 }, 414 }, 415 ] 416 }); 417 418 }); 419 420 describe("reports errors in all projects on incremental compile", () => { 421 function verifyIncrementalErrors(subScenario: string, buildOptions: readonly string[]) { 422 verifyTscWatch({ 423 scenario, 424 subScenario: `reportErrors/${subScenario}`, 425 commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`, ...buildOptions], 426 sys: () => createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), 427 changes: [ 428 { 429 caption: "change logic", 430 change: sys => sys.writeFile(logic[1].path, `${logic[1].content} 431let y: string = 10;`), 432 // Builds logic 433 timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, 434 }, 435 { 436 caption: "change core", 437 change: sys => sys.writeFile(core[1].path, `${core[1].content} 438let x: string = 10;`), 439 // Builds core 440 timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, 441 } 442 ] 443 }); 444 } 445 verifyIncrementalErrors("when preserveWatchOutput is not used", emptyArray); 446 verifyIncrementalErrors("when preserveWatchOutput is passed on command line", ["--preserveWatchOutput"]); 447 448 describe("when declaration emit errors are present", () => { 449 const solution = "solution"; 450 const subProject = "app"; 451 const subProjectLocation = `${projectsLocation}/${solution}/${subProject}`; 452 const fileWithError: File = { 453 path: `${subProjectLocation}/fileWithError.ts`, 454 content: `export var myClassWithError = class { 455 tags() { } 456 private p = 12 457 };` 458 }; 459 const fileWithFixedError: File = { 460 path: fileWithError.path, 461 content: fileWithError.content.replace("private p = 12", "") 462 }; 463 const fileWithoutError: File = { 464 path: `${subProjectLocation}/fileWithoutError.ts`, 465 content: `export class myClass { }` 466 }; 467 const tsconfig: File = { 468 path: `${subProjectLocation}/tsconfig.json`, 469 content: JSON.stringify({ compilerOptions: { composite: true } }) 470 }; 471 472 function incrementalBuild(sys: WatchedSystem) { 473 sys.checkTimeoutQueueLengthAndRun(1); // Build the app 474 sys.checkTimeoutQueueLength(0); 475 } 476 477 const fixError: TscWatchCompileChange = { 478 caption: "Fix error in fileWithError", 479 // Fix error 480 change: sys => sys.writeFile(fileWithError.path, fileWithFixedError.content), 481 timeouts: incrementalBuild 482 }; 483 484 const changeFileWithoutError: TscWatchCompileChange = { 485 caption: "Change fileWithoutError", 486 change: sys => sys.writeFile(fileWithoutError.path, fileWithoutError.content.replace(/myClass/g, "myClass2")), 487 timeouts: incrementalBuild 488 }; 489 490 verifyTscWatch({ 491 scenario, 492 subScenario: "reportErrors/declarationEmitErrors/when fixing error files all files are emitted", 493 commandLineArgs: ["-b", "-w", subProject], 494 sys: () => createWatchedSystem( 495 [libFile, fileWithError, fileWithoutError, tsconfig], 496 { currentDirectory: `${projectsLocation}/${solution}` } 497 ), 498 changes: [ 499 fixError 500 ] 501 }); 502 503 verifyTscWatch({ 504 scenario, 505 subScenario: "reportErrors/declarationEmitErrors/when file with no error changes", 506 commandLineArgs: ["-b", "-w", subProject], 507 sys: () => createWatchedSystem( 508 [libFile, fileWithError, fileWithoutError, tsconfig], 509 { currentDirectory: `${projectsLocation}/${solution}` } 510 ), 511 changes: [ 512 changeFileWithoutError 513 ] 514 }); 515 516 describe("when reporting errors on introducing error", () => { 517 const introduceError: TscWatchCompileChange = { 518 caption: "Introduce error", 519 change: sys => sys.writeFile(fileWithError.path, fileWithError.content), 520 timeouts: incrementalBuild, 521 }; 522 523 verifyTscWatch({ 524 scenario, 525 subScenario: "reportErrors/declarationEmitErrors/introduceError/when fixing errors only changed file is emitted", 526 commandLineArgs: ["-b", "-w", subProject], 527 sys: () => createWatchedSystem( 528 [libFile, fileWithFixedError, fileWithoutError, tsconfig], 529 { currentDirectory: `${projectsLocation}/${solution}` } 530 ), 531 changes: [ 532 introduceError, 533 fixError 534 ] 535 }); 536 537 verifyTscWatch({ 538 scenario, 539 subScenario: "reportErrors/declarationEmitErrors/introduceError/when file with no error changes", 540 commandLineArgs: ["-b", "-w", subProject], 541 sys: () => createWatchedSystem( 542 [libFile, fileWithFixedError, fileWithoutError, tsconfig], 543 { currentDirectory: `${projectsLocation}/${solution}` } 544 ), 545 changes: [ 546 introduceError, 547 changeFileWithoutError 548 ] 549 }); 550 }); 551 }); 552 }); 553 554 describe("tsc-watch and tsserver works with project references", () => { 555 describe("invoking when references are already built", () => { 556 function verifyWatchesOfProject(host: TsBuildWatchSystem, expectedWatchedFiles: readonly string[], expectedWatchedDirectoriesRecursive: readonly string[], expectedWatchedDirectories?: readonly string[]) { 557 checkWatchedFilesDetailed(host, expectedWatchedFiles, 1); 558 checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories || emptyArray, 1, /*recursive*/ false); 559 checkWatchedDirectoriesDetailed(host, expectedWatchedDirectoriesRecursive, 1, /*recursive*/ true); 560 } 561 562 function createSolutionOfProject(allFiles: readonly File[], 563 currentDirectory: string, 564 solutionBuilderconfig: string, 565 getOutputFileStamps: (host: TsBuildWatchSystem) => readonly OutputFileStamp[]) { 566 // Build the composite project 567 const host = createTsBuildWatchSystem(allFiles, { currentDirectory }); 568 const solutionBuilder = createSolutionBuilder(host, [solutionBuilderconfig], {}); 569 solutionBuilder.build(); 570 const outputFileStamps = getOutputFileStamps(host); 571 for (const stamp of outputFileStamps) { 572 assert.isDefined(stamp[1], `${stamp[0]} expected to be present`); 573 } 574 return { host, solutionBuilder }; 575 } 576 577 function createSolutionAndWatchModeOfProject( 578 allFiles: readonly File[], 579 currentDirectory: string, 580 solutionBuilderconfig: string, 581 watchConfig: string, 582 getOutputFileStamps: (host: TsBuildWatchSystem) => readonly OutputFileStamp[]) { 583 // Build the composite project 584 const { host, solutionBuilder } = createSolutionOfProject(allFiles, currentDirectory, solutionBuilderconfig, getOutputFileStamps); 585 586 // Build in watch mode 587 const watch = createWatchOfConfigFile(watchConfig, host); 588 checkOutputErrorsInitial(host, emptyArray); 589 590 return { host, solutionBuilder, watch }; 591 } 592 593 function createSolutionAndServiceOfProject(allFiles: readonly File[], 594 currentDirectory: string, 595 solutionBuilderconfig: string, 596 openFileName: string, 597 getOutputFileStamps: (host: TsBuildWatchSystem) => readonly OutputFileStamp[]) { 598 // Build the composite project 599 const { host, solutionBuilder } = createSolutionOfProject(allFiles, currentDirectory, solutionBuilderconfig, getOutputFileStamps); 600 601 // service 602 const service = projectSystem.createProjectService(host); 603 service.openClientFile(openFileName); 604 605 return { host, solutionBuilder, service }; 606 } 607 608 function checkProjectActualFiles(service: projectSystem.TestProjectService, configFile: string, expectedFiles: readonly string[]) { 609 projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); 610 projectSystem.checkProjectActualFiles(service.configuredProjects.get(configFile.toLowerCase())!, expectedFiles); 611 } 612 613 function verifyDependencies(watch: Watch, filePath: string, expected: readonly string[]) { 614 checkArray(`${filePath} dependencies`, watch.getCurrentProgram().getAllDependencies(watch.getCurrentProgram().getSourceFile(filePath)!), expected); 615 } 616 617 describe("on sample project", () => { 618 const coreIndexDts = projectFileName(SubProject.core, "index.d.ts"); 619 const coreAnotherModuleDts = projectFileName(SubProject.core, "anotherModule.d.ts"); 620 const logicIndexDts = projectFileName(SubProject.logic, "index.d.ts"); 621 const expectedWatchedDirectoriesRecursive = projectSystem.getTypeRootsFromLocation(projectPath(SubProject.tests)); 622 const expectedProjectFiles = () => [libFile, ...tests, ...logic.slice(1), ...core.slice(1, core.length - 1)].map(f => f.path); 623 const expectedProgramFiles = () => [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, logicIndexDts]; 624 625 function createSolutionAndWatchMode() { 626 return createSolutionAndWatchModeOfProject(allFiles, projectsLocation, `${project}/${SubProject.tests}`, tests[0].path, getOutputFileStamps); 627 } 628 629 function createSolutionAndService() { 630 return createSolutionAndServiceOfProject(allFiles, projectsLocation, `${project}/${SubProject.tests}`, tests[1].path, getOutputFileStamps); 631 } 632 633 function verifyWatches(host: TsBuildWatchSystem, withTsserver?: boolean) { 634 verifyWatchesOfProject( 635 host, 636 withTsserver ? 637 [...core.slice(0, core.length - 1), ...logic, tests[0], libFile].map(f => f.path.toLowerCase()) : 638 [core[0], logic[0], ...tests, libFile].map(f => f.path).concat([coreIndexDts, coreAnotherModuleDts, logicIndexDts].map(f => f.toLowerCase())), 639 expectedWatchedDirectoriesRecursive 640 ); 641 } 642 643 function verifyScenario( 644 edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder<EmitAndSemanticDiagnosticsBuilderProgram>) => void, 645 expectedProgramFilesAfterEdit: () => readonly string[], 646 expectedProjectFilesAfterEdit: () => readonly string[] 647 ) { 648 it("with tsc-watch", () => { 649 const { host, solutionBuilder, watch } = createSolutionAndWatchMode(); 650 651 edit(host, solutionBuilder); 652 653 host.checkTimeoutQueueLengthAndRun(1); 654 checkOutputErrorsIncremental(host, emptyArray); 655 checkProgramActualFiles(watch.getCurrentProgram().getProgram(), expectedProgramFilesAfterEdit()); 656 657 }); 658 659 it("with tsserver", () => { 660 const { host, solutionBuilder, service } = createSolutionAndService(); 661 662 edit(host, solutionBuilder); 663 664 host.checkTimeoutQueueLengthAndRun(2); 665 checkProjectActualFiles(service, tests[0].path, expectedProjectFilesAfterEdit()); 666 }); 667 } 668 669 describe("verifies dependencies and watches", () => { 670 it("with tsc-watch", () => { 671 const { host, watch } = createSolutionAndWatchMode(); 672 verifyWatches(host); 673 verifyDependencies(watch, coreIndexDts, [coreIndexDts]); 674 verifyDependencies(watch, coreAnotherModuleDts, [coreAnotherModuleDts]); 675 verifyDependencies(watch, logicIndexDts, [logicIndexDts, coreAnotherModuleDts]); 676 verifyDependencies(watch, tests[1].path, expectedProgramFiles().filter(f => f !== libFile.path)); 677 }); 678 679 it("with tsserver", () => { 680 const { host } = createSolutionAndService(); 681 verifyWatches(host, /*withTsserver*/ true); 682 }); 683 }); 684 685 describe("local edit in ts file, result in watch compilation because logic.d.ts is written", () => { 686 verifyScenario((host, solutionBuilder) => { 687 host.writeFile(logic[1].path, `${logic[1].content} 688function foo() { 689}`); 690 solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath); 691 solutionBuilder.buildNextInvalidatedProject(); 692 693 // not ideal, but currently because of d.ts but no new file is written 694 // There will be timeout queued even though file contents are same 695 }, expectedProgramFiles, expectedProjectFiles); 696 }); 697 698 describe("non local edit in ts file, rebuilds in watch compilation", () => { 699 verifyScenario((host, solutionBuilder) => { 700 host.writeFile(logic[1].path, `${logic[1].content} 701export function gfoo() { 702}`); 703 solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath); 704 solutionBuilder.buildNextInvalidatedProject(); 705 }, expectedProgramFiles, expectedProjectFiles); 706 }); 707 708 describe("change in project reference config file builds correctly", () => { 709 verifyScenario((host, solutionBuilder) => { 710 host.writeFile(logic[0].path, JSON.stringify({ 711 compilerOptions: { composite: true, declaration: true, declarationDir: "decls" }, 712 references: [{ path: "../core" }] 713 })); 714 solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath, ConfigFileProgramReloadLevel.Full); 715 solutionBuilder.buildNextInvalidatedProject(); 716 }, () => [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, projectFilePath(SubProject.logic, "decls/index.d.ts")], expectedProjectFiles); 717 }); 718 }); 719 720 describe("on transitive references", () => { 721 const project = "transitiveReferences"; 722 const aTsFile = getFileFromProject(project, "a.ts"); 723 const bTsFile = getFileFromProject(project, "b.ts"); 724 const cTsFile = getFileFromProject(project, "c.ts"); 725 const aTsconfigFile = getFileFromProject(project, "tsconfig.a.json"); 726 const bTsconfigFile = getFileFromProject(project, "tsconfig.b.json"); 727 const cTsconfigFile = getFileFromProject(project, "tsconfig.c.json"); 728 const refs = getFileFromProject(project, "refs/a.d.ts"); 729 730 function getRootFile(multiFolder: boolean, fileFromDisk: File, multiFolderPath: string): File { 731 return multiFolder ? { 732 path: getFilePathInProject(project, multiFolderPath), 733 content: fileFromDisk.content 734 // Replace the relative imports 735 .replace("./", "../") 736 } : fileFromDisk; 737 } 738 739 function dtsFile(extensionLessFile: string) { 740 return getFilePathInProject(project, `${extensionLessFile}.d.ts`); 741 } 742 743 function jsFile(extensionLessFile: string) { 744 return getFilePathInProject(project, `${extensionLessFile}.js`); 745 } 746 747 function verifyWatchState( 748 host: TsBuildWatchSystem, 749 watch: Watch, 750 expectedProgramFiles: readonly string[], 751 expectedWatchedFiles: readonly string[], 752 expectedWatchedDirectoriesRecursive: readonly string[], 753 dependencies: readonly [string, readonly string[]][], 754 expectedWatchedDirectories?: readonly string[]) { 755 checkProgramActualFiles(watch.getCurrentProgram().getProgram(), expectedProgramFiles); 756 verifyWatchesOfProject(host, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, expectedWatchedDirectories); 757 for (const [file, deps] of dependencies) { 758 verifyDependencies(watch, file, deps); 759 } 760 } 761 762 function getTsConfigFile(multiFolder: boolean, fileFromDisk: File, folder: string): File { 763 if (!multiFolder) return fileFromDisk; 764 765 return { 766 path: getFilePathInProject(project, `${folder}/tsconfig.json`), 767 content: fileFromDisk.content 768 // Replace files array 769 .replace(`${folder}.ts`, "index.ts") 770 // Replace path mappings 771 .replace("./*", "../*") 772 .replace("./refs", "../refs") 773 // Replace references 774 .replace("tsconfig.a.json", "../a") 775 .replace("tsconfig.b.json", "../b") 776 }; 777 } 778 779 // function writeFile(file: File) { 780 // Harness.IO.writeFile(file.path.replace(projectsLocation, "c:/temp"), file.content); 781 // } 782 783 function verifyTransitiveReferences(multiFolder: boolean) { 784 const aTs = getRootFile(multiFolder, aTsFile, "a/index.ts"); 785 const bTs = getRootFile(multiFolder, bTsFile, "b/index.ts"); 786 const cTs = getRootFile(multiFolder, cTsFile, "c/index.ts"); 787 788 const configToBuild = multiFolder ? "c/tsconfig.json" : "tsconfig.c.json"; 789 const aTsconfig = getTsConfigFile(multiFolder, aTsconfigFile, "a"); 790 const bTsconfig = getTsConfigFile(multiFolder, bTsconfigFile, "b"); 791 const cTsconfig = getTsConfigFile(multiFolder, cTsconfigFile, "c"); 792 793 // if (multiFolder) { 794 // writeFile(aTs); 795 // writeFile(bTs); 796 // writeFile(cTs); 797 // writeFile(aTsconfig); 798 // writeFile(bTsconfig); 799 // writeFile(cTsconfig); 800 // } 801 802 const allFiles = [libFile, aTs, bTs, cTs, aTsconfig, bTsconfig, cTsconfig, refs]; 803 const aDts = dtsFile(multiFolder ? "a/index" : "a"), bDts = dtsFile(multiFolder ? "b/index" : "b"); 804 const expectedFiles = [jsFile(multiFolder ? "a/index" : "a"), aDts, jsFile(multiFolder ? "b/index" : "b"), bDts, jsFile(multiFolder ? "c/index" : "c")]; 805 const expectedProgramFiles = [cTs.path, libFile.path, aDts, refs.path, bDts]; 806 const expectedProjectFiles = [cTs.path, libFile.path, aTs.path, refs.path, bTs.path]; 807 const expectedWatchedFiles = expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()); 808 const expectedProjectWatchedFiles = expectedProjectFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()); 809 const expectedWatchedDirectories = multiFolder ? [ 810 getProjectPath(project).toLowerCase() // watches for directories created for resolution of b 811 ] : emptyArray; 812 const nrefsPath = multiFolder ? ["../nrefs/*"] : ["./nrefs/*"]; 813 const expectedWatchedDirectoriesRecursive = [ 814 ...(multiFolder ? [ 815 getFilePathInProject(project, "a"), // Failed to package json 816 getFilePathInProject(project, "b"), // Failed to package json 817 ] : []), 818 getFilePathInProject(project, "refs"), // Failed lookup since refs/a.ts does not exist 819 ...projectSystem.getTypeRootsFromLocation(multiFolder ? getFilePathInProject(project, "c") : getProjectPath(project)) 820 ].map(s => s.toLowerCase()); 821 822 const defaultDependencies: readonly [string, readonly string[]][] = [ 823 [aDts, [aDts]], 824 [bDts, [bDts, aDts]], 825 [refs.path, [refs.path]], 826 [cTs.path, [cTs.path, refs.path, bDts, aDts]] 827 ]; 828 829 function createSolutionAndWatchMode() { 830 return createSolutionAndWatchModeOfProject(allFiles, getProjectPath(project), configToBuild, configToBuild, getOutputFileStamps); 831 } 832 833 function createSolutionAndService() { 834 return createSolutionAndServiceOfProject(allFiles, getProjectPath(project), configToBuild, cTs.path, getOutputFileStamps); 835 } 836 837 function getOutputFileStamps(host: TsBuildWatchSystem) { 838 return expectedFiles.map(file => transformOutputToOutputFileStamp(file, host)); 839 } 840 841 function verifyProgram(host: TsBuildWatchSystem, watch: Watch) { 842 verifyWatchState(host, watch, expectedProgramFiles, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, defaultDependencies, expectedWatchedDirectories); 843 } 844 845 function verifyProject(host: TsBuildWatchSystem, service: projectSystem.TestProjectService, orphanInfos?: readonly string[]) { 846 verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos }); 847 } 848 849 interface VerifyServerState { 850 host: TsBuildWatchSystem; 851 service: projectSystem.TestProjectService; 852 expectedProjectFiles: readonly string[]; 853 expectedProjectWatchedFiles: readonly string[]; 854 expectedWatchedDirectoriesRecursive: readonly string[]; 855 orphanInfos?: readonly string[]; 856 } 857 function verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos }: VerifyServerState) { 858 checkProjectActualFiles(service, cTsconfig.path, expectedProjectFiles.concat(cTsconfig.path)); 859 const watchedFiles = expectedProjectWatchedFiles.filter(f => f !== cTs.path.toLowerCase()); 860 const actualOrphan = arrayFrom(mapDefinedIterator( 861 service.filenameToScriptInfo.values(), 862 v => v.containingProjects.length === 0 ? v.fileName : undefined 863 )); 864 assert.equal(actualOrphan.length, orphanInfos ? orphanInfos.length : 0, `Orphans found: ${JSON.stringify(actualOrphan, /*replacer*/ undefined, " ")}`); 865 if (orphanInfos && orphanInfos.length) { 866 for (const orphan of orphanInfos) { 867 const info = service.getScriptInfoForPath(orphan as Path); 868 assert.isDefined(info, `${orphan} expected to be present. Actual: ${JSON.stringify(actualOrphan, /*replacer*/ undefined, " ")}`); 869 assert.equal(info!.containingProjects.length, 0); 870 watchedFiles.push(orphan); 871 } 872 } 873 verifyWatchesOfProject(host, watchedFiles, expectedWatchedDirectoriesRecursive, expectedWatchedDirectories); 874 } 875 876 interface VerifyScenario { 877 edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder<EmitAndSemanticDiagnosticsBuilderProgram>) => void; 878 schedulesFailedWatchUpdate?: boolean; 879 expectedEditErrors: readonly string[]; 880 expectedProgramFiles: readonly string[]; 881 expectedProjectFiles: readonly string[]; 882 expectedWatchedFiles: readonly string[]; 883 expectedProjectWatchedFiles: readonly string[]; 884 expectedWatchedDirectoriesRecursive: readonly string[]; 885 dependencies: readonly [string, readonly string[]][]; 886 revert?: (host: TsBuildWatchSystem) => void; 887 orphanInfosAfterEdit?: readonly string[]; 888 orphanInfosAfterRevert?: readonly string[]; 889 } 890 function verifyScenario({ edit, schedulesFailedWatchUpdate, expectedEditErrors, expectedProgramFiles, expectedProjectFiles, expectedWatchedFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, dependencies, revert, orphanInfosAfterEdit, orphanInfosAfterRevert }: VerifyScenario) { 891 it("with tsc-watch", () => { 892 const { host, solutionBuilder, watch } = createSolutionAndWatchMode(); 893 894 edit(host, solutionBuilder); 895 896 host.checkTimeoutQueueLengthAndRun(schedulesFailedWatchUpdate ? 2 : 1); 897 checkOutputErrorsIncremental(host, expectedEditErrors); 898 verifyWatchState(host, watch, expectedProgramFiles, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, dependencies, expectedWatchedDirectories); 899 900 if (revert) { 901 revert(host); 902 903 host.checkTimeoutQueueLengthAndRun(schedulesFailedWatchUpdate ? 2 : 1); 904 checkOutputErrorsIncremental(host, emptyArray); 905 verifyProgram(host, watch); 906 } 907 }); 908 909 if (!multiFolder) return; // With side by side file open is in inferred project without any settings 910 911 it("with tsserver", () => { 912 const { host, solutionBuilder, service } = createSolutionAndService(); 913 914 edit(host, solutionBuilder); 915 916 host.checkTimeoutQueueLengthAndRun(schedulesFailedWatchUpdate ? 3 : 2); 917 verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos: orphanInfosAfterEdit }); 918 919 if (revert) { 920 revert(host); 921 922 host.checkTimeoutQueueLengthAndRun(schedulesFailedWatchUpdate ? 3 : 2); 923 verifyProject(host, service, orphanInfosAfterRevert); 924 } 925 }); 926 } 927 928 describe("verifies dependencies and watches", () => { 929 // Initial build 930 it("with tsc-watch", () => { 931 const { host, watch } = createSolutionAndWatchMode(); 932 verifyProgram(host, watch); 933 }); 934 if (!multiFolder) return; 935 it("with tsserver", () => { 936 const { host, service } = createSolutionAndService(); 937 verifyProject(host, service); 938 }); 939 }); 940 941 describe("non local edit updates the program and watch correctly", () => { 942 verifyScenario({ 943 edit: (host, solutionBuilder) => { 944 // edit 945 host.writeFile(bTs.path, `${bTs.content}\nexport function gfoo() {\n}`); 946 solutionBuilder.invalidateProject((bTsconfig.path.toLowerCase() as ResolvedConfigFilePath)); 947 solutionBuilder.buildNextInvalidatedProject(); 948 }, 949 expectedEditErrors: emptyArray, 950 expectedProgramFiles, 951 expectedProjectFiles, 952 expectedWatchedFiles, 953 expectedProjectWatchedFiles, 954 expectedWatchedDirectoriesRecursive, 955 dependencies: defaultDependencies 956 }); 957 }); 958 959 describe("edit on config file", () => { 960 const nrefReplacer = (f: string) => f.replace("refs", "nrefs"); 961 const nrefs: File = { 962 path: getFilePathInProject(project, "nrefs/a.d.ts"), 963 content: refs.content 964 }; 965 verifyScenario({ 966 edit: host => { 967 const cTsConfigJson = JSON.parse(cTsconfig.content); 968 host.ensureFileOrFolder(nrefs); 969 cTsConfigJson.compilerOptions.paths = { "@ref/*": nrefsPath }; 970 host.writeFile(cTsconfig.path, JSON.stringify(cTsConfigJson)); 971 }, 972 expectedEditErrors: emptyArray, 973 expectedProgramFiles: expectedProgramFiles.map(nrefReplacer), 974 expectedProjectFiles: expectedProjectFiles.map(nrefReplacer), 975 expectedWatchedFiles: expectedWatchedFiles.map(nrefReplacer), 976 expectedProjectWatchedFiles: expectedProjectWatchedFiles.map(nrefReplacer), 977 expectedWatchedDirectoriesRecursive: expectedWatchedDirectoriesRecursive.map(nrefReplacer), 978 dependencies: [ 979 [aDts, [aDts]], 980 [bDts, [bDts, aDts]], 981 [nrefs.path, [nrefs.path]], 982 [cTs.path, [cTs.path, nrefs.path, bDts, aDts]] 983 ], 984 // revert the update 985 revert: host => host.writeFile(cTsconfig.path, cTsconfig.content), 986 // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open 987 orphanInfosAfterEdit: [refs.path.toLowerCase()], 988 // AfterRevert:: Extra watched files on server since the script infos arent deleted till next file open 989 orphanInfosAfterRevert: [nrefs.path.toLowerCase()] 990 }); 991 }); 992 993 describe("edit in referenced config file", () => { 994 const nrefs: File = { 995 path: getFilePathInProject(project, "nrefs/a.d.ts"), 996 content: "export declare class A {}" 997 }; 998 const expectedProgramFiles = [cTs.path, bDts, nrefs.path, refs.path, libFile.path]; 999 const expectedProjectFiles = [cTs.path, bTs.path, nrefs.path, refs.path, libFile.path]; 1000 const [, ...expectedWatchedDirectoriesRecursiveWithoutA] = expectedWatchedDirectoriesRecursive; // Not looking in a folder for resolution in multi folder scenario 1001 verifyScenario({ 1002 edit: host => { 1003 const bTsConfigJson = JSON.parse(bTsconfig.content); 1004 host.ensureFileOrFolder(nrefs); 1005 bTsConfigJson.compilerOptions.paths = { "@ref/*": nrefsPath }; 1006 host.writeFile(bTsconfig.path, JSON.stringify(bTsConfigJson)); 1007 }, 1008 expectedEditErrors: emptyArray, 1009 expectedProgramFiles, 1010 expectedProjectFiles, 1011 expectedWatchedFiles: expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()), 1012 expectedProjectWatchedFiles: expectedProjectFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()), 1013 expectedWatchedDirectoriesRecursive: (multiFolder ? expectedWatchedDirectoriesRecursiveWithoutA : expectedWatchedDirectoriesRecursive).concat(getFilePathInProject(project, "nrefs").toLowerCase()), 1014 dependencies: [ 1015 [nrefs.path, [nrefs.path]], 1016 [bDts, [bDts, nrefs.path]], 1017 [refs.path, [refs.path]], 1018 [cTs.path, [cTs.path, refs.path, bDts, nrefs.path]], 1019 ], 1020 // revert the update 1021 revert: host => host.writeFile(bTsconfig.path, bTsconfig.content), 1022 // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open 1023 orphanInfosAfterEdit: [aTs.path.toLowerCase()], 1024 // AfterRevert:: Extra watched files on server since the script infos arent deleted till next file open 1025 orphanInfosAfterRevert: [nrefs.path.toLowerCase()] 1026 }); 1027 }); 1028 1029 describe("deleting referenced config file", () => { 1030 const expectedProgramFiles = [cTs.path, bTs.path, refs.path, libFile.path]; 1031 const expectedWatchedFiles = expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path).map(s => s.toLowerCase()); 1032 const [, ...expectedWatchedDirectoriesRecursiveWithoutA] = expectedWatchedDirectoriesRecursive; // Not looking in a folder for resolution in multi folder scenario 1033 // Resolutions should change now 1034 // Should map to b.ts instead with options from our own config 1035 verifyScenario({ 1036 edit: host => host.deleteFile(bTsconfig.path), 1037 schedulesFailedWatchUpdate: multiFolder, 1038 expectedEditErrors: [ 1039 `${multiFolder ? "c/tsconfig.json" : "tsconfig.c.json"}(9,21): error TS6053: File '/user/username/projects/transitiveReferences/${multiFolder ? "b" : "tsconfig.b.json"}' not found.\n` 1040 ], 1041 expectedProgramFiles, 1042 expectedProjectFiles: expectedProgramFiles, 1043 expectedWatchedFiles, 1044 expectedProjectWatchedFiles: expectedWatchedFiles, 1045 expectedWatchedDirectoriesRecursive: multiFolder ? expectedWatchedDirectoriesRecursiveWithoutA : expectedWatchedDirectoriesRecursive, 1046 dependencies: [ 1047 [bTs.path, [bTs.path, refs.path]], 1048 [refs.path, [refs.path]], 1049 [cTs.path, [cTs.path, refs.path, bTs.path]], 1050 ], 1051 // revert the update 1052 revert: host => host.writeFile(bTsconfig.path, bTsconfig.content), 1053 // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open 1054 orphanInfosAfterEdit: [aTs.path.toLowerCase(), aTsconfig.path.toLowerCase()], 1055 }); 1056 }); 1057 1058 describe("deleting transitively referenced config file", () => { 1059 verifyScenario({ 1060 edit: host => host.deleteFile(aTsconfig.path), 1061 schedulesFailedWatchUpdate: multiFolder, 1062 expectedEditErrors: [ 1063 `${multiFolder ? "b/tsconfig.json" : "tsconfig.b.json"}(10,21): error TS6053: File '/user/username/projects/transitiveReferences/${multiFolder ? "a" : "tsconfig.a.json"}' not found.\n` 1064 ], 1065 expectedProgramFiles: expectedProgramFiles.map(s => s.replace(aDts, aTs.path)), 1066 expectedProjectFiles, 1067 expectedWatchedFiles: expectedWatchedFiles.map(s => s.replace(aDts.toLowerCase(), aTs.path.toLocaleLowerCase())), 1068 expectedProjectWatchedFiles, 1069 expectedWatchedDirectoriesRecursive, 1070 dependencies: [ 1071 [aTs.path, [aTs.path]], 1072 [bDts, [bDts, aTs.path]], 1073 [refs.path, [refs.path]], 1074 [cTs.path, [cTs.path, refs.path, bDts, aTs.path]], 1075 ], 1076 // revert the update 1077 revert: host => host.writeFile(aTsconfig.path, aTsconfig.content), 1078 }); 1079 }); 1080 } 1081 1082 describe("when config files are side by side", () => { 1083 verifyTransitiveReferences(/*multiFolder*/ false); 1084 1085 it("when referenced project uses different module resolution", () => { 1086 const bTs: File = { 1087 path: bTsFile.path, 1088 content: `import {A} from "a";export const b = new A();` 1089 }; 1090 const bTsconfig: File = { 1091 path: bTsconfigFile.path, 1092 content: JSON.stringify({ 1093 compilerOptions: { composite: true, moduleResolution: "classic" }, 1094 files: ["b.ts"], 1095 references: [{ path: "tsconfig.a.json" }] 1096 }) 1097 }; 1098 const allFiles = [libFile, aTsFile, bTs, cTsFile, aTsconfigFile, bTsconfig, cTsconfigFile, refs]; 1099 const aDts = dtsFile("a"), bDts = dtsFile("b"); 1100 const expectedFiles = [jsFile("a"), aDts, jsFile("b"), bDts, jsFile("c")]; 1101 const expectedProgramFiles = [cTsFile.path, libFile.path, aDts, refs.path, bDts]; 1102 const expectedWatchedFiles = expectedProgramFiles.concat(cTsconfigFile.path, bTsconfigFile.path, aTsconfigFile.path).map(s => s.toLowerCase()); 1103 const expectedWatchedDirectoriesRecursive = [ 1104 getFilePathInProject(project, "refs"), // Failed lookup since refs/a.ts does not exist 1105 ...projectSystem.getTypeRootsFromLocation(getProjectPath(project)) 1106 ].map(s => s.toLowerCase()); 1107 1108 const defaultDependencies: readonly [string, readonly string[]][] = [ 1109 [aDts, [aDts]], 1110 [bDts, [bDts, aDts]], 1111 [refs.path, [refs.path]], 1112 [cTsFile.path, [cTsFile.path, refs.path, bDts, aDts]] 1113 ]; 1114 function getOutputFileStamps(host: TsBuildWatchSystem) { 1115 return expectedFiles.map(file => transformOutputToOutputFileStamp(file, host)); 1116 } 1117 const { host, watch } = createSolutionAndWatchModeOfProject(allFiles, getProjectPath(project), "tsconfig.c.json", "tsconfig.c.json", getOutputFileStamps); 1118 verifyWatchState(host, watch, expectedProgramFiles, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, defaultDependencies); 1119 }); 1120 }); 1121 describe("when config files are in side by side folders", () => { 1122 verifyTransitiveReferences(/*multiFolder*/ true); 1123 }); 1124 }); 1125 }); 1126 }); 1127 1128 verifyTscWatch({ 1129 scenario, 1130 subScenario: "incremental updates in verbose mode", 1131 commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`, "-verbose"], 1132 sys: () => createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), 1133 changes: [ 1134 { 1135 caption: "Make non dts change", 1136 change: sys => sys.writeFile(logic[1].path, `${logic[1].content} 1137function someFn() { }`), 1138 timeouts: sys => { 1139 sys.checkTimeoutQueueLengthAndRun(1); // build logic 1140 sys.checkTimeoutQueueLengthAndRun(1); // build tests 1141 }, 1142 }, 1143 { 1144 caption: "Make dts change", 1145 change: sys => sys.writeFile(logic[1].path, `${logic[1].content} 1146export function someFn() { }`), 1147 timeouts: sys => { 1148 sys.checkTimeoutQueueLengthAndRun(1); // build logic 1149 sys.checkTimeoutQueueLengthAndRun(1); // build tests 1150 }, 1151 } 1152 ], 1153 }); 1154 1155 verifyTscWatch({ 1156 scenario, 1157 subScenario: "works when noUnusedParameters changes to false", 1158 commandLineArgs: ["-b", "-w"], 1159 sys: () => { 1160 const index: File = { 1161 path: `${projectRoot}/index.ts`, 1162 content: `const fn = (a: string, b: string) => b;` 1163 }; 1164 const configFile: File = { 1165 path: `${projectRoot}/tsconfig.json`, 1166 content: JSON.stringify({ 1167 compilerOptions: { 1168 noUnusedParameters: true 1169 } 1170 }) 1171 }; 1172 return createWatchedSystem([index, configFile, libFile], { currentDirectory: projectRoot }); 1173 }, 1174 changes: [ 1175 { 1176 caption: "Change tsconfig to set noUnusedParameters to false", 1177 change: sys => sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ 1178 compilerOptions: { 1179 noUnusedParameters: false 1180 } 1181 })), 1182 timeouts: runQueuedTimeoutCallbacks, 1183 }, 1184 ] 1185 }); 1186 1187 verifyTscWatch({ 1188 scenario, 1189 subScenario: "should not trigger recompilation because of program emit", 1190 commandLineArgs: ["-b", "-w", `${project}/${SubProject.core}`, "-verbose"], 1191 sys: () => createWatchedSystem([libFile, ...core], { currentDirectory: projectsLocation }), 1192 changes: [ 1193 noopChange, 1194 { 1195 caption: "Add new file", 1196 change: sys => sys.writeFile(`${project}/${SubProject.core}/file3.ts`, `export const y = 10;`), 1197 timeouts: checkSingleTimeoutQueueLengthAndRun 1198 }, 1199 noopChange, 1200 ] 1201 }); 1202 1203 verifyTscWatch({ 1204 scenario, 1205 subScenario: "should not trigger recompilation because of program emit with outDir specified", 1206 commandLineArgs: ["-b", "-w", `${project}/${SubProject.core}`, "-verbose"], 1207 sys: () => { 1208 const [coreConfig, ...rest] = core; 1209 const newCoreConfig: File = { path: coreConfig.path, content: JSON.stringify({ compilerOptions: { composite: true, outDir: "outDir" } }) }; 1210 return createWatchedSystem([libFile, newCoreConfig, ...rest], { currentDirectory: projectsLocation }); 1211 }, 1212 changes: [ 1213 noopChange, 1214 { 1215 caption: "Add new file", 1216 change: sys => sys.writeFile(`${project}/${SubProject.core}/file3.ts`, `export const y = 10;`), 1217 timeouts: checkSingleTimeoutQueueLengthAndRun 1218 }, 1219 noopChange 1220 ] 1221 }); 1222 1223 verifyTscWatch({ 1224 scenario, 1225 subScenario: "works with extended source files", 1226 commandLineArgs: ["-b", "-w", "-v", "project1.tsconfig.json", "project2.tsconfig.json"], 1227 sys: () => { 1228 const alphaExtendedConfigFile: File = { 1229 path: "/a/b/alpha.tsconfig.json", 1230 content: "{}" 1231 }; 1232 const project1Config: File = { 1233 path: "/a/b/project1.tsconfig.json", 1234 content: JSON.stringify({ 1235 extends: "./alpha.tsconfig.json", 1236 compilerOptions: { 1237 composite: true, 1238 }, 1239 files: [commonFile1.path, commonFile2.path] 1240 }) 1241 }; 1242 const bravoExtendedConfigFile: File = { 1243 path: "/a/b/bravo.tsconfig.json", 1244 content: JSON.stringify({ 1245 extends: "./alpha.tsconfig.json" 1246 }) 1247 }; 1248 const otherFile: File = { 1249 path: "/a/b/other.ts", 1250 content: "let z = 0;", 1251 }; 1252 const project2Config: File = { 1253 path: "/a/b/project2.tsconfig.json", 1254 content: JSON.stringify({ 1255 extends: "./bravo.tsconfig.json", 1256 compilerOptions: { 1257 composite: true, 1258 }, 1259 files: [otherFile.path] 1260 }) 1261 }; 1262 return createWatchedSystem([ 1263 libFile, 1264 alphaExtendedConfigFile, project1Config, commonFile1, commonFile2, 1265 bravoExtendedConfigFile, project2Config, otherFile 1266 ], { currentDirectory: "/a/b" }); 1267 }, 1268 changes: [ 1269 { 1270 caption: "Modify alpha config", 1271 change: sys => sys.writeFile("/a/b/alpha.tsconfig.json", JSON.stringify({ 1272 compilerOptions: { strict: true } 1273 })), 1274 timeouts: checkSingleTimeoutQueueLengthAndRun // Build project1 1275 }, 1276 { 1277 caption: "Build project 2", 1278 change: noop, 1279 timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2 1280 }, 1281 { 1282 caption: "change bravo config", 1283 change: sys => sys.writeFile("/a/b/bravo.tsconfig.json", JSON.stringify({ 1284 extends: "./alpha.tsconfig.json", 1285 compilerOptions: { strict: false } 1286 })), 1287 timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2 1288 }, 1289 { 1290 caption: "project 2 extends alpha", 1291 change: sys => sys.writeFile("/a/b/project2.tsconfig.json", JSON.stringify({ 1292 extends: "./alpha.tsconfig.json", 1293 })), 1294 timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2 1295 }, 1296 { 1297 caption: "update aplha config", 1298 change: sys => sys.writeFile("/a/b/alpha.tsconfig.json", "{}"), 1299 timeouts: checkSingleTimeoutQueueLengthAndRun, // build project1 1300 }, 1301 { 1302 caption: "Build project 2", 1303 change: noop, 1304 timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout // Build project2 1305 }, 1306 ] 1307 }); 1308 1309 verifyTscWatch({ 1310 scenario, 1311 subScenario: "works correctly when project with extended config is removed", 1312 commandLineArgs: ["-b", "-w", "-v"], 1313 sys: () => { 1314 const alphaExtendedConfigFile: File = { 1315 path: "/a/b/alpha.tsconfig.json", 1316 content: JSON.stringify({ 1317 strict: true 1318 }) 1319 }; 1320 const project1Config: File = { 1321 path: "/a/b/project1.tsconfig.json", 1322 content: JSON.stringify({ 1323 extends: "./alpha.tsconfig.json", 1324 compilerOptions: { 1325 composite: true, 1326 }, 1327 files: [commonFile1.path, commonFile2.path] 1328 }) 1329 }; 1330 const bravoExtendedConfigFile: File = { 1331 path: "/a/b/bravo.tsconfig.json", 1332 content: JSON.stringify({ 1333 strict: true 1334 }) 1335 }; 1336 const otherFile: File = { 1337 path: "/a/b/other.ts", 1338 content: "let z = 0;", 1339 }; 1340 const project2Config: File = { 1341 path: "/a/b/project2.tsconfig.json", 1342 content: JSON.stringify({ 1343 extends: "./bravo.tsconfig.json", 1344 compilerOptions: { 1345 composite: true, 1346 }, 1347 files: [otherFile.path] 1348 }) 1349 }; 1350 const configFile: File = { 1351 path: "/a/b/tsconfig.json", 1352 content: JSON.stringify({ 1353 references: [ 1354 { 1355 path: "./project1.tsconfig.json", 1356 }, 1357 { 1358 path: "./project2.tsconfig.json", 1359 }, 1360 ], 1361 files: [], 1362 }) 1363 }; 1364 return createWatchedSystem([ 1365 libFile, configFile, 1366 alphaExtendedConfigFile, project1Config, commonFile1, commonFile2, 1367 bravoExtendedConfigFile, project2Config, otherFile 1368 ], { currentDirectory: "/a/b" }); 1369 }, 1370 changes: [ 1371 { 1372 caption: "Remove project2 from base config", 1373 change: sys => sys.modifyFile("/a/b/tsconfig.json", JSON.stringify({ 1374 references: [ 1375 { 1376 path: "./project1.tsconfig.json", 1377 }, 1378 ], 1379 files: [], 1380 })), 1381 timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, 1382 } 1383 ] 1384 }); 1385 }); 1386 1387 describe("unittests:: tsbuild:: watchMode:: with demo project", () => { 1388 const projectLocation = `${projectsLocation}/demo`; 1389 let coreFiles: File[]; 1390 let animalFiles: File[]; 1391 let zooFiles: File[]; 1392 let solutionFile: File; 1393 let baseConfig: File; 1394 let allFiles: File[]; 1395 before(() => { 1396 coreFiles = subProjectFiles("core", ["tsconfig.json", "utilities.ts"]); 1397 animalFiles = subProjectFiles("animals", ["tsconfig.json", "animal.ts", "dog.ts", "index.ts"]); 1398 zooFiles = subProjectFiles("zoo", ["tsconfig.json", "zoo.ts"]); 1399 solutionFile = projectFile("tsconfig.json"); 1400 baseConfig = projectFile("tsconfig-base.json"); 1401 allFiles = [...coreFiles, ...animalFiles, ...zooFiles, solutionFile, baseConfig, { path: libFile.path, content: libContent }]; 1402 }); 1403 1404 after(() => { 1405 coreFiles = undefined!; 1406 animalFiles = undefined!; 1407 zooFiles = undefined!; 1408 solutionFile = undefined!; 1409 baseConfig = undefined!; 1410 allFiles = undefined!; 1411 }); 1412 1413 verifyTscWatch({ 1414 scenario: "demo", 1415 subScenario: "updates with circular reference", 1416 commandLineArgs: ["-b", "-w", "-verbose"], 1417 sys: () => { 1418 const sys = createWatchedSystem(allFiles, { currentDirectory: projectLocation }); 1419 sys.writeFile(coreFiles[0].path, coreFiles[0].content.replace( 1420 "}", 1421 `}, 1422 "references": [ 1423 { 1424 "path": "../zoo" 1425 } 1426 ]` 1427 )); 1428 return sys; 1429 }, 1430 changes: [ 1431 { 1432 caption: "Fix error", 1433 change: sys => sys.writeFile(coreFiles[0].path, coreFiles[0].content), 1434 timeouts: sys => { 1435 sys.checkTimeoutQueueLengthAndRun(1); // build core 1436 sys.checkTimeoutQueueLengthAndRun(1); // build animals 1437 sys.checkTimeoutQueueLengthAndRun(1); // build zoo 1438 sys.checkTimeoutQueueLengthAndRun(1); // build solution 1439 sys.checkTimeoutQueueLength(0); 1440 }, 1441 } 1442 ] 1443 }); 1444 1445 verifyTscWatch({ 1446 scenario: "demo", 1447 subScenario: "updates with bad reference", 1448 commandLineArgs: ["-b", "-w", "-verbose"], 1449 sys: () => { 1450 const sys = createWatchedSystem(allFiles, { currentDirectory: projectLocation }); 1451 sys.writeFile(coreFiles[1].path, `import * as A from '../animals'; 1452${coreFiles[1].content}`); 1453 return sys; 1454 }, 1455 changes: [ 1456 { 1457 caption: "Prepend a line", 1458 change: sys => sys.writeFile(coreFiles[1].path, ` 1459import * as A from '../animals'; 1460${coreFiles[1].content}`), 1461 // build core 1462 timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, 1463 } 1464 ] 1465 }); 1466 1467 function subProjectFiles(subProject: string, fileNames: readonly string[]): File[] { 1468 return fileNames.map(file => projectFile(`${subProject}/${file}`)); 1469 } 1470 1471 function projectFile(fileName: string): File { 1472 return getFileFromProject("demo", fileName); 1473 } 1474 }); 1475 1476 describe("unittests:: tsbuild:: watchMode:: with noEmitOnError", () => { 1477 function change(caption: string, content: string): TscWatchCompileChange { 1478 return { 1479 caption, 1480 change: sys => sys.writeFile(`${projectsLocation}/noEmitOnError/src/main.ts`, content), 1481 // build project 1482 timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, 1483 }; 1484 } 1485 1486 const noChange: TscWatchCompileChange = { 1487 caption: "No change", 1488 change: sys => sys.writeFile(`${projectsLocation}/noEmitOnError/src/main.ts`, sys.readFile(`${projectsLocation}/noEmitOnError/src/main.ts`)!), 1489 // build project 1490 timeouts: checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout, 1491 }; 1492 verifyTscWatch({ 1493 scenario: "noEmitOnError", 1494 subScenario: "does not emit any files on error", 1495 commandLineArgs: ["-b", "-w", "-verbose"], 1496 sys: () => createWatchedSystem( 1497 [ 1498 ...["tsconfig.json", "shared/types/db.ts", "src/main.ts", "src/other.ts"] 1499 .map(f => getFileFromProject("noEmitOnError", f)), 1500 { path: libFile.path, content: libContent } 1501 ], 1502 { currentDirectory: `${projectsLocation}/noEmitOnError` } 1503 ), 1504 changes: [ 1505 noChange, 1506 change("Fix Syntax error", `import { A } from "../shared/types/db"; 1507const a = { 1508 lastName: 'sdsd' 1509};`), 1510 change("Semantic Error", `import { A } from "../shared/types/db"; 1511const a: string = 10;`), 1512 noChange, 1513 change("Fix Semantic Error", `import { A } from "../shared/types/db"; 1514const a: string = "hello";`), 1515 noChange, 1516 ], 1517 baselineIncremental: true 1518 }); 1519 }); 1520 1521 describe("unittests:: tsbuild:: watchMode:: with reexport when referenced project reexports definitions from another file", () => { 1522 function build(sys: WatchedSystem) { 1523 sys.checkTimeoutQueueLengthAndRun(1); // build src/pure 1524 sys.checkTimeoutQueueLengthAndRun(1); // build src/main 1525 sys.checkTimeoutQueueLengthAndRun(1); // build src 1526 sys.checkTimeoutQueueLength(0); 1527 } 1528 verifyTscWatch({ 1529 scenario: "reexport", 1530 subScenario: "Reports errors correctly", 1531 commandLineArgs: ["-b", "-w", "-verbose", "src"], 1532 sys: () => createWatchedSystem( 1533 [ 1534 ...[ 1535 "src/tsconfig.json", 1536 "src/main/tsconfig.json", "src/main/index.ts", 1537 "src/pure/tsconfig.json", "src/pure/index.ts", "src/pure/session.ts" 1538 ] 1539 .map(f => getFileFromProject("reexport", f)), 1540 { path: libFile.path, content: libContent } 1541 ], 1542 { currentDirectory: `${projectsLocation}/reexport` } 1543 ), 1544 changes: [ 1545 { 1546 caption: "Introduce error", 1547 change: sys => replaceFileText(sys, `${projectsLocation}/reexport/src/pure/session.ts`, "// ", ""), 1548 timeouts: build, 1549 }, 1550 { 1551 caption: "Fix error", 1552 change: sys => replaceFileText(sys, `${projectsLocation}/reexport/src/pure/session.ts`, "bar: ", "// bar: "), 1553 timeouts: build 1554 } 1555 ] 1556 }); 1557 }); 1558 1559 describe("unittests:: tsbuild:: watchMode:: configFileErrors:: reports syntax errors in config file", () => { 1560 function build(sys: WatchedSystem) { 1561 sys.checkTimeoutQueueLengthAndRun(1); // build the project 1562 sys.checkTimeoutQueueLength(0); 1563 } 1564 verifyTscWatch({ 1565 scenario: "configFileErrors", 1566 subScenario: "reports syntax errors in config file", 1567 sys: () => createWatchedSystem( 1568 [ 1569 { path: `${projectRoot}/a.ts`, content: "export function foo() { }" }, 1570 { path: `${projectRoot}/b.ts`, content: "export function bar() { }" }, 1571 { 1572 path: `${projectRoot}/tsconfig.json`, 1573 content: Utils.dedent` 1574{ 1575 "compilerOptions": { 1576 "composite": true, 1577 }, 1578 "files": [ 1579 "a.ts" 1580 "b.ts" 1581 ] 1582}` 1583 }, 1584 libFile 1585 ], 1586 { currentDirectory: projectRoot } 1587 ), 1588 commandLineArgs: ["--b", "-w"], 1589 changes: [ 1590 { 1591 caption: "reports syntax errors after change to config file", 1592 change: sys => replaceFileText(sys, `${projectRoot}/tsconfig.json`, ",", `, 1593 "declaration": true,`), 1594 timeouts: build, 1595 }, 1596 { 1597 caption: "reports syntax errors after change to ts file", 1598 change: sys => replaceFileText(sys, `${projectRoot}/a.ts`, "foo", "fooBar"), 1599 timeouts: build, 1600 }, 1601 { 1602 caption: "reports error when there is no change to tsconfig file", 1603 change: sys => replaceFileText(sys, `${projectRoot}/tsconfig.json`, "", ""), 1604 timeouts: build, 1605 }, 1606 { 1607 caption: "builds after fixing config file errors", 1608 change: sys => sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ 1609 compilerOptions: { composite: true, declaration: true }, 1610 files: ["a.ts", "b.ts"] 1611 })), 1612 timeouts: build, 1613 } 1614 ] 1615 }); 1616 }); 1617} 1618