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