1namespace ts { 2 describe("unittests:: tsbuild:: on 'sample1' project", () => { 3 let projFs: vfs.FileSystem; 4 const testsOutputs = ["/src/tests/index.js", "/src/tests/index.d.ts", "/src/tests/tsconfig.tsbuildinfo"]; 5 const logicOutputs = ["/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts", "/src/logic/tsconfig.tsbuildinfo"]; 6 const coreOutputs = ["/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map", "/src/core/tsconfig.tsbuildinfo"]; 7 const allExpectedOutputs = [...testsOutputs, ...logicOutputs, ...coreOutputs]; 8 9 before(() => { 10 projFs = loadProjectFromDisk("tests/projects/sample1"); 11 }); 12 13 after(() => { 14 projFs = undefined!; // Release the contents 15 }); 16 17 function getSampleFsAfterBuild() { 18 const fs = projFs.shadow(); 19 const host = fakes.SolutionBuilderHost.create(fs); 20 const builder = createSolutionBuilder(host, ["/src/tests"], {}); 21 builder.build(); 22 fs.makeReadonly(); 23 return fs; 24 } 25 26 describe("sanity check of clean build of 'sample1' project", () => { 27 verifyTsc({ 28 scenario: "sample1", 29 subScenario: "builds correctly when outDir is specified", 30 fs: () => projFs, 31 commandLineArgs: ["--b", "/src/tests"], 32 modifyFs: fs => fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({ 33 compilerOptions: { composite: true, declaration: true, sourceMap: true, outDir: "outDir" }, 34 references: [{ path: "../core" }] 35 })), 36 }); 37 38 verifyTsc({ 39 scenario: "sample1", 40 subScenario: "builds correctly when declarationDir is specified", 41 fs: () => projFs, 42 commandLineArgs: ["--b", "/src/tests"], 43 modifyFs: fs => fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({ 44 compilerOptions: { composite: true, declaration: true, sourceMap: true, declarationDir: "out/decls" }, 45 references: [{ path: "../core" }] 46 })), 47 }); 48 49 verifyTsc({ 50 scenario: "sample1", 51 subScenario: "builds correctly when project is not composite or doesnt have any references", 52 fs: () => projFs, 53 commandLineArgs: ["--b", "/src/core", "--verbose"], 54 modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"composite": true,`, ""), 55 }); 56 }); 57 58 describe("dry builds", () => { 59 verifyTsc({ 60 scenario: "sample1", 61 subScenario: "does not write any files in a dry build", 62 fs: () => projFs, 63 commandLineArgs: ["--b", "/src/tests", "--dry"], 64 }); 65 }); 66 67 describe("clean builds", () => { 68 verifyTscSerializedIncrementalEdits({ 69 scenario: "sample1", 70 subScenario: "removes all files it built", 71 fs: getSampleFsAfterBuild, 72 commandLineArgs: ["--b", "/src/tests", "--clean"], 73 incrementalScenarios: noChangeOnlyRuns 74 }); 75 76 it("cleans till project specified", () => { 77 const fs = projFs.shadow(); 78 const host = fakes.SolutionBuilderHost.create(fs); 79 const builder = createSolutionBuilder(host, ["/src/tests"], {}); 80 builder.build(); 81 const result = builder.clean("/src/logic"); 82 host.assertDiagnosticMessages(/*empty*/); 83 verifyOutputsPresent(fs, testsOutputs); 84 verifyOutputsAbsent(fs, [...logicOutputs, ...coreOutputs]); 85 assert.equal(result, ExitStatus.Success); 86 }); 87 88 it("cleaning project in not build order doesnt throw error", () => { 89 const fs = projFs.shadow(); 90 const host = fakes.SolutionBuilderHost.create(fs); 91 const builder = createSolutionBuilder(host, ["/src/tests"], {}); 92 builder.build(); 93 const result = builder.clean("/src/logic2"); 94 host.assertDiagnosticMessages(/*empty*/); 95 verifyOutputsPresent(fs, allExpectedOutputs); 96 assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped); 97 }); 98 }); 99 100 describe("force builds", () => { 101 verifyTscSerializedIncrementalEdits({ 102 scenario: "sample1", 103 subScenario: "always builds under with force option", 104 fs: () => projFs, 105 commandLineArgs: ["--b", "/src/tests", "--force"], 106 incrementalScenarios: noChangeOnlyRuns 107 }); 108 }); 109 110 describe("can detect when and what to rebuild", () => { 111 function initializeWithBuild(opts?: BuildOptions) { 112 const { fs, tick } = getFsWithTime(projFs); 113 const host = fakes.SolutionBuilderHost.create(fs); 114 let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); 115 builder.build(); 116 host.clearDiagnostics(); 117 tick(); 118 builder = createSolutionBuilder(host, ["/src/tests"], { ...(opts || {}), verbose: true }); 119 return { fs, host, builder }; 120 } 121 122 verifyTscIncrementalEdits({ 123 scenario: "sample1", 124 subScenario: "can detect when and what to rebuild", 125 fs: getSampleFsAfterBuild, 126 commandLineArgs: ["--b", "/src/tests", "--verbose"], 127 incrementalScenarios: [ 128 // Update a file in the leaf node (tests), only it should rebuild the last one 129 { 130 subScenario: "Only builds the leaf node project", 131 buildKind: BuildKind.IncrementalDtsUnchanged, 132 modifyFs: fs => fs.writeFileSync("/src/tests/index.ts", "const m = 10;"), 133 }, 134 // Update a file in the parent (without affecting types), should get fast downstream builds 135 { 136 subScenario: "Detects type-only changes in upstream projects", 137 buildKind: BuildKind.IncrementalDtsChange, 138 modifyFs: fs => replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"), 139 }, 140 { 141 subScenario: "indicates that it would skip builds during a dry build", 142 buildKind: BuildKind.IncrementalDtsUnchanged, 143 modifyFs: noop, 144 commandLineArgs: ["--b", "/src/tests", "--dry"], 145 }, 146 { 147 subScenario: "rebuilds from start if force option is set", 148 buildKind: BuildKind.IncrementalDtsChange, 149 modifyFs: noop, 150 commandLineArgs: ["--b", "/src/tests", "--verbose", "--force"], 151 }, 152 { 153 subScenario: "rebuilds when tsconfig changes", 154 buildKind: BuildKind.IncrementalDtsChange, 155 modifyFs: fs => replaceText(fs, "/src/tests/tsconfig.json", `"composite": true`, `"composite": true, "target": "es3"`), 156 }, 157 ] 158 }); 159 160 it("rebuilds completely when version in tsbuildinfo doesnt match ts version", () => { 161 const { host, builder } = initializeWithBuild(); 162 changeCompilerVersion(host); 163 builder.build(); 164 host.assertDiagnosticMessages( 165 getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), 166 [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/core/tsconfig.json", fakes.version, version], 167 [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], 168 [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/logic/tsconfig.json", fakes.version, version], 169 [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], 170 [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/tests/tsconfig.json", fakes.version, version], 171 [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"], 172 ); 173 }); 174 175 it("does not rebuild if there is no program and bundle in the ts build info event if version doesnt match ts version", () => { 176 const { fs, tick } = getFsWithTime(projFs); 177 const host = fakes.SolutionBuilderHost.create(fs, /*options*/ undefined, /*setParentNodes*/ undefined, createAbstractBuilder); 178 let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); 179 builder.build(); 180 host.assertDiagnosticMessages( 181 getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), 182 [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], 183 [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], 184 [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], 185 [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], 186 [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/tests/tsconfig.json", "src/tests/index.js"], 187 [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"] 188 ); 189 verifyOutputsPresent(fs, allExpectedOutputs); 190 191 host.clearDiagnostics(); 192 tick(); 193 builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); 194 changeCompilerVersion(host); 195 builder.build(); 196 host.assertDiagnosticMessages( 197 getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), 198 [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/core/tsconfig.json", "src/core/anotherModule.ts", "src/core/anotherModule.js"], 199 [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/logic/tsconfig.json", "src/logic/index.ts", "src/logic/index.js"], 200 [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/tests/tsconfig.json", "src/tests/index.ts", "src/tests/index.js"] 201 ); 202 }); 203 204 verifyTscSerializedIncrementalEdits({ 205 scenario: "sample1", 206 subScenario: "rebuilds when extended config file changes", 207 fs: () => projFs, 208 commandLineArgs: ["--b", "/src/tests", "--verbose"], 209 modifyFs: fs => { 210 fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: { target: "es3" } })); 211 replaceText(fs, "/src/tests/tsconfig.json", `"references": [`, `"extends": "./tsconfig.base.json", "references": [`); 212 }, 213 incrementalScenarios: [{ 214 buildKind: BuildKind.IncrementalDtsChange, 215 modifyFs: fs => fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: {} })) 216 }] 217 }); 218 219 it("builds till project specified", () => { 220 const fs = projFs.shadow(); 221 const host = fakes.SolutionBuilderHost.create(fs); 222 const builder = createSolutionBuilder(host, ["/src/tests"], {}); 223 const result = builder.build("/src/logic"); 224 host.assertDiagnosticMessages(/*empty*/); 225 verifyOutputsAbsent(fs, testsOutputs); 226 verifyOutputsPresent(fs, [...logicOutputs, ...coreOutputs]); 227 assert.equal(result, ExitStatus.Success); 228 }); 229 230 it("building project in not build order doesnt throw error", () => { 231 const fs = projFs.shadow(); 232 const host = fakes.SolutionBuilderHost.create(fs); 233 const builder = createSolutionBuilder(host, ["/src/tests"], {}); 234 const result = builder.build("/src/logic2"); 235 host.assertDiagnosticMessages(/*empty*/); 236 verifyOutputsAbsent(fs, allExpectedOutputs); 237 assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped); 238 }); 239 240 it("building using getNextInvalidatedProject", () => { 241 interface SolutionBuilderResult<T> { 242 project: ResolvedConfigFileName; 243 result: T; 244 } 245 246 const fs = projFs.shadow(); 247 const host = fakes.SolutionBuilderHost.create(fs); 248 const builder = createSolutionBuilder(host, ["/src/tests"], {}); 249 verifyBuildNextResult({ 250 project: "/src/core/tsconfig.json" as ResolvedConfigFileName, 251 result: ExitStatus.Success 252 }, coreOutputs, [...logicOutputs, ...testsOutputs]); 253 254 verifyBuildNextResult({ 255 project: "/src/logic/tsconfig.json" as ResolvedConfigFileName, 256 result: ExitStatus.Success 257 }, [...coreOutputs, ...logicOutputs], testsOutputs); 258 259 verifyBuildNextResult({ 260 project: "/src/tests/tsconfig.json" as ResolvedConfigFileName, 261 result: ExitStatus.Success 262 }, allExpectedOutputs, emptyArray); 263 264 verifyBuildNextResult(/*expected*/ undefined, allExpectedOutputs, emptyArray); 265 266 function verifyBuildNextResult( 267 expected: SolutionBuilderResult<ExitStatus> | undefined, 268 presentOutputs: readonly string[], 269 absentOutputs: readonly string[] 270 ) { 271 const project = builder.getNextInvalidatedProject(); 272 const result = project && project.done(); 273 assert.deepEqual(project && { project: project.project, result }, expected); 274 verifyOutputsPresent(fs, presentOutputs); 275 verifyOutputsAbsent(fs, absentOutputs); 276 } 277 }); 278 279 it("building using buildReferencedProject", () => { 280 const fs = projFs.shadow(); 281 const host = fakes.SolutionBuilderHost.create(fs); 282 const builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); 283 builder.buildReferences("/src/tests"); 284 host.assertDiagnosticMessages( 285 getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json"), 286 [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], 287 [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], 288 [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], 289 [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], 290 ); 291 verifyOutputsPresent(fs, [...coreOutputs, ...logicOutputs]); 292 verifyOutputsAbsent(fs, testsOutputs); 293 }); 294 }); 295 296 describe("downstream-blocked compilations", () => { 297 verifyTsc({ 298 scenario: "sample1", 299 subScenario: "does not build downstream projects if upstream projects have errors", 300 fs: () => projFs, 301 commandLineArgs: ["--b", "/src/tests", "--verbose"], 302 modifyFs: fs => replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`) 303 }); 304 }); 305 306 describe("project invalidation", () => { 307 it("invalidates projects correctly", () => { 308 const { fs, time, tick } = getFsWithTime(projFs); 309 const host = fakes.SolutionBuilderHost.create(fs); 310 const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); 311 312 builder.build(); 313 host.assertDiagnosticMessages(/*empty*/); 314 315 // Update a timestamp in the middle project 316 tick(); 317 appendText(fs, "/src/logic/index.ts", "function foo() {}"); 318 const originalWriteFile = fs.writeFileSync; 319 const writtenFiles = new Map<string, true>(); 320 fs.writeFileSync = (path, data, encoding) => { 321 writtenFiles.set(path, true); 322 originalWriteFile.call(fs, path, data, encoding); 323 }; 324 // Because we haven't reset the build context, the builder should assume there's nothing to do right now 325 const status = builder.getUpToDateStatusOfProject("/src/logic"); 326 assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); 327 verifyInvalidation(/*expectedToWriteTests*/ false); 328 329 // Rebuild this project 330 fs.writeFileSync("/src/logic/index.ts", `${fs.readFileSync("/src/logic/index.ts")} 331export class cNew {}`); 332 verifyInvalidation(/*expectedToWriteTests*/ true); 333 334 function verifyInvalidation(expectedToWriteTests: boolean) { 335 // Rebuild this project 336 tick(); 337 builder.invalidateProject("/src/logic/tsconfig.json" as ResolvedConfigFilePath); 338 builder.buildNextInvalidatedProject(); 339 // The file should be updated 340 assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt"); 341 assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); 342 assert.isFalse(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should *not* have been rebuilt"); 343 assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); 344 writtenFiles.clear(); 345 346 // Build downstream projects should update 'tests', but not 'core' 347 tick(); 348 builder.buildNextInvalidatedProject(); 349 if (expectedToWriteTests) { 350 assert.isTrue(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should have been rebuilt"); 351 } 352 else { 353 assert.equal(writtenFiles.size, 0, "Should not write any new files"); 354 } 355 assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have new timestamp"); 356 assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); 357 } 358 }); 359 }); 360 361 const coreChanges: TscIncremental[] = [ 362 { 363 buildKind: BuildKind.IncrementalDtsChange, 364 modifyFs: fs => appendText(fs, "/src/core/index.ts", ` 365export class someClass { }`), 366 }, 367 { 368 buildKind: BuildKind.IncrementalDtsUnchanged, 369 modifyFs: fs => appendText(fs, "/src/core/index.ts", ` 370class someClass2 { }`), 371 } 372 ]; 373 374 describe("lists files", () => { 375 verifyTscSerializedIncrementalEdits({ 376 scenario: "sample1", 377 subScenario: "listFiles", 378 fs: () => projFs, 379 commandLineArgs: ["--b", "/src/tests", "--listFiles"], 380 incrementalScenarios: coreChanges 381 }); 382 verifyTscSerializedIncrementalEdits({ 383 scenario: "sample1", 384 subScenario: "listEmittedFiles", 385 fs: () => projFs, 386 commandLineArgs: ["--b", "/src/tests", "--listEmittedFiles"], 387 incrementalScenarios: coreChanges 388 }); 389 verifyTscSerializedIncrementalEdits({ 390 scenario: "sample1", 391 subScenario: "explainFiles", 392 fs: () => projFs, 393 commandLineArgs: ["--b", "/src/tests", "--explainFiles", "--v"], 394 incrementalScenarios: coreChanges 395 }); 396 }); 397 398 describe("emit output", () => { 399 verifyTscSerializedIncrementalEdits({ 400 subScenario: "sample", 401 fs: () => projFs, 402 scenario: "sample1", 403 commandLineArgs: ["--b", "/src/tests", "--verbose"], 404 baselineSourceMap: true, 405 baselineReadFileCalls: true, 406 incrementalScenarios: [ 407 ...coreChanges, 408 { 409 subScenario: "when logic config changes declaration dir", 410 buildKind: BuildKind.IncrementalDtsChange, 411 modifyFs: fs => replaceText(fs, "/src/logic/tsconfig.json", `"declaration": true,`, `"declaration": true, 412 "declarationDir": "decls",`), 413 }, 414 noChangeRun, 415 ], 416 }); 417 418 verifyTsc({ 419 scenario: "sample1", 420 subScenario: "when logic specifies tsBuildInfoFile", 421 fs: () => projFs, 422 modifyFs: fs => replaceText(fs, "/src/logic/tsconfig.json", `"composite": true,`, `"composite": true, 423 "tsBuildInfoFile": "ownFile.tsbuildinfo",`), 424 commandLineArgs: ["--b", "/src/tests", "--verbose"], 425 baselineSourceMap: true, 426 baselineReadFileCalls: true 427 }); 428 429 verifyTscSerializedIncrementalEdits({ 430 subScenario: "when declaration option changes", 431 fs: () => projFs, 432 scenario: "sample1", 433 commandLineArgs: ["--b", "/src/core", "--verbose"], 434 modifyFs: fs => fs.writeFileSync("/src/core/tsconfig.json", `{ 435 "compilerOptions": { 436 "incremental": true, 437 "skipDefaultLibCheck": true 438 } 439}`), 440 incrementalScenarios: [{ 441 buildKind: BuildKind.IncrementalDtsChange, 442 modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"incremental": true,`, `"incremental": true, "declaration": true,`), 443 }], 444 }); 445 446 verifyTscSerializedIncrementalEdits({ 447 subScenario: "when target option changes", 448 fs: () => projFs, 449 scenario: "sample1", 450 commandLineArgs: ["--b", "/src/core", "--verbose"], 451 modifyFs: fs => { 452 fs.writeFileSync("/lib/lib.esnext.full.d.ts", `/// <reference no-default-lib="true"/> 453/// <reference lib="esnext" />`); 454 fs.writeFileSync("/lib/lib.esnext.d.ts", libContent); 455 fs.writeFileSync("/lib/lib.d.ts", `/// <reference no-default-lib="true"/> 456/// <reference lib="esnext" />`); 457 fs.writeFileSync("/src/core/tsconfig.json", `{ 458 "compilerOptions": { 459 "incremental": true, 460"listFiles": true, 461"listEmittedFiles": true, 462 "target": "esnext", 463 } 464}`); 465 }, 466 incrementalScenarios: [{ 467 buildKind: BuildKind.IncrementalDtsChange, 468 modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", "esnext", "es5"), 469 }], 470 }); 471 472 verifyTscSerializedIncrementalEdits({ 473 subScenario: "when module option changes", 474 fs: () => projFs, 475 scenario: "sample1", 476 commandLineArgs: ["--b", "/src/core", "--verbose"], 477 modifyFs: fs => fs.writeFileSync("/src/core/tsconfig.json", `{ 478 "compilerOptions": { 479 "incremental": true, 480 "module": "commonjs" 481 } 482}`), 483 incrementalScenarios: [{ 484 buildKind: BuildKind.IncrementalDtsChange, 485 modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"module": "commonjs"`, `"module": "amd"`), 486 }], 487 }); 488 489 verifyTscSerializedIncrementalEdits({ 490 subScenario: "when esModuleInterop option changes", 491 fs: () => projFs, 492 scenario: "sample1", 493 commandLineArgs: ["--b", "/src/tests", "--verbose"], 494 modifyFs: fs => fs.writeFileSync("/src/tests/tsconfig.json", `{ 495 "references": [ 496 { "path": "../core" }, 497 { "path": "../logic" } 498 ], 499 "files": ["index.ts"], 500 "compilerOptions": { 501 "composite": true, 502 "declaration": true, 503 "forceConsistentCasingInFileNames": true, 504 "skipDefaultLibCheck": true, 505 "esModuleInterop": false 506 } 507}`), 508 incrementalScenarios: [{ 509 buildKind: BuildKind.IncrementalDtsChange, 510 modifyFs: fs => replaceText(fs, "/src/tests/tsconfig.json", `"esModuleInterop": false`, `"esModuleInterop": true`), 511 }], 512 }); 513 }); 514 }); 515} 516