1namespace ts { 2 describe("unittests:: tsc:: incremental::", () => { 3 verifyTscWithEdits({ 4 scenario: "incremental", 5 subScenario: "when passing filename for buildinfo on commandline", 6 fs: () => loadProjectFromFiles({ 7 "/src/project/src/main.ts": "export const x = 10;", 8 "/src/project/tsconfig.json": Utils.dedent` 9 { 10 "compilerOptions": { 11 "target": "es5", 12 "module": "commonjs", 13 }, 14 "include": [ 15 "src/**/*.ts" 16 ] 17 }`, 18 }), 19 commandLineArgs: ["--incremental", "--p", "src/project", "--tsBuildInfoFile", "src/project/.tsbuildinfo", "--explainFiles"], 20 edits: noChangeOnlyRuns 21 }); 22 23 verifyTscWithEdits({ 24 scenario: "incremental", 25 subScenario: "when passing rootDir from commandline", 26 fs: () => loadProjectFromFiles({ 27 "/src/project/src/main.ts": "export const x = 10;", 28 "/src/project/tsconfig.json": Utils.dedent` 29 { 30 "compilerOptions": { 31 "incremental": true, 32 "outDir": "dist", 33 }, 34 }`, 35 }), 36 commandLineArgs: ["--p", "src/project", "--rootDir", "src/project/src"], 37 edits: noChangeOnlyRuns 38 }); 39 40 verifyTscWithEdits({ 41 scenario: "incremental", 42 subScenario: "with only dts files", 43 fs: () => loadProjectFromFiles({ 44 "/src/project/src/main.d.ts": "export const x = 10;", 45 "/src/project/src/another.d.ts": "export const y = 10;", 46 "/src/project/tsconfig.json": "{}", 47 }), 48 commandLineArgs: ["--incremental", "--p", "src/project"], 49 edits: [ 50 noChangeRun, 51 { 52 subScenario: "incremental-declaration-doesnt-change", 53 modifyFs: fs => appendText(fs, "/src/project/src/main.d.ts", "export const xy = 100;") 54 } 55 ] 56 }); 57 58 verifyTscWithEdits({ 59 scenario: "incremental", 60 subScenario: "when passing rootDir is in the tsconfig", 61 fs: () => loadProjectFromFiles({ 62 "/src/project/src/main.ts": "export const x = 10;", 63 "/src/project/tsconfig.json": Utils.dedent` 64 { 65 "compilerOptions": { 66 "incremental": true, 67 "outDir": "./built", 68 "rootDir": "./" 69 }, 70 }`, 71 }), 72 commandLineArgs: ["--p", "src/project"], 73 edits: noChangeOnlyRuns 74 }); 75 76 verifyTscWithEdits({ 77 scenario: "incremental", 78 subScenario: "tsbuildinfo has error", 79 fs: () => loadProjectFromFiles({ 80 "/src/project/main.ts": "export const x = 10;", 81 "/src/project/tsconfig.json": "{}", 82 "/src/project/tsconfig.tsbuildinfo": "Some random string", 83 }), 84 commandLineArgs: ["--p", "src/project", "-i"], 85 edits: [{ 86 subScenario: "tsbuildinfo written has error", 87 modifyFs: fs => prependText(fs, "/src/project/tsconfig.tsbuildinfo", "Some random string"), 88 }] 89 }); 90 91 describe("with noEmitOnError", () => { 92 let projFs: vfs.FileSystem; 93 before(() => { 94 projFs = loadProjectFromDisk("tests/projects/noEmitOnError"); 95 }); 96 after(() => { 97 projFs = undefined!; 98 }); 99 100 function verifyNoEmitOnError(subScenario: string, fixModifyFs: TestTscEdit["modifyFs"], modifyFs?: TestTscEdit["modifyFs"]) { 101 verifyTscWithEdits({ 102 scenario: "incremental", 103 subScenario, 104 fs: () => projFs, 105 commandLineArgs: ["--incremental", "-p", "src"], 106 modifyFs, 107 edits: [ 108 noChangeWithExportsDiscrepancyRun, 109 { 110 subScenario: "incremental-declaration-doesnt-change", 111 modifyFs: fixModifyFs 112 }, 113 noChangeRun, 114 ], 115 baselinePrograms: true 116 }); 117 } 118 verifyNoEmitOnError( 119 "with noEmitOnError syntax errors", 120 fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; 121const a = { 122 lastName: 'sdsd' 123};`, "utf-8") 124 ); 125 126 verifyNoEmitOnError( 127 "with noEmitOnError semantic errors", 128 fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; 129const a: string = "hello";`, "utf-8"), 130 fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; 131const a: string = 10;`, "utf-8"), 132 ); 133 }); 134 135 describe("when noEmit changes between compilation", () => { 136 verifyNoEmitChanges({ incremental: true }); 137 verifyNoEmitChanges({ incremental: true, declaration: true }); 138 verifyNoEmitChanges({ composite: true }); 139 140 function verifyNoEmitChanges(compilerOptions: CompilerOptions) { 141 const discrepancyExplanation = () => [ 142 ...noChangeWithExportsDiscrepancyRun.discrepancyExplanation!(), 143 "Clean build will not have latestChangedDtsFile as there was no emit and emitSignatures as undefined for files", 144 "Incremental will store the past latestChangedDtsFile and emitSignatures", 145 ]; 146 const discrepancyIfNoDtsEmit = getEmitDeclarations(compilerOptions) ? 147 undefined : 148 noChangeWithExportsDiscrepancyRun.discrepancyExplanation; 149 const noChangeRunWithNoEmit: TestTscEdit = { 150 ...noChangeRun, 151 subScenario: "No Change run with noEmit", 152 commandLineArgs: ["--p", "src/project", "--noEmit"], 153 discrepancyExplanation: compilerOptions.composite ? 154 discrepancyExplanation : 155 !compilerOptions.declaration ? 156 noChangeWithExportsDiscrepancyRun.discrepancyExplanation : 157 undefined, 158 }; 159 const noChangeRunWithEmit: TestTscEdit = { 160 ...noChangeRun, 161 subScenario: "No Change run with emit", 162 commandLineArgs: ["--p", "src/project"], 163 discrepancyExplanation: discrepancyIfNoDtsEmit, 164 }; 165 let optionsString = ""; 166 for (const key in compilerOptions) { 167 if (hasProperty(compilerOptions, key)) { 168 optionsString += ` ${key}`; 169 } 170 } 171 172 verifyTscWithEdits({ 173 scenario: "incremental", 174 subScenario: `noEmit changes${optionsString}`, 175 commandLineArgs: ["--p", "src/project"], 176 fs, 177 edits: [ 178 noChangeRunWithNoEmit, 179 noChangeRunWithNoEmit, 180 { 181 subScenario: "Introduce error but still noEmit", 182 commandLineArgs: ["--p", "src/project", "--noEmit"], 183 modifyFs: fs => replaceText(fs, "/src/project/src/class.ts", "prop", "prop1"), 184 discrepancyExplanation: compilerOptions.composite ? 185 discrepancyExplanation : 186 compilerOptions.declaration ? 187 noChangeWithExportsDiscrepancyRun.discrepancyExplanation : 188 undefined, 189 }, 190 { 191 subScenario: "Fix error and emit", 192 modifyFs: fs => replaceText(fs, "/src/project/src/class.ts", "prop1", "prop"), 193 discrepancyExplanation: discrepancyIfNoDtsEmit, 194 }, 195 noChangeRunWithEmit, 196 noChangeRunWithNoEmit, 197 noChangeRunWithNoEmit, 198 noChangeRunWithEmit, 199 { 200 subScenario: "Introduce error and emit", 201 modifyFs: fs => replaceText(fs, "/src/project/src/class.ts", "prop", "prop1"), 202 discrepancyExplanation: discrepancyIfNoDtsEmit, 203 }, 204 noChangeRunWithEmit, 205 noChangeRunWithNoEmit, 206 noChangeRunWithNoEmit, 207 noChangeRunWithEmit, 208 { 209 subScenario: "Fix error and no emit", 210 commandLineArgs: ["--p", "src/project", "--noEmit"], 211 modifyFs: fs => replaceText(fs, "/src/project/src/class.ts", "prop1", "prop"), 212 discrepancyExplanation: compilerOptions.composite ? 213 discrepancyExplanation : 214 noChangeWithExportsDiscrepancyRun.discrepancyExplanation, 215 }, 216 noChangeRunWithEmit, 217 noChangeRunWithNoEmit, 218 noChangeRunWithNoEmit, 219 noChangeRunWithEmit, 220 ], 221 }); 222 223 verifyTscWithEdits({ 224 scenario: "incremental", 225 subScenario: `noEmit changes with initial noEmit${optionsString}`, 226 commandLineArgs: ["--p", "src/project", "--noEmit"], 227 fs, 228 edits: [ 229 noChangeRunWithEmit, 230 { 231 subScenario: "Introduce error with emit", 232 commandLineArgs: ["--p", "src/project"], 233 modifyFs: fs => replaceText(fs, "/src/project/src/class.ts", "prop", "prop1"), 234 }, 235 { 236 subScenario: "Fix error and no emit", 237 modifyFs: fs => replaceText(fs, "/src/project/src/class.ts", "prop1", "prop"), 238 discrepancyExplanation: compilerOptions.composite ? 239 discrepancyExplanation : 240 noChangeWithExportsDiscrepancyRun.discrepancyExplanation, 241 }, 242 noChangeRunWithEmit, 243 ], 244 }); 245 246 function fs() { 247 return loadProjectFromFiles({ 248 "/src/project/src/class.ts": Utils.dedent` 249 export class classC { 250 prop = 1; 251 }`, 252 "/src/project/src/indirectClass.ts": Utils.dedent` 253 import { classC } from './class'; 254 export class indirectClass { 255 classC = new classC(); 256 }`, 257 "/src/project/src/directUse.ts": Utils.dedent` 258 import { indirectClass } from './indirectClass'; 259 new indirectClass().classC.prop;`, 260 "/src/project/src/indirectUse.ts": Utils.dedent` 261 import { indirectClass } from './indirectClass'; 262 new indirectClass().classC.prop;`, 263 "/src/project/src/noChangeFile.ts": Utils.dedent` 264 export function writeLog(s: string) { 265 }`, 266 "/src/project/src/noChangeFileWithEmitSpecificError.ts": Utils.dedent` 267 function someFunc(arguments: boolean, ...rest: any[]) { 268 }`, 269 "/src/project/tsconfig.json": JSON.stringify({ compilerOptions }), 270 }); 271 } 272 } 273 }); 274 275 verifyTscWithEdits({ 276 scenario: "incremental", 277 subScenario: `when global file is added, the signatures are updated`, 278 fs: () => loadProjectFromFiles({ 279 "/src/project/src/main.ts": Utils.dedent` 280 /// <reference path="./filePresent.ts"/> 281 /// <reference path="./fileNotFound.ts"/> 282 function main() { } 283 `, 284 "/src/project/src/anotherFileWithSameReferenes.ts": Utils.dedent` 285 /// <reference path="./filePresent.ts"/> 286 /// <reference path="./fileNotFound.ts"/> 287 function anotherFileWithSameReferenes() { } 288 `, 289 "/src/project/src/filePresent.ts": `function something() { return 10; }`, 290 "/src/project/tsconfig.json": JSON.stringify({ 291 compilerOptions: { composite: true, }, 292 include: ["src/**/*.ts"] 293 }), 294 }), 295 commandLineArgs: ["--p", "src/project"], 296 edits: [ 297 noChangeRun, 298 { 299 subScenario: "Modify main file", 300 modifyFs: fs => appendText(fs, `/src/project/src/main.ts`, `something();`), 301 }, 302 { 303 subScenario: "Modify main file again", 304 modifyFs: fs => appendText(fs, `/src/project/src/main.ts`, `something();`), 305 }, 306 { 307 subScenario: "Add new file and update main file", 308 modifyFs: fs => { 309 fs.writeFileSync(`/src/project/src/newFile.ts`, "function foo() { return 20; }"); 310 prependText(fs, `/src/project/src/main.ts`, `/// <reference path="./newFile.ts"/> 311`); 312 appendText(fs, `/src/project/src/main.ts`, `foo();`); 313 }, 314 }, 315 { 316 subScenario: "Write file that could not be resolved", 317 modifyFs: fs => fs.writeFileSync(`/src/project/src/fileNotFound.ts`, "function something2() { return 20; }"), 318 }, 319 { 320 subScenario: "Modify main file", 321 modifyFs: fs => appendText(fs, `/src/project/src/main.ts`, `something();`), 322 }, 323 ], 324 baselinePrograms: true, 325 }); 326 327 describe("when synthesized imports are added to files", () => { 328 function getJsxLibraryContent() { 329 return ` 330export {}; 331declare global { 332 namespace JSX { 333 interface Element {} 334 interface IntrinsicElements { 335 div: { 336 propA?: boolean; 337 }; 338 } 339 } 340}`; 341 } 342 343 verifyTsc({ 344 scenario: "react-jsx-emit-mode", 345 subScenario: "with no backing types found doesn't crash", 346 fs: () => loadProjectFromFiles({ 347 "/src/project/node_modules/react/jsx-runtime.js": "export {}", // js needs to be present so there's a resolution result 348 "/src/project/node_modules/@types/react/index.d.ts": getJsxLibraryContent(), // doesn't contain a jsx-runtime definition 349 "/src/project/src/index.tsx": `export const App = () => <div propA={true}></div>;`, 350 "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { module: "commonjs", jsx: "react-jsx", incremental: true, jsxImportSource: "react" } }) 351 }), 352 commandLineArgs: ["--p", "src/project"] 353 }); 354 355 verifyTsc({ 356 scenario: "react-jsx-emit-mode", 357 subScenario: "with no backing types found doesn't crash under --strict", 358 fs: () => loadProjectFromFiles({ 359 "/src/project/node_modules/react/jsx-runtime.js": "export {}", // js needs to be present so there's a resolution result 360 "/src/project/node_modules/@types/react/index.d.ts": getJsxLibraryContent(), // doesn't contain a jsx-runtime definition 361 "/src/project/src/index.tsx": `export const App = () => <div propA={true}></div>;`, 362 "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { module: "commonjs", jsx: "react-jsx", incremental: true, jsxImportSource: "react" } }) 363 }), 364 commandLineArgs: ["--p", "src/project", "--strict"] 365 }); 366 }); 367 368 verifyTscWithEdits({ 369 scenario: "incremental", 370 subScenario: "when new file is added to the referenced project", 371 commandLineArgs: ["-i", "-p", `src/projects/project2`], 372 fs: () => loadProjectFromFiles({ 373 "/src/projects/project1/tsconfig.json": JSON.stringify({ 374 compilerOptions: { 375 module: "none", 376 composite: true 377 }, 378 exclude: ["temp"] 379 }), 380 "/src/projects/project1/class1.ts": `class class1 {}`, 381 "/src/projects/project1/class1.d.ts": `declare class class1 {}`, 382 "/src/projects/project2/tsconfig.json": JSON.stringify({ 383 compilerOptions: { 384 module: "none", 385 composite: true 386 }, 387 references: [ 388 { path: "../project1" } 389 ] 390 }), 391 "/src/projects/project2/class2.ts": `class class2 {}`, 392 }), 393 edits: [ 394 { 395 subScenario: "Add class3 to project1 and build it", 396 modifyFs: fs => fs.writeFileSync("/src/projects/project1/class3.ts", `class class3 {}`, "utf-8"), 397 discrepancyExplanation: () => [ 398 "Ts buildinfo will not be updated in incremental build so it will have semantic diagnostics cached from previous build", 399 "But in clean build because of global diagnostics, semantic diagnostics are not queried so not cached in tsbuildinfo", 400 ], 401 }, 402 { 403 subScenario: "Add output of class3", 404 modifyFs: fs => fs.writeFileSync("/src/projects/project1/class3.d.ts", `declare class class3 {}`, "utf-8"), 405 }, 406 { 407 subScenario: "Add excluded file to project1", 408 modifyFs: fs => { 409 fs.mkdirSync("/src/projects/project1/temp"); 410 fs.writeFileSync("/src/projects/project1/temp/file.d.ts", `declare class file {}`, "utf-8"); 411 }, 412 }, 413 { 414 subScenario: "Delete output for class3", 415 modifyFs: fs => fs.unlinkSync("/src/projects/project1/class3.d.ts"), 416 discrepancyExplanation: () => [ 417 "Ts buildinfo will be updated but will retain lib file errors from previous build and not others because they are emitted because of change which results in clearing their semantic diagnostics cache", 418 "But in clean build because of global diagnostics, semantic diagnostics are not queried so not cached in tsbuildinfo", 419 ], 420 }, 421 { 422 subScenario: "Create output for class3", 423 modifyFs: fs => fs.writeFileSync("/src/projects/project1/class3.d.ts", `declare class class3 {}`, "utf-8"), 424 }, 425 ] 426 }); 427 428 429 verifyTscWithEdits({ 430 scenario: "incremental", 431 subScenario: "when project has strict true", 432 commandLineArgs: ["-noEmit", "-p", `src/project`], 433 fs: () => loadProjectFromFiles({ 434 "/src/project/tsconfig.json": JSON.stringify({ 435 compilerOptions: { 436 incremental: true, 437 strict: true, 438 }, 439 }), 440 "/src/project/class1.ts": `export class class1 {}`, 441 }), 442 edits: noChangeOnlyRuns, 443 baselinePrograms: true 444 }); 445 446 verifyTscWithEdits({ 447 scenario: "incremental", 448 subScenario: "serializing error chains", 449 commandLineArgs: ["-p", `src/project`], 450 fs: () => loadProjectFromFiles({ 451 "/src/project/tsconfig.json": JSON.stringify({ 452 compilerOptions: { 453 incremental: true, 454 strict: true, 455 jsx: "react", 456 module: "esnext", 457 }, 458 }), 459 "/src/project/index.tsx": Utils.dedent` 460 declare namespace JSX { 461 interface ElementChildrenAttribute { children: {}; } 462 interface IntrinsicElements { div: {} } 463 } 464 465 declare var React: any; 466 467 declare function Component(props: never): any; 468 declare function Component(props: { children?: number }): any; 469 (<Component> 470 <div /> 471 <div /> 472 </Component>)` 473 }, `\ninterface ReadonlyArray<T> { readonly length: number }`), 474 edits: noChangeOnlyRuns, 475 }); 476 477 verifyTsc({ 478 scenario: "incremental", 479 subScenario: "ts file with no-default-lib that augments the global scope", 480 fs: () => loadProjectFromFiles({ 481 "/src/project/src/main.ts": Utils.dedent` 482 /// <reference no-default-lib="true"/> 483 /// <reference lib="esnext" /> 484 485 declare global { 486 interface Test { 487 } 488 } 489 490 export {}; 491 `, 492 "/src/project/tsconfig.json": Utils.dedent` 493 { 494 "compilerOptions": { 495 "target": "ESNext", 496 "module": "ESNext", 497 "incremental": true, 498 "outDir": "dist", 499 }, 500 }`, 501 }), 502 commandLineArgs: ["--p", "src/project", "--rootDir", "src/project/src"], 503 modifyFs: (fs) => { 504 fs.writeFileSync("/lib/lib.esnext.d.ts", libContent); 505 } 506 }); 507 508 verifyTscWithEdits({ 509 scenario: "incremental", 510 subScenario: "change to type that gets used as global through export in another file", 511 commandLineArgs: ["-p", `src/project`], 512 fs: () => loadProjectFromFiles({ 513 "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, }), 514 "/src/project/class1.ts": `const a: MagicNumber = 1; 515console.log(a);`, 516 "/src/project/constants.ts": "export default 1;", 517 "/src/project/types.d.ts": `type MagicNumber = typeof import('./constants').default`, 518 }), 519 edits: [{ 520 subScenario: "Modify imports used in global file", 521 modifyFs: fs => fs.writeFileSync("/src/project/constants.ts", "export default 2;"), 522 }], 523 }); 524 525 verifyTscWithEdits({ 526 scenario: "incremental", 527 subScenario: "change to type that gets used as global through export in another file through indirect import", 528 commandLineArgs: ["-p", `src/project`], 529 fs: () => loadProjectFromFiles({ 530 "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, }), 531 "/src/project/class1.ts": `const a: MagicNumber = 1; 532console.log(a);`, 533 "/src/project/constants.ts": "export default 1;", 534 "/src/project/reexport.ts": `export { default as ConstantNumber } from "./constants"`, 535 "/src/project/types.d.ts": `type MagicNumber = typeof import('./reexport').ConstantNumber`, 536 }), 537 edits: [{ 538 subScenario: "Modify imports used in global file", 539 modifyFs: fs => fs.writeFileSync("/src/project/constants.ts", "export default 2;"), 540 }], 541 }); 542 543 function verifyModifierChange(declaration: boolean) { 544 verifyTscWithEdits({ 545 scenario: "incremental", 546 subScenario: `change to modifier of class expression field${declaration ? " with declaration emit enabled" : ""}`, 547 commandLineArgs: ["-p", "src/project", "--incremental"], 548 fs: () => loadProjectFromFiles({ 549 "/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { declaration } }), 550 "/src/project/main.ts": Utils.dedent` 551 import MessageablePerson from './MessageablePerson.js'; 552 function logMessage( person: MessageablePerson ) { 553 console.log( person.message ); 554 }`, 555 "/src/project/MessageablePerson.ts": Utils.dedent` 556 const Messageable = () => { 557 return class MessageableClass { 558 public message = 'hello'; 559 } 560 }; 561 const wrapper = () => Messageable(); 562 type MessageablePerson = InstanceType<ReturnType<typeof wrapper>>; 563 export default MessageablePerson;`, 564 }), 565 modifyFs: fs => appendText(fs, "/lib/lib.d.ts", Utils.dedent` 566 type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any; 567 type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;` 568 ), 569 edits: [ 570 { 571 subScenario: "modify public to protected", 572 modifyFs: fs => replaceText(fs, "/src/project/MessageablePerson.ts", "public", "protected"), 573 }, 574 { 575 subScenario: "modify protected to public", 576 modifyFs: fs => replaceText(fs, "/src/project/MessageablePerson.ts", "protected", "public"), 577 }, 578 ], 579 }); 580 } 581 verifyModifierChange(/*declaration*/ false); 582 verifyModifierChange(/*declaration*/ true); 583 }); 584} 585