• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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