1namespace ts { 2 describe("unittests:: tsbuild:: on 'sample1' project", () => { 3 let projFs: vfs.FileSystem; 4 let projFsWithBuild: vfs.FileSystem; 5 before(() => { 6 projFs = loadProjectFromDisk("tests/projects/sample1"); 7 }); 8 9 after(() => { 10 projFs = undefined!; // Release the contents 11 projFsWithBuild = undefined!; 12 }); 13 14 function getTsBuildProjectFile(project: string, file: string): tscWatch.File { 15 return { 16 path: TestFSWithWatch.getTsBuildProjectFilePath(project, file), 17 content: projFs.readFileSync(`/src/${project}/${file}`, "utf8")! 18 }; 19 } 20 21 function getSampleFsAfterBuild() { 22 if (projFsWithBuild) return projFsWithBuild; 23 const fs = projFs.shadow(); 24 const sys = new fakes.System(fs, { executingFilePath: "/lib/tsc" }); 25 const host = createSolutionBuilderHostForBaseline(sys as TscCompileSystem); 26 const builder = createSolutionBuilder(host, ["/src/tests"], {}); 27 builder.build(); 28 fs.makeReadonly(); 29 return projFsWithBuild = fs; 30 } 31 32 describe("sanity check of clean build of 'sample1' project", () => { 33 verifyTsc({ 34 scenario: "sample1", 35 subScenario: "builds correctly when outDir is specified", 36 fs: () => projFs, 37 commandLineArgs: ["--b", "/src/tests"], 38 modifyFs: fs => fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({ 39 compilerOptions: { composite: true, declaration: true, sourceMap: true, outDir: "outDir" }, 40 references: [{ path: "../core" }] 41 })), 42 }); 43 44 verifyTsc({ 45 scenario: "sample1", 46 subScenario: "builds correctly when declarationDir is specified", 47 fs: () => projFs, 48 commandLineArgs: ["--b", "/src/tests"], 49 modifyFs: fs => fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({ 50 compilerOptions: { composite: true, declaration: true, sourceMap: true, declarationDir: "out/decls" }, 51 references: [{ path: "../core" }] 52 })), 53 }); 54 55 verifyTsc({ 56 scenario: "sample1", 57 subScenario: "builds correctly when project is not composite or doesnt have any references", 58 fs: () => projFs, 59 commandLineArgs: ["--b", "/src/core", "--verbose"], 60 modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"composite": true,`, ""), 61 }); 62 }); 63 64 describe("dry builds", () => { 65 verifyTsc({ 66 scenario: "sample1", 67 subScenario: "does not write any files in a dry build", 68 fs: () => projFs, 69 commandLineArgs: ["--b", "/src/tests", "--dry"], 70 }); 71 }); 72 73 describe("clean builds", () => { 74 verifyTscWithEdits({ 75 scenario: "sample1", 76 subScenario: "removes all files it built", 77 fs: getSampleFsAfterBuild, 78 commandLineArgs: ["--b", "/src/tests", "--clean"], 79 edits: noChangeOnlyRuns 80 }); 81 82 verifyTscCompileLike(testTscCompileLike, { 83 scenario: "sample1", 84 subScenario: "cleans till project specified", 85 fs: getSampleFsAfterBuild, 86 commandLineArgs: ["--b", "/src/logic", "--clean"], 87 compile: sys => { 88 const buildHost = createSolutionBuilderHostForBaseline(sys); 89 const builder = createSolutionBuilder(buildHost, ["/src/third/tsconfig.json"], {}); 90 sys.exit(builder.clean("/src/logic")); 91 } 92 }); 93 94 verifyTscCompileLike(testTscCompileLike, { 95 scenario: "sample1", 96 subScenario: "cleaning project in not build order doesnt throw error", 97 fs: getSampleFsAfterBuild, 98 commandLineArgs: ["--b", "/src/logic2", "--clean"], 99 compile: sys => { 100 const buildHost = createSolutionBuilderHostForBaseline(sys); 101 const builder = createSolutionBuilder(buildHost, ["/src/third/tsconfig.json"], {}); 102 sys.exit(builder.clean("/src/logic2")); 103 } 104 }); 105 }); 106 107 describe("force builds", () => { 108 verifyTscWithEdits({ 109 scenario: "sample1", 110 subScenario: "always builds under with force option", 111 fs: () => projFs, 112 commandLineArgs: ["--b", "/src/tests", "--force"], 113 edits: noChangeOnlyRuns 114 }); 115 }); 116 117 describe("can detect when and what to rebuild", () => { 118 verifyTscWithEdits({ 119 scenario: "sample1", 120 subScenario: "can detect when and what to rebuild", 121 fs: getSampleFsAfterBuild, 122 commandLineArgs: ["--b", "/src/tests", "--verbose"], 123 edits: [ 124 // Update a file in the leaf node (tests), only it should rebuild the last one 125 { 126 subScenario: "Only builds the leaf node project", 127 modifyFs: fs => fs.writeFileSync("/src/tests/index.ts", "const m = 10;"), 128 }, 129 // Update a file in the parent (without affecting types), should get fast downstream builds 130 { 131 subScenario: "Detects type-only changes in upstream projects", 132 modifyFs: fs => replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"), 133 }, 134 { 135 subScenario: "rebuilds when tsconfig changes", 136 modifyFs: fs => replaceText(fs, "/src/tests/tsconfig.json", `"composite": true`, `"composite": true, "target": "es3"`), 137 }, 138 ] 139 }); 140 141 verifyTscWithEdits({ 142 scenario: "sample1", 143 subScenario: "when input file text does not change but its modified time changes", 144 fs: () => projFs, 145 commandLineArgs: ["--b", "/src/tests", "--verbose"], 146 edits: [ 147 { 148 subScenario: "upstream project changes without changing file text", 149 modifyFs: fs => { 150 const time = new Date(fs.time()); 151 fs.utimesSync("/src/core/index.ts", time, time); 152 }, 153 }, 154 ] 155 }); 156 157 verifyTsc({ 158 scenario: "sample1", 159 subScenario: "indicates that it would skip builds during a dry build", 160 fs: getSampleFsAfterBuild, 161 commandLineArgs: ["--b", "/src/tests", "--dry"], 162 }); 163 164 verifyTsc({ 165 scenario: "sample1", 166 subScenario: "rebuilds from start if force option is set", 167 fs: getSampleFsAfterBuild, 168 commandLineArgs: ["--b", "/src/tests", "--verbose", "--force"], 169 }); 170 171 verifyTscWithEdits({ 172 scenario: "sample1", 173 subScenario: "tsbuildinfo has error", 174 fs: () => loadProjectFromFiles({ 175 "/src/project/main.ts": "export const x = 10;", 176 "/src/project/tsconfig.json": "{}", 177 "/src/project/tsconfig.tsbuildinfo": "Some random string", 178 }), 179 commandLineArgs: ["--b", "src/project", "-i", "-v"], 180 edits: [{ 181 subScenario: "tsbuildinfo written has error", 182 modifyFs: fs => prependText(fs, "/src/project/tsconfig.tsbuildinfo", "Some random string"), 183 }] 184 }); 185 186 verifyTscCompileLike(testTscCompileLike, { 187 scenario: "sample1", 188 subScenario: "rebuilds completely when version in tsbuildinfo doesnt match ts version", 189 fs: getSampleFsAfterBuild, 190 commandLineArgs: ["--b", "/src/tests", "--verbose"], 191 compile: sys => { 192 // Buildinfo will have version which does not match with current ts version 193 const buildHost = createSolutionBuilderHostForBaseline(sys, "FakeTSCurrentVersion"); 194 const builder = createSolutionBuilder(buildHost, ["/src/tests"], { verbose: true }); 195 sys.exit(builder.build()); 196 } 197 }); 198 199 verifyTscCompileLike(testTscCompileLike, { 200 scenario: "sample1", 201 subScenario: "does not rebuild if there is no program and bundle in the ts build info event if version doesnt match ts version", 202 fs: () => { 203 const fs = projFs.shadow(); 204 const host = fakes.SolutionBuilderHost.create(fs, /*options*/ undefined, /*setParentNodes*/ undefined, createAbstractBuilder); 205 const builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); 206 builder.build(); 207 fs.makeReadonly(); 208 return fs; 209 }, 210 commandLineArgs: ["--b", "/src/tests", "--verbose"], 211 compile: sys => { 212 // Buildinfo will have version which does not match with current ts version 213 const buildHost = createSolutionBuilderHostForBaseline(sys, "FakeTSCurrentVersion"); 214 const builder = createSolutionBuilder(buildHost, ["/src/tests"], { verbose: true }); 215 sys.exit(builder.build()); 216 }, 217 }); 218 219 verifyTscWithEdits({ 220 scenario: "sample1", 221 subScenario: "rebuilds when extended config file changes", 222 fs: () => projFs, 223 commandLineArgs: ["--b", "/src/tests", "--verbose"], 224 modifyFs: fs => { 225 fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: { target: "es3" } })); 226 replaceText(fs, "/src/tests/tsconfig.json", `"references": [`, `"extends": "./tsconfig.base.json", "references": [`); 227 }, 228 edits: [{ 229 subScenario: "incremental-declaration-changes", 230 modifyFs: fs => fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: {} })) 231 }] 232 }); 233 234 verifyTscCompileLike(testTscCompileLike, { 235 scenario: "sample1", 236 subScenario: "builds till project specified", 237 fs: () => projFs, 238 commandLineArgs: ["--build", "/src/logic/tsconfig.json"], 239 compile: sys => { 240 const buildHost = createSolutionBuilderHostForBaseline(sys); 241 const builder = createSolutionBuilder(buildHost, ["/src/tests"], {}); 242 sys.exit(builder.build("/src/logic/tsconfig.json")); 243 } 244 }); 245 246 verifyTscCompileLike(testTscCompileLike, { 247 scenario: "sample1", 248 subScenario: "building project in not build order doesnt throw error", 249 fs: () => projFs, 250 commandLineArgs: ["--build", "/src/logic2/tsconfig.json"], 251 compile: sys => { 252 const buildHost = createSolutionBuilderHostForBaseline(sys); 253 const builder = createSolutionBuilder(buildHost, ["/src/tests"], {}); 254 sys.exit(builder.build("/src/logic2/tsconfig.json")); 255 } 256 }); 257 258 it("building using getNextInvalidatedProject", () => { 259 const coreConfig = getTsBuildProjectFile("core", "tsconfig.json"); 260 const coreIndex = getTsBuildProjectFile("core", "index.ts"); 261 const coreDecl = getTsBuildProjectFile("core", "some_decl.d.ts"); 262 const coreAnotherModule = getTsBuildProjectFile("core", "anotherModule.ts"); 263 const logicConfig = getTsBuildProjectFile("logic", "tsconfig.json"); 264 const logicIndex = getTsBuildProjectFile("logic", "index.ts"); 265 const testsConfig = getTsBuildProjectFile("tests", "tsconfig.json"); 266 const testsIndex = getTsBuildProjectFile("tests", "index.ts"); 267 const baseline: string[] = []; 268 let oldSnap: ReturnType<TestFSWithWatch.TestServerHost["snap"]> | undefined; 269 const system = TestFSWithWatch.changeToHostTrackingWrittenFiles( 270 fakes.patchHostForBuildInfoReadWrite( 271 tscWatch.createWatchedSystem([ 272 coreConfig, coreIndex, coreDecl, coreAnotherModule, 273 logicConfig, logicIndex, 274 testsConfig, testsIndex, 275 tscWatch.libFile 276 ]) 277 ) 278 ); 279 280 const host = createSolutionBuilderHostForBaseline(system); 281 const builder = createSolutionBuilder(host, [testsConfig.path], {}); 282 baseline.push("Input::"); 283 baselineState(); 284 verifyBuildNextResult(); // core 285 verifyBuildNextResult(); // logic 286 verifyBuildNextResult();// tests 287 verifyBuildNextResult(); // All Done 288 Harness.Baseline.runBaseline(`tsbuild/sample1/building-using-getNextInvalidatedProject.js`, baseline.join("\r\n")); 289 290 function verifyBuildNextResult() { 291 const project = builder.getNextInvalidatedProject(); 292 const result = project && project.done(); 293 baseline.push(`Project Result:: ${JSON.stringify({ project: project?.project, result })}`); 294 baselineState(); 295 } 296 297 function baselineState() { 298 system.serializeOutput(baseline); 299 system.diff(baseline, oldSnap); 300 system.writtenFiles.clear(); 301 oldSnap = system.snap(); 302 } 303 }); 304 305 verifyTscCompileLike(testTscCompileLike, { 306 scenario: "sample1", 307 subScenario: "building using buildReferencedProject", 308 fs: () => projFs, 309 commandLineArgs: ["--build", "/src/logic2/tsconfig.json"], 310 compile: sys => { 311 const buildHost = createSolutionBuilderHostForBaseline(sys); 312 const builder = createSolutionBuilder(buildHost, ["/src/tests"], { verbose: true }); 313 sys.exit(builder.buildReferences("/src/tests")); 314 } 315 }); 316 }); 317 318 describe("downstream-blocked compilations", () => { 319 verifyTscWithEdits({ 320 scenario: "sample1", 321 subScenario: "does not build downstream projects if upstream projects have errors", 322 fs: () => projFs, 323 commandLineArgs: ["--b", "/src/tests", "--verbose"], 324 modifyFs: fs => replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`), 325 edits: noChangeOnlyRuns 326 }); 327 }); 328 329 describe("project invalidation", () => { 330 it("invalidates projects correctly", () => { 331 const coreConfig = getTsBuildProjectFile("core", "tsconfig.json"); 332 const coreIndex = getTsBuildProjectFile("core", "index.ts"); 333 const coreDecl = getTsBuildProjectFile("core", "some_decl.d.ts"); 334 const coreAnotherModule = getTsBuildProjectFile("core", "anotherModule.ts"); 335 const logicConfig = getTsBuildProjectFile("logic", "tsconfig.json"); 336 const logicIndex = getTsBuildProjectFile("logic", "index.ts"); 337 const testsConfig = getTsBuildProjectFile("tests", "tsconfig.json"); 338 const testsIndex = getTsBuildProjectFile("tests", "index.ts"); 339 const baseline: string[] = []; 340 let oldSnap: ReturnType<TestFSWithWatch.TestServerHost["snap"]> | undefined; 341 const system = TestFSWithWatch.changeToHostTrackingWrittenFiles( 342 fakes.patchHostForBuildInfoReadWrite( 343 tscWatch.createWatchedSystem([ 344 coreConfig, coreIndex, coreDecl, coreAnotherModule, 345 logicConfig, logicIndex, 346 testsConfig, testsIndex, 347 tscWatch.libFile 348 ]) 349 ) 350 ); 351 352 const host = createSolutionBuilderHostForBaseline(system); 353 const builder = createSolutionBuilder(host, [testsConfig.path], { dry: false, force: false, verbose: false }); 354 builder.build(); 355 baselineState("Build of project"); 356 357 // Update a timestamp in the middle project 358 system.appendFile(logicIndex.path, "function foo() {}"); 359 360 // Because we haven't reset the build context, the builder should assume there's nothing to do right now 361 const status = builder.getUpToDateStatusOfProject(logicConfig.path); 362 baseline.push(`Project should still be upto date: ${UpToDateStatusType[status.type]}`); 363 verifyInvalidation("non Dts change to logic"); 364 365 // Rebuild this project 366 system.appendFile(logicIndex.path, `export class cNew {}`); 367 verifyInvalidation("Dts change to Logic"); 368 Harness.Baseline.runBaseline(`tsbuild/sample1/invalidates-projects-correctly.js`, baseline.join("\r\n")); 369 370 function verifyInvalidation(heading: string) { 371 // Rebuild this project 372 builder.invalidateProject(logicConfig.path as ResolvedConfigFilePath); 373 builder.getNextInvalidatedProject()?.done(); 374 baselineState(`${heading}:: After rebuilding logicConfig`); 375 376 // Build downstream projects should update 'tests', but not 'core' 377 builder.getNextInvalidatedProject()?.done(); 378 baselineState(`${heading}:: After building next project`); 379 } 380 381 function baselineState(heading: string) { 382 baseline.push(heading); 383 system.serializeOutput(baseline); 384 system.diff(baseline, oldSnap); 385 system.writtenFiles.clear(); 386 oldSnap = system.snap(); 387 } 388 }); 389 }); 390 391 const coreChanges: TestTscEdit[] = [ 392 { 393 subScenario: "incremental-declaration-changes", 394 modifyFs: fs => appendText(fs, "/src/core/index.ts", ` 395export class someClass { }`), 396 }, 397 { 398 subScenario: "incremental-declaration-doesnt-change", 399 modifyFs: fs => appendText(fs, "/src/core/index.ts", ` 400class someClass2 { }`), 401 }, 402 noChangeRun, 403 ]; 404 405 describe("lists files", () => { 406 verifyTscWithEdits({ 407 scenario: "sample1", 408 subScenario: "listFiles", 409 fs: () => projFs, 410 commandLineArgs: ["--b", "/src/tests", "--listFiles"], 411 edits: coreChanges 412 }); 413 verifyTscWithEdits({ 414 scenario: "sample1", 415 subScenario: "listEmittedFiles", 416 fs: () => projFs, 417 commandLineArgs: ["--b", "/src/tests", "--listEmittedFiles"], 418 edits: coreChanges 419 }); 420 verifyTscWithEdits({ 421 scenario: "sample1", 422 subScenario: "explainFiles", 423 fs: () => projFs, 424 commandLineArgs: ["--b", "/src/tests", "--explainFiles", "--v"], 425 edits: coreChanges 426 }); 427 }); 428 429 describe("emit output", () => { 430 verifyTscWithEdits({ 431 subScenario: "sample", 432 fs: () => projFs, 433 scenario: "sample1", 434 commandLineArgs: ["--b", "/src/tests", "--verbose"], 435 baselineSourceMap: true, 436 baselineReadFileCalls: true, 437 edits: [ 438 ...coreChanges, 439 { 440 subScenario: "when logic config changes declaration dir", 441 modifyFs: fs => replaceText(fs, "/src/logic/tsconfig.json", `"declaration": true,`, `"declaration": true, 442 "declarationDir": "decls",`), 443 }, 444 noChangeRun, 445 ], 446 }); 447 448 verifyTsc({ 449 scenario: "sample1", 450 subScenario: "when logic specifies tsBuildInfoFile", 451 fs: () => projFs, 452 modifyFs: fs => replaceText(fs, "/src/logic/tsconfig.json", `"composite": true,`, `"composite": true, 453 "tsBuildInfoFile": "ownFile.tsbuildinfo",`), 454 commandLineArgs: ["--b", "/src/tests", "--verbose"], 455 baselineSourceMap: true, 456 baselineReadFileCalls: true 457 }); 458 459 verifyTscWithEdits({ 460 subScenario: "when declaration option changes", 461 fs: () => projFs, 462 scenario: "sample1", 463 commandLineArgs: ["--b", "/src/core", "--verbose"], 464 modifyFs: fs => fs.writeFileSync("/src/core/tsconfig.json", `{ 465 "compilerOptions": { 466 "incremental": true, 467 "skipDefaultLibCheck": true 468 } 469}`), 470 edits: [{ 471 subScenario: "incremental-declaration-changes", 472 modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"incremental": true,`, `"incremental": true, "declaration": true,`), 473 }], 474 }); 475 476 verifyTscWithEdits({ 477 subScenario: "when target option changes", 478 fs: () => projFs, 479 scenario: "sample1", 480 commandLineArgs: ["--b", "/src/core", "--verbose"], 481 modifyFs: fs => { 482 fs.writeFileSync("/lib/lib.esnext.full.d.ts", `/// <reference no-default-lib="true"/> 483/// <reference lib="esnext" />`); 484 fs.writeFileSync("/lib/lib.esnext.d.ts", libContent); 485 fs.writeFileSync("/lib/lib.d.ts", `/// <reference no-default-lib="true"/> 486/// <reference lib="esnext" />`); 487 fs.writeFileSync("/src/core/tsconfig.json", `{ 488 "compilerOptions": { 489 "incremental": true, 490"listFiles": true, 491"listEmittedFiles": true, 492 "target": "esnext", 493 } 494}`); 495 }, 496 edits: [{ 497 subScenario: "incremental-declaration-changes", 498 modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", "esnext", "es5"), 499 }], 500 }); 501 502 verifyTscWithEdits({ 503 subScenario: "when module option changes", 504 fs: () => projFs, 505 scenario: "sample1", 506 commandLineArgs: ["--b", "/src/core", "--verbose"], 507 modifyFs: fs => fs.writeFileSync("/src/core/tsconfig.json", `{ 508 "compilerOptions": { 509 "incremental": true, 510 "module": "commonjs" 511 } 512}`), 513 edits: [{ 514 subScenario: "incremental-declaration-changes", 515 modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"module": "commonjs"`, `"module": "amd"`), 516 }], 517 }); 518 519 verifyTscWithEdits({ 520 subScenario: "when esModuleInterop option changes", 521 fs: () => projFs, 522 scenario: "sample1", 523 commandLineArgs: ["--b", "/src/tests", "--verbose"], 524 modifyFs: fs => fs.writeFileSync("/src/tests/tsconfig.json", `{ 525 "references": [ 526 { "path": "../core" }, 527 { "path": "../logic" } 528 ], 529 "files": ["index.ts"], 530 "compilerOptions": { 531 "composite": true, 532 "declaration": true, 533 "forceConsistentCasingInFileNames": true, 534 "skipDefaultLibCheck": true, 535 "esModuleInterop": false 536 } 537}`), 538 edits: [{ 539 subScenario: "incremental-declaration-changes", 540 modifyFs: fs => replaceText(fs, "/src/tests/tsconfig.json", `"esModuleInterop": false`, `"esModuleInterop": true`), 541 }], 542 }); 543 544 verifyTsc({ 545 scenario: "sample1", 546 subScenario: "reports error if input file is missing", 547 fs: () => projFs, 548 commandLineArgs: ["--b", "/src/tests", "--v"], 549 modifyFs: fs => { 550 fs.writeFileSync("/src/core/tsconfig.json", JSON.stringify({ 551 compilerOptions: { composite: true }, 552 files: ["anotherModule.ts", "index.ts", "some_decl.d.ts"] 553 })); 554 fs.unlinkSync("/src/core/anotherModule.ts"); 555 } 556 }); 557 558 verifyTsc({ 559 scenario: "sample1", 560 subScenario: "reports error if input file is missing with force", 561 fs: () => projFs, 562 commandLineArgs: ["--b", "/src/tests", "--v", "--f"], 563 modifyFs: fs => { 564 fs.writeFileSync("/src/core/tsconfig.json", JSON.stringify({ 565 compilerOptions: { composite: true }, 566 files: ["anotherModule.ts", "index.ts", "some_decl.d.ts"] 567 })); 568 fs.unlinkSync("/src/core/anotherModule.ts"); 569 } 570 }); 571 }); 572 }); 573} 574