1namespace ts.tscWatch { 2 describe("unittests:: tsc-watch:: Emit times and Error updates in builder after program changes", () => { 3 const config: File = { 4 path: `${projectRoot}/tsconfig.json`, 5 content: `{}` 6 }; 7 interface VerifyEmitAndErrorUpdatesWorker extends VerifyEmitAndErrorUpdates { 8 configFile: () => File; 9 } 10 function verifyEmitAndErrorUpdatesWorker({ 11 subScenario, 12 files, 13 currentDirectory, 14 lib, 15 configFile, 16 changes, 17 baselineIncremental 18 }: VerifyEmitAndErrorUpdatesWorker) { 19 verifyTscWatch({ 20 scenario: "emitAndErrorUpdates", 21 subScenario, 22 commandLineArgs: ["--w"], 23 sys: () => createWatchedSystem( 24 [...files(), configFile(), lib?.() || libFile], 25 { currentDirectory: currentDirectory || projectRoot } 26 ), 27 changes, 28 baselineIncremental 29 }); 30 } 31 32 function changeCompilerOptions(input: VerifyEmitAndErrorUpdates, additionalOptions: CompilerOptions): File { 33 const configFile = input.configFile?.() || config; 34 const content = JSON.parse(configFile.content); 35 content.compilerOptions = { ...content.compilerOptions, ...additionalOptions }; 36 return { path: configFile.path, content: JSON.stringify(content) }; 37 } 38 39 interface VerifyEmitAndErrorUpdates { 40 subScenario: string 41 files: () => File[]; 42 currentDirectory?: string; 43 lib?: () => File; 44 changes: TscWatchCompileChange[]; 45 configFile?: () => File; 46 baselineIncremental?: boolean 47 } 48 function verifyEmitAndErrorUpdates(input: VerifyEmitAndErrorUpdates) { 49 verifyEmitAndErrorUpdatesWorker({ 50 ...input, 51 subScenario: `default/${input.subScenario}`, 52 configFile: () => input.configFile?.() || config 53 }); 54 55 verifyEmitAndErrorUpdatesWorker({ 56 ...input, 57 subScenario: `defaultAndD/${input.subScenario}`, 58 configFile: () => changeCompilerOptions(input, { declaration: true }) 59 }); 60 61 verifyEmitAndErrorUpdatesWorker({ 62 ...input, 63 subScenario: `isolatedModules/${input.subScenario}`, 64 configFile: () => changeCompilerOptions(input, { isolatedModules: true }) 65 }); 66 67 verifyEmitAndErrorUpdatesWorker({ 68 ...input, 69 subScenario: `isolatedModulesAndD/${input.subScenario}`, 70 configFile: () => changeCompilerOptions(input, { isolatedModules: true, declaration: true }) 71 }); 72 73 verifyEmitAndErrorUpdatesWorker({ 74 ...input, 75 subScenario: `assumeChangesOnlyAffectDirectDependencies/${input.subScenario}`, 76 configFile: () => changeCompilerOptions(input, { assumeChangesOnlyAffectDirectDependencies: true }) 77 }); 78 79 verifyEmitAndErrorUpdatesWorker({ 80 ...input, 81 subScenario: `assumeChangesOnlyAffectDirectDependenciesAndD/${input.subScenario}`, 82 configFile: () => changeCompilerOptions(input, { assumeChangesOnlyAffectDirectDependencies: true, declaration: true }) 83 }); 84 } 85 86 describe("deep import changes", () => { 87 const aFile: File = { 88 path: `${projectRoot}/a.ts`, 89 content: `import {B} from './b'; 90declare var console: any; 91let b = new B(); 92console.log(b.c.d);` 93 }; 94 95 function verifyDeepImportChange(subScenario: string, bFile: File, cFile: File) { 96 verifyEmitAndErrorUpdates({ 97 subScenario: `deepImportChanges/${subScenario}`, 98 files: () => [aFile, bFile, cFile], 99 changes: [ 100 { 101 caption: "Rename property d to d2 of class C", 102 change: sys => sys.writeFile(cFile.path, cFile.content.replace("d", "d2")), 103 timeouts: runQueuedTimeoutCallbacks, 104 } 105 ], 106 }); 107 } 108 109 describe("updates errors when deep import file changes", () => { 110 const bFile: File = { 111 path: `${projectRoot}/b.ts`, 112 content: `import {C} from './c'; 113export class B 114{ 115 c = new C(); 116}` 117 }; 118 const cFile: File = { 119 path: `${projectRoot}/c.ts`, 120 content: `export class C 121{ 122 d = 1; 123}` 124 }; 125 verifyDeepImportChange( 126 "updates errors when deep import file changes", 127 bFile, 128 cFile 129 ); 130 }); 131 132 describe("updates errors when deep import through declaration file changes", () => { 133 const bFile: File = { 134 path: `${projectRoot}/b.d.ts`, 135 content: `import {C} from './c'; 136export class B 137{ 138 c: C; 139}` 140 }; 141 const cFile: File = { 142 path: `${projectRoot}/c.d.ts`, 143 content: `export class C 144{ 145 d: number; 146}` 147 }; 148 verifyDeepImportChange( 149 "updates errors when deep import through declaration file changes", 150 bFile, 151 cFile 152 ); 153 }); 154 }); 155 156 describe("updates errors in file not exporting a deep multilevel import that changes", () => { 157 const aFile: File = { 158 path: `${projectRoot}/a.ts`, 159 content: `export interface Point { 160 name: string; 161 c: Coords; 162} 163export interface Coords { 164 x2: number; 165 y: number; 166}` 167 }; 168 const bFile: File = { 169 path: `${projectRoot}/b.ts`, 170 content: `import { Point } from "./a"; 171export interface PointWrapper extends Point { 172}` 173 }; 174 const cFile: File = { 175 path: `${projectRoot}/c.ts`, 176 content: `import { PointWrapper } from "./b"; 177export function getPoint(): PointWrapper { 178 return { 179 name: "test", 180 c: { 181 x: 1, 182 y: 2 183 } 184 } 185};` 186 }; 187 const dFile: File = { 188 path: `${projectRoot}/d.ts`, 189 content: `import { getPoint } from "./c"; 190getPoint().c.x;` 191 }; 192 const eFile: File = { 193 path: `${projectRoot}/e.ts`, 194 content: `import "./d";` 195 }; 196 verifyEmitAndErrorUpdates({ 197 subScenario: "file not exporting a deep multilevel import that changes", 198 files: () => [aFile, bFile, cFile, dFile, eFile], 199 changes: [ 200 { 201 caption: "Rename property x2 to x of interface Coords", 202 change: sys => sys.writeFile(aFile.path, aFile.content.replace("x2", "x")), 203 timeouts: runQueuedTimeoutCallbacks, 204 } 205 ] 206 }); 207 }); 208 209 describe("updates errors when file transitively exported file changes", () => { 210 const config: File = { 211 path: `${projectRoot}/tsconfig.json`, 212 content: JSON.stringify({ 213 files: ["app.ts"], 214 compilerOptions: { baseUrl: "." } 215 }) 216 }; 217 const app: File = { 218 path: `${projectRoot}/app.ts`, 219 content: `import { Data } from "lib2/public"; 220export class App { 221 public constructor() { 222 new Data().test(); 223 } 224}` 225 }; 226 const lib2Public: File = { 227 path: `${projectRoot}/lib2/public.ts`, 228 content: `export * from "./data";` 229 }; 230 const lib2Data: File = { 231 path: `${projectRoot}/lib2/data.ts`, 232 content: `import { ITest } from "lib1/public"; 233export class Data { 234 public test() { 235 const result: ITest = { 236 title: "title" 237 } 238 return result; 239 } 240}` 241 }; 242 const lib1Public: File = { 243 path: `${projectRoot}/lib1/public.ts`, 244 content: `export * from "./tools/public";` 245 }; 246 const lib1ToolsPublic: File = { 247 path: `${projectRoot}/lib1/tools/public.ts`, 248 content: `export * from "./tools.interface";` 249 }; 250 const lib1ToolsInterface: File = { 251 path: `${projectRoot}/lib1/tools/tools.interface.ts`, 252 content: `export interface ITest { 253 title: string; 254}` 255 }; 256 257 function verifyTransitiveExports(subScenario: string, files: readonly File[]) { 258 verifyEmitAndErrorUpdates({ 259 subScenario: `updates errors when file transitively exported file changes/${subScenario}`, 260 files: () => [lib1ToolsInterface, lib1ToolsPublic, app, lib2Public, lib1Public, ...files], 261 configFile: () => config, 262 changes: [ 263 { 264 caption: "Rename property title to title2 of interface ITest", 265 change: sys => sys.writeFile(lib1ToolsInterface.path, lib1ToolsInterface.content.replace("title", "title2")), 266 timeouts: runQueuedTimeoutCallbacks, 267 } 268 ] 269 }); 270 } 271 describe("when there are no circular import and exports", () => { 272 verifyTransitiveExports( 273 "when there are no circular import and exports", 274 [lib2Data] 275 ); 276 }); 277 278 describe("when there are circular import and exports", () => { 279 const lib2Data: File = { 280 path: `${projectRoot}/lib2/data.ts`, 281 content: `import { ITest } from "lib1/public"; import { Data2 } from "./data2"; 282export class Data { 283 public dat?: Data2; public test() { 284 const result: ITest = { 285 title: "title" 286 } 287 return result; 288 } 289}` 290 }; 291 const lib2Data2: File = { 292 path: `${projectRoot}/lib2/data2.ts`, 293 content: `import { Data } from "./data"; 294export class Data2 { 295 public dat?: Data; 296}` 297 }; 298 verifyTransitiveExports( 299 "when there are circular import and exports", 300 [lib2Data, lib2Data2] 301 ); 302 }); 303 }); 304 305 describe("with noEmitOnError", () => { 306 function change(caption: string, content: string): TscWatchCompileChange { 307 return { 308 caption, 309 change: sys => sys.writeFile(`${TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`, content), 310 // build project 311 timeouts: checkSingleTimeoutQueueLengthAndRun 312 }; 313 } 314 const noChange: TscWatchCompileChange = { 315 caption: "No change", 316 change: sys => sys.writeFile(`${TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`, sys.readFile(`${TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`)!), 317 // build project 318 timeouts: checkSingleTimeoutQueueLengthAndRun, 319 }; 320 verifyEmitAndErrorUpdates({ 321 subScenario: "with noEmitOnError", 322 currentDirectory: `${TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError`, 323 files: () => ["shared/types/db.ts", "src/main.ts", "src/other.ts"] 324 .map(f => TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", f)), 325 lib: () => ({ path: libFile.path, content: libContent }), 326 configFile: () => TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", "tsconfig.json"), 327 changes: [ 328 noChange, 329 change("Fix Syntax error", `import { A } from "../shared/types/db"; 330const a = { 331 lastName: 'sdsd' 332};`), 333 change("Semantic Error", `import { A } from "../shared/types/db"; 334const a: string = 10;`), 335 noChange, 336 change("Fix Semantic Error", `import { A } from "../shared/types/db"; 337const a: string = "hello";`), 338 noChange, 339 ], 340 baselineIncremental: true 341 }); 342 }); 343 }); 344} 345