1namespace ts.tscWatch { 2 export const projects = `/user/username/projects`; 3 export const projectRoot = `${projects}/myproject`; 4 export import WatchedSystem = TestFSWithWatch.TestServerHost; 5 export type File = TestFSWithWatch.File; 6 export type SymLink = TestFSWithWatch.SymLink; 7 export import libFile = TestFSWithWatch.libFile; 8 export import createWatchedSystem = TestFSWithWatch.createWatchedSystem; 9 export import checkArray = TestFSWithWatch.checkArray; 10 export import checkOutputContains = TestFSWithWatch.checkOutputContains; 11 export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; 12 13 export const commonFile1: File = { 14 path: "/a/b/commonFile1.ts", 15 content: "let x = 1" 16 }; 17 export const commonFile2: File = { 18 path: "/a/b/commonFile2.ts", 19 content: "let y = 1" 20 }; 21 22 export function checkProgramActualFiles(program: Program, expectedFiles: readonly string[]) { 23 checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); 24 } 25 26 export function getDiagnosticMessageChain(message: DiagnosticMessage, args?: (string | number)[], next?: DiagnosticMessageChain[]): DiagnosticMessageChain { 27 let text = getLocaleSpecificMessage(message); 28 if (args?.length) { 29 text = formatStringFromArgs(text, args); 30 } 31 return { 32 messageText: text, 33 category: message.category, 34 code: message.code, 35 next 36 }; 37 } 38 39 function isDiagnosticMessageChain(message: DiagnosticMessage | DiagnosticMessageChain): message is DiagnosticMessageChain { 40 return !!(message as DiagnosticMessageChain).messageText; 41 } 42 43 export function getDiagnosticOfFileFrom(file: SourceFile | undefined, start: number | undefined, length: number | undefined, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { 44 return { 45 file, 46 start, 47 length, 48 49 messageText: isDiagnosticMessageChain(message) ? 50 message : 51 getDiagnosticMessageChain(message, args).messageText, 52 category: message.category, 53 code: message.code, 54 }; 55 } 56 57 export function getDiagnosticWithoutFile(message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { 58 return getDiagnosticOfFileFrom(/*file*/ undefined, /*start*/ undefined, /*length*/ undefined, message, ...args); 59 } 60 61 export function getDiagnosticOfFile(file: SourceFile, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { 62 return getDiagnosticOfFileFrom(file, start, length, message, ...args); 63 } 64 65 export function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { 66 return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase())), 67 start, length, message, ...args); 68 } 69 70 export function getUnknownCompilerOption(program: Program, configFile: File, option: string) { 71 const quotedOption = `"${option}"`; 72 return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); 73 } 74 75 export function getUnknownDidYouMeanCompilerOption(program: Program, configFile: File, option: string, didYouMean: string) { 76 const quotedOption = `"${option}"`; 77 return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, option, didYouMean); 78 } 79 80 export function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) { 81 const quotedModuleName = `"${moduleName}"`; 82 return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option, moduleName); 83 } 84 85 export function runQueuedTimeoutCallbacks(sys: WatchedSystem) { 86 sys.runQueuedTimeoutCallbacks(); 87 } 88 89 export function checkSingleTimeoutQueueLengthAndRun(sys: WatchedSystem) { 90 sys.checkTimeoutQueueLengthAndRun(1); 91 } 92 93 export function checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout(sys: WatchedSystem) { 94 sys.checkTimeoutQueueLengthAndRun(1); 95 sys.checkTimeoutQueueLength(0); 96 } 97 98 export type WatchOrSolution<T extends BuilderProgram> = void | SolutionBuilder<T> | WatchOfConfigFile<T> | WatchOfFilesAndCompilerOptions<T>; 99 export interface TscWatchCompileChange<T extends BuilderProgram = EmitAndSemanticDiagnosticsBuilderProgram> { 100 caption: string; 101 change: (sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles) => void; 102 timeouts: ( 103 sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles, 104 programs: readonly CommandLineProgram[], 105 watchOrSolution: WatchOrSolution<T> 106 ) => void; 107 } 108 export interface TscWatchCheckOptions { 109 baselineSourceMap?: boolean; 110 baselineDependencies?: boolean; 111 } 112 export interface TscWatchCompileBase<T extends BuilderProgram = EmitAndSemanticDiagnosticsBuilderProgram> extends TscWatchCheckOptions { 113 scenario: string; 114 subScenario: string; 115 commandLineArgs: readonly string[]; 116 changes: readonly TscWatchCompileChange<T>[]; 117 } 118 export interface TscWatchCompile extends TscWatchCompileBase { 119 sys: () => WatchedSystem; 120 } 121 122 export const noopChange: TscWatchCompileChange = { 123 caption: "No change", 124 change: noop, 125 timeouts: sys => sys.checkTimeoutQueueLength(0), 126 }; 127 128 export type SystemSnap = ReturnType<WatchedSystem["snap"]>; 129 function tscWatchCompile(input: TscWatchCompile) { 130 it("tsc-watch:: Generates files matching the baseline", () => { 131 const { sys, baseline, oldSnap } = createBaseline(input.sys()); 132 const { 133 scenario, subScenario, 134 commandLineArgs, changes, 135 baselineSourceMap, baselineDependencies 136 } = input; 137 138 if (!isWatch(commandLineArgs)) sys.exit = exitCode => sys.exitCode = exitCode; 139 const { cb, getPrograms } = commandLineCallbacks(sys); 140 const watchOrSolution = executeCommandLine( 141 sys, 142 cb, 143 commandLineArgs, 144 ); 145 runWatchBaseline({ 146 scenario, 147 subScenario, 148 commandLineArgs, 149 sys, 150 baseline, 151 oldSnap, 152 getPrograms, 153 baselineSourceMap, 154 baselineDependencies, 155 changes, 156 watchOrSolution 157 }); 158 }); 159 } 160 161 export interface BaselineBase { 162 baseline: string[]; 163 sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles; 164 oldSnap: SystemSnap; 165 } 166 167 export interface Baseline extends BaselineBase, CommandLineCallbacks { 168 } 169 170 export function createBaseline(system: WatchedSystem, modifySystem?: (sys: WatchedSystem, originalRead: WatchedSystem["readFile"]) => void): Baseline { 171 const originalRead = system.readFile; 172 const initialSys = fakes.patchHostForBuildInfoReadWrite(system); 173 modifySystem?.(initialSys, originalRead); 174 const sys = TestFSWithWatch.changeToHostTrackingWrittenFiles(initialSys); 175 const baseline: string[] = []; 176 baseline.push("Input::"); 177 sys.diff(baseline); 178 const { cb, getPrograms } = commandLineCallbacks(sys); 179 return { sys, baseline, oldSnap: sys.snap(), cb, getPrograms }; 180 } 181 182 export function createSolutionBuilderWithWatchHostForBaseline(sys: WatchedSystem, cb: ExecuteCommandLineCallbacks) { 183 const host = createSolutionBuilderWithWatchHost(sys, 184 /*createProgram*/ undefined, 185 createDiagnosticReporter(sys, /*pretty*/ true), 186 createBuilderStatusReporter(sys, /*pretty*/ true), 187 createWatchStatusReporter(sys, /*pretty*/ true) 188 ); 189 host.afterProgramEmitAndDiagnostics = cb; 190 host.afterEmitBundle = cb; 191 return host; 192 } 193 194 interface CreateWatchCompilerHostOfConfigFileForBaseline<T extends BuilderProgram> extends CreateWatchCompilerHostOfConfigFileInput<T> { 195 system: WatchedSystem, 196 cb: ExecuteCommandLineCallbacks; 197 } 198 199 export function createWatchCompilerHostOfConfigFileForBaseline<T extends BuilderProgram = EmitAndSemanticDiagnosticsBuilderProgram>( 200 input: CreateWatchCompilerHostOfConfigFileForBaseline<T> 201 ) { 202 const host = createWatchCompilerHostOfConfigFile({ 203 ...input, 204 reportDiagnostic: createDiagnosticReporter(input.system, /*pretty*/ true), 205 reportWatchStatus: createWatchStatusReporter(input.system, /*pretty*/ true), 206 }); 207 updateWatchHostForBaseline(host, input.cb); 208 return host; 209 } 210 211 interface CreateWatchCompilerHostOfFilesAndCompilerOptionsForBaseline<T extends BuilderProgram> extends CreateWatchCompilerHostOfFilesAndCompilerOptionsInput<T> { 212 system: WatchedSystem, 213 cb: ExecuteCommandLineCallbacks; 214 } 215 export function createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline<T extends BuilderProgram = EmitAndSemanticDiagnosticsBuilderProgram>( 216 input: CreateWatchCompilerHostOfFilesAndCompilerOptionsForBaseline<T> 217 ) { 218 const host = createWatchCompilerHostOfFilesAndCompilerOptions({ 219 ...input, 220 reportDiagnostic: createDiagnosticReporter(input.system, /*pretty*/ true), 221 reportWatchStatus: createWatchStatusReporter(input.system, /*pretty*/ true), 222 }); 223 updateWatchHostForBaseline(host, input.cb); 224 return host; 225 } 226 227 function updateWatchHostForBaseline<T extends BuilderProgram>(host: WatchCompilerHost<T>, cb: ExecuteCommandLineCallbacks) { 228 const emitFilesAndReportErrors = host.afterProgramCreate!; 229 host.afterProgramCreate = builderProgram => { 230 emitFilesAndReportErrors.call(host, builderProgram); 231 cb(builderProgram); 232 }; 233 return host; 234 } 235 236 export function applyChange(sys: BaselineBase["sys"], baseline: BaselineBase["baseline"], change: TscWatchCompileChange["change"], caption?: TscWatchCompileChange["caption"]) { 237 const oldSnap = sys.snap(); 238 baseline.push(`Change::${caption ? " " + caption : ""}`, ""); 239 change(sys); 240 baseline.push("Input::"); 241 sys.diff(baseline, oldSnap); 242 return sys.snap(); 243 } 244 245 export interface RunWatchBaseline<T extends BuilderProgram> extends BaselineBase, TscWatchCompileBase<T> { 246 sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles; 247 getPrograms: () => readonly CommandLineProgram[]; 248 watchOrSolution: WatchOrSolution<T>; 249 } 250 export function runWatchBaseline<T extends BuilderProgram = EmitAndSemanticDiagnosticsBuilderProgram>({ 251 scenario, subScenario, commandLineArgs, 252 getPrograms, sys, baseline, oldSnap, 253 baselineSourceMap, baselineDependencies, 254 changes, watchOrSolution 255 }: RunWatchBaseline<T>) { 256 baseline.push(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}`); 257 let programs = watchBaseline({ 258 baseline, 259 getPrograms, 260 oldPrograms: emptyArray, 261 sys, 262 oldSnap, 263 baselineSourceMap, 264 baselineDependencies, 265 }); 266 267 for (const { caption, change, timeouts } of changes) { 268 oldSnap = applyChange(sys, baseline, change, caption); 269 timeouts(sys, programs, watchOrSolution); 270 programs = watchBaseline({ 271 baseline, 272 getPrograms, 273 oldPrograms: programs, 274 sys, 275 oldSnap, 276 baselineSourceMap, 277 baselineDependencies, 278 }); 279 } 280 Harness.Baseline.runBaseline(`${isBuild(commandLineArgs) ? "tsbuild" : "tsc"}${isWatch(commandLineArgs) ? "Watch" : ""}/${scenario}/${subScenario.split(" ").join("-")}.js`, baseline.join("\r\n")); 281 } 282 283 function isWatch(commandLineArgs: readonly string[]) { 284 return forEach(commandLineArgs, arg => { 285 if (arg.charCodeAt(0) !== CharacterCodes.minus) return false; 286 const option = arg.slice(arg.charCodeAt(1) === CharacterCodes.minus ? 2 : 1).toLowerCase(); 287 return option === "watch" || option === "w"; 288 }); 289 } 290 291 export interface WatchBaseline extends BaselineBase, TscWatchCheckOptions { 292 oldPrograms: readonly (CommandLineProgram | undefined)[]; 293 getPrograms: () => readonly CommandLineProgram[]; 294 } 295 export function watchBaseline({ baseline, getPrograms, oldPrograms, sys, oldSnap, baselineSourceMap, baselineDependencies }: WatchBaseline) { 296 if (baselineSourceMap) generateSourceMapBaselineFiles(sys); 297 sys.serializeOutput(baseline); 298 const programs = baselinePrograms(baseline, getPrograms, oldPrograms, baselineDependencies); 299 sys.serializeWatches(baseline); 300 baseline.push(`exitCode:: ExitStatus.${ExitStatus[sys.exitCode as ExitStatus]}`, ""); 301 sys.diff(baseline, oldSnap); 302 sys.writtenFiles.forEach((value, key) => { 303 assert.equal(value, 1, `Expected to write file ${key} only once`); 304 }); 305 sys.writtenFiles.clear(); 306 return programs; 307 } 308 309 export function baselinePrograms(baseline: string[], getPrograms: () => readonly CommandLineProgram[], oldPrograms: readonly (CommandLineProgram | undefined)[], baselineDependencies: boolean | undefined) { 310 const programs = getPrograms(); 311 for (let i = 0; i < programs.length; i++) { 312 baselineProgram(baseline, programs[i], oldPrograms[i], baselineDependencies); 313 } 314 return programs; 315 } 316 317 function baselineProgram(baseline: string[], [program, builderProgram]: CommandLineProgram, oldProgram: CommandLineProgram | undefined, baselineDependencies: boolean | undefined) { 318 if (program !== oldProgram?.[0]) { 319 const options = program.getCompilerOptions(); 320 baseline.push(`Program root files: ${JSON.stringify(program.getRootFileNames())}`); 321 baseline.push(`Program options: ${JSON.stringify(options)}`); 322 baseline.push(`Program structureReused: ${(ts as any).StructureIsReused[program.structureIsReused]}`); 323 baseline.push("Program files::"); 324 for (const file of program.getSourceFiles()) { 325 baseline.push(file.fileName); 326 } 327 } 328 else { 329 baseline.push(`Program: Same as old program`); 330 } 331 baseline.push(""); 332 333 if (!builderProgram) return; 334 if (builderProgram !== oldProgram?.[1]) { 335 const state = builderProgram.getState(); 336 const internalState = state as unknown as BuilderProgramState; 337 if (state.semanticDiagnosticsPerFile?.size) { 338 baseline.push("Semantic diagnostics in builder refreshed for::"); 339 for (const file of program.getSourceFiles()) { 340 if (!internalState.semanticDiagnosticsFromOldState || !internalState.semanticDiagnosticsFromOldState.has(file.resolvedPath)) { 341 baseline.push(file.fileName); 342 } 343 } 344 } 345 else { 346 baseline.push("No cached semantic diagnostics in the builder::"); 347 } 348 if (internalState) { 349 baseline.push(""); 350 if (internalState.hasCalledUpdateShapeSignature?.size) { 351 baseline.push("Shape signatures in builder refreshed for::"); 352 internalState.hasCalledUpdateShapeSignature.forEach((path: Path) => { 353 const info = state.fileInfos.get(path); 354 if (info?.version === info?.signature || !info?.signature) { 355 baseline.push(path + " (used version)"); 356 } 357 else if (internalState.filesChangingSignature?.has(path)) { 358 baseline.push(path + " (computed .d.ts during emit)"); 359 } 360 else { 361 baseline.push(path + " (computed .d.ts)"); 362 } 363 }); 364 } 365 else { 366 baseline.push("No shapes updated in the builder::"); 367 } 368 } 369 baseline.push(""); 370 if (!baselineDependencies) return; 371 baseline.push("Dependencies for::"); 372 for (const file of builderProgram.getSourceFiles()) { 373 baseline.push(`${file.fileName}:`); 374 for (const depenedency of builderProgram.getAllDependencies(file)) { 375 baseline.push(` ${depenedency}`); 376 } 377 } 378 } 379 else { 380 baseline.push(`BuilderProgram: Same as old builder program`); 381 } 382 baseline.push(""); 383 } 384 385 export interface VerifyTscWatch extends TscWatchCompile { 386 baselineIncremental?: boolean; 387 } 388 export function verifyTscWatch(input: VerifyTscWatch) { 389 describe(input.scenario, () => { 390 describe(input.subScenario, () => { 391 tscWatchCompile(input); 392 }); 393 if (input.baselineIncremental) { 394 describe(`${input.subScenario} with incremental`, () => { 395 tscWatchCompile({ 396 ...input, 397 subScenario: `${input.subScenario} with incremental`, 398 commandLineArgs: [...input.commandLineArgs, "--incremental"], 399 }); 400 }); 401 } 402 }); 403 } 404 405 export function replaceFileText(sys: WatchedSystem, file: string, searchValue: string | RegExp, replaceValue: string) { 406 const content = Debug.checkDefined(sys.readFile(file)); 407 sys.writeFile(file, content.replace(searchValue, replaceValue)); 408 } 409 410 export function createSolutionBuilder(system: WatchedSystem, rootNames: readonly string[], originalRead?: WatchedSystem["readFile"]) { 411 const host = createSolutionBuilderHostForBaseline(system, /*versionToWrite*/ undefined, originalRead); 412 return ts.createSolutionBuilder(host, rootNames, {}); 413 } 414 415 export function ensureErrorFreeBuild(host: WatchedSystem, rootNames: readonly string[]) { 416 // ts build should succeed 417 solutionBuildWithBaseline(host, rootNames); 418 assert.equal(host.getOutput().length, 0, JSON.stringify(host.getOutput(), /*replacer*/ undefined, " ")); 419 } 420 421 export function solutionBuildWithBaseline(sys: WatchedSystem, solutionRoots: readonly string[], originalRead?: WatchedSystem["readFile"]) { 422 const originalReadFile = sys.readFile; 423 const originalWrite = sys.write; 424 const originalWriteFile = sys.writeFile; 425 const solutionBuilder = createSolutionBuilder(TestFSWithWatch.changeToHostTrackingWrittenFiles( 426 fakes.patchHostForBuildInfoReadWrite(sys) 427 ), solutionRoots, originalRead); 428 solutionBuilder.build(); 429 sys.readFile = originalReadFile; 430 sys.write = originalWrite; 431 sys.writeFile = originalWriteFile; 432 return sys; 433 } 434 435 export function createSystemWithSolutionBuild(solutionRoots: readonly string[], files: TestFSWithWatch.FileOrFolderOrSymLinkMap | readonly TestFSWithWatch.FileOrFolderOrSymLink[], params?: TestFSWithWatch.TestServerHostCreationParameters) { 436 return solutionBuildWithBaseline(createWatchedSystem(files, params), solutionRoots); 437 } 438} 439