• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts.tscWatch {
2    describe("unittests:: tsc-watch:: emit file --incremental", () => {
3        const project = "/users/username/projects/project";
4
5        const configFile: File = {
6            path: `${project}/tsconfig.json`,
7            content: JSON.stringify({ compilerOptions: { incremental: true } })
8        };
9
10        interface VerifyIncrementalWatchEmitInput {
11            subScenario: string;
12            files: () => readonly File[];
13            optionsToExtend?: readonly string[];
14            modifyFs?: (host: WatchedSystem) => void;
15        }
16        function verifyIncrementalWatchEmit(input: VerifyIncrementalWatchEmitInput) {
17            describe(input.subScenario, () => {
18                it("with tsc --w", () => {
19                    verifyIncrementalWatchEmitWorker(input, /*incremental*/ false);
20                });
21                it("with tsc", () => {
22                    verifyIncrementalWatchEmitWorker(input, /*incremental*/ true);
23                });
24            });
25        }
26
27        function verifyIncrementalWatchEmitWorker(
28            { subScenario, files, optionsToExtend, modifyFs }: VerifyIncrementalWatchEmitInput,
29            incremental: boolean
30        ) {
31            const { sys, baseline, oldSnap, cb, getPrograms } = createBaseline(createWatchedSystem(files(), { currentDirectory: project }));
32            if (incremental) sys.exit = exitCode => sys.exitCode = exitCode;
33            const argsToPass = [incremental ? "-i" : "-w", ...(optionsToExtend || emptyArray)];
34            baseline.push(`${sys.getExecutingFilePath()} ${argsToPass.join(" ")}`);
35            let oldPrograms: readonly CommandLineProgram[] = emptyArray;
36            build(oldSnap);
37
38            if (modifyFs) {
39                const oldSnap = applyChange(sys, baseline, modifyFs);
40                build(oldSnap);
41            }
42
43            Harness.Baseline.runBaseline(`${isBuild(argsToPass) ? "tsbuild/watchMode" : "tscWatch"}/incremental/${subScenario.split(" ").join("-")}-${incremental ? "incremental" : "watch"}.js`, baseline.join("\r\n"));
44
45            function build(oldSnap: SystemSnap) {
46                const closer = executeCommandLine(
47                    sys,
48                    cb,
49                    argsToPass,
50                );
51                oldPrograms = watchBaseline({
52                    baseline,
53                    getPrograms,
54                    oldPrograms,
55                    sys,
56                    oldSnap
57                });
58                if (closer) closer.close();
59            }
60        }
61
62        describe("non module compilation", () => {
63            const file1: File = {
64                path: `${project}/file1.ts`,
65                content: "const x = 10;"
66            };
67            const file2: File = {
68                path: `${project}/file2.ts`,
69                content: "const y = 20;"
70            };
71            describe("own file emit without errors", () => {
72                function verify(subScenario: string, optionsToExtend?: readonly string[]) {
73                    const modifiedFile2Content = file2.content.replace("y", "z").replace("20", "10");
74                    verifyIncrementalWatchEmit({
75                        files: () => [libFile, file1, file2, configFile],
76                        optionsToExtend,
77                        subScenario: `own file emit without errors/${subScenario}`,
78                        modifyFs: host => host.writeFile(file2.path, modifiedFile2Content),
79                    });
80                }
81                verify("without commandline options");
82                verify("with commandline parameters that are not relative", ["-p", "tsconfig.json"]);
83            });
84
85            verifyIncrementalWatchEmit({
86                files: () => [libFile, file1, configFile, {
87                    path: file2.path,
88                    content: `const y: string = 20;`
89                }],
90                subScenario: "own file emit with errors",
91                modifyFs: host => host.writeFile(file1.path, file1.content.replace("x", "z")),
92            });
93
94            verifyIncrementalWatchEmit({
95                files: () => [libFile, file1, file2, {
96                    path: configFile.path,
97                    content: JSON.stringify({ compilerOptions: { incremental: true, outFile: "out.js" } })
98                }],
99                subScenario: "with --out",
100            });
101        });
102
103        describe("module compilation", () => {
104            const file1: File = {
105                path: `${project}/file1.ts`,
106                content: "export const x = 10;"
107            };
108            const file2: File = {
109                path: `${project}/file2.ts`,
110                content: "export const y = 20;"
111            };
112            const config: File = {
113                path: configFile.path,
114                content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd" } })
115            };
116
117            verifyIncrementalWatchEmit({
118                files: () => [libFile, file1, file2, config],
119                subScenario: "module compilation/own file emit without errors",
120                modifyFs: host => host.writeFile(file2.path, file2.content.replace("y", "z").replace("20", "10")),
121            });
122
123            describe("own file emit with errors", () => {
124                const fileModified: File = {
125                    path: file2.path,
126                    content: `export const y: string = 20;`
127                };
128
129                verifyIncrementalWatchEmit({
130                    files: () => [libFile, file1, fileModified, config],
131                    subScenario: "module compilation/own file emit with errors",
132                    modifyFs: host => host.writeFile(file1.path, file1.content.replace("x = 10", "z = 10")),
133                });
134
135                it("verify that state is read correctly", () => {
136                    const system = createWatchedSystem([libFile, file1, fileModified, config], { currentDirectory: project });
137                    const reportDiagnostic = createDiagnosticReporter(system);
138                    const parsedConfig = parseConfigFileWithSystem("tsconfig.json", {}, /*extendedConfigCache*/ undefined, /*watchOptionsToExtend*/ undefined, system, reportDiagnostic)!;
139                    performIncrementalCompilation({
140                        rootNames: parsedConfig.fileNames,
141                        options: parsedConfig.options,
142                        projectReferences: parsedConfig.projectReferences,
143                        configFileParsingDiagnostics: getConfigFileParsingDiagnostics(parsedConfig),
144                        reportDiagnostic,
145                        system
146                    });
147
148                    const command = parseConfigFileWithSystem("tsconfig.json", {}, /*extendedConfigCache*/ undefined, /*watchOptionsToExtend*/ undefined, system, noop)!;
149                    const builderProgram = createIncrementalProgram({
150                        rootNames: command.fileNames,
151                        options: command.options,
152                        projectReferences: command.projectReferences,
153                        configFileParsingDiagnostics: getConfigFileParsingDiagnostics(command),
154                        host: createIncrementalCompilerHost(command.options, system)
155                    });
156
157                    const state = builderProgram.getState();
158                    assert.equal(state.changedFilesSet!.size, 0, "changes");
159
160                    assert.equal(state.fileInfos.size, 3, "FileInfo size");
161                    assert.deepEqual(state.fileInfos.get(libFile.path as Path), {
162                        version: system.createHash(libFile.content),
163                        signature: system.createHash(libFile.content),
164                        affectsGlobalScope: true,
165                        impliedFormat: undefined,
166                    });
167                    assert.deepEqual(state.fileInfos.get(file1.path as Path), {
168                        version: system.createHash(file1.content),
169                        signature: system.createHash(file1.content),
170                        affectsGlobalScope: undefined,
171                        impliedFormat: undefined,
172                    });
173                    assert.deepEqual(state.fileInfos.get(file2.path as Path), {
174                        version: system.createHash(fileModified.content),
175                        signature: system.createHash(fileModified.content),
176                        affectsGlobalScope: undefined,
177                        impliedFormat: undefined,
178                    });
179
180                    assert.deepEqual(state.compilerOptions, {
181                        incremental: true,
182                        module: ModuleKind.AMD,
183                        configFilePath: config.path
184                    });
185
186                    assert.equal(arrayFrom(state.referencedMap!.keys()).length, 0);
187                    assert.equal(arrayFrom(state.exportedModulesMap!.keys()).length, 0);
188
189                    assert.equal(state.semanticDiagnosticsPerFile!.size, 3);
190                    assert.deepEqual(state.semanticDiagnosticsPerFile!.get(libFile.path as Path), emptyArray);
191                    assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file1.path as Path), emptyArray);
192                    assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file2.path as Path), [{
193                        file: state.program!.getSourceFileByPath(file2.path as Path)!,
194                        start: 13,
195                        length: 1,
196                        code: Diagnostics.Type_0_is_not_assignable_to_type_1.code,
197                        category: Diagnostics.Type_0_is_not_assignable_to_type_1.category,
198                        messageText: "Type 'number' is not assignable to type 'string'.",
199                        relatedInformation: undefined,
200                        reportsUnnecessary: undefined,
201                        reportsDeprecated: undefined,
202                        source: undefined,
203                        skippedOn: undefined,
204                    }]);
205                });
206            });
207
208            verifyIncrementalWatchEmit({
209                files: () => [libFile, file1, file2, {
210                    path: configFile.path,
211                    content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd", outFile: "out.js" } })
212                }],
213                subScenario: "module compilation/with --out",
214            });
215        });
216
217        verifyIncrementalWatchEmit({
218            files: () => {
219                const config: File = {
220                    path: configFile.path,
221                    content: JSON.stringify({
222                        compilerOptions: {
223                            incremental: true,
224                            target: "es5",
225                            module: "commonjs",
226                            declaration: true,
227                            emitDeclarationOnly: true
228                        }
229                    })
230                };
231                const aTs: File = {
232                    path: `${project}/a.ts`,
233                    content: `import { B } from "./b";
234export interface A {
235    b: B;
236}
237`
238                };
239                const bTs: File = {
240                    path: `${project}/b.ts`,
241                    content: `import { C } from "./c";
242export interface B {
243    b: C;
244}
245`
246                };
247                const cTs: File = {
248                    path: `${project}/c.ts`,
249                    content: `import { A } from "./a";
250export interface C {
251    a: A;
252}
253`
254                };
255                const indexTs: File = {
256                    path: `${project}/index.ts`,
257                    content: `export { A } from "./a";
258export { B } from "./b";
259export { C } from "./c";
260`
261                };
262                return [libFile, aTs, bTs, cTs, indexTs, config];
263            },
264            subScenario: "incremental with circular references",
265            modifyFs: host => host.writeFile(`${project}/a.ts`, `import { B } from "./b";
266export interface A {
267    b: B;
268    foo: any;
269}
270`)
271        });
272
273        verifyIncrementalWatchEmit({
274            subScenario: "when file with ambient global declaration file is deleted",
275            files: () => [
276                { path: libFile.path, content: libContent },
277                { path: `${project}/globals.d.ts`, content: `declare namespace Config { const value: string;} ` },
278                { path: `${project}/index.ts`, content: `console.log(Config.value);` },
279                { path: configFile.path, content: JSON.stringify({ compilerOptions: { incremental: true, } }) }
280            ],
281            modifyFs: host => host.deleteFile(`${project}/globals.d.ts`)
282        });
283
284        describe("with option jsxImportSource", () => {
285            const jsxImportSourceOptions = { module: "commonjs", jsx: "react-jsx", incremental: true, jsxImportSource: "react" };
286            const jsxLibraryContent = `export namespace JSX {
287    interface Element {}
288    interface IntrinsicElements {
289        div: {
290            propA?: boolean;
291        };
292    }
293}
294export function jsx(...args: any[]): void;
295export function jsxs(...args: any[]): void;
296export const Fragment: unique symbol;
297`;
298
299            verifyIncrementalWatchEmit({
300                subScenario: "jsxImportSource option changed",
301                files: () => [
302                    { path: libFile.path, content: libContent },
303                    { path: `${project}/node_modules/react/jsx-runtime/index.d.ts`, content: jsxLibraryContent },
304                    { path: `${project}/node_modules/react/package.json`, content: JSON.stringify({ name: "react", version: "0.0.1" }) },
305                    { path: `${project}/node_modules/preact/jsx-runtime/index.d.ts`, content: jsxLibraryContent.replace("propA", "propB") },
306                    { path: `${project}/node_modules/preact/package.json`, content: JSON.stringify({ name: "preact", version: "0.0.1" }) },
307                    { path: `${project}/index.tsx`, content: `export const App = () => <div propA={true}></div>;` },
308                    { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) }
309                ],
310                modifyFs: host => host.writeFile(configFile.path, JSON.stringify({ compilerOptions: { ...jsxImportSourceOptions, jsxImportSource: "preact" } })),
311                optionsToExtend: ["--explainFiles"]
312            });
313
314            verifyIncrementalWatchEmit({
315                subScenario: "jsxImportSource backing types added",
316                files: () => [
317                    { path: libFile.path, content: libContent },
318                    { path: `${project}/index.tsx`, content: `export const App = () => <div propA={true}></div>;` },
319                    { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) }
320                ],
321                modifyFs: host => {
322                    host.createDirectory(`${project}/node_modules`);
323                    host.createDirectory(`${project}/node_modules/react`);
324                    host.createDirectory(`${project}/node_modules/react/jsx-runtime`);
325                    host.writeFile(`${project}/node_modules/react/jsx-runtime/index.d.ts`, jsxLibraryContent);
326                    host.writeFile(`${project}/node_modules/react/package.json`, JSON.stringify({ name: "react", version: "0.0.1" }));
327                }
328            });
329
330            verifyIncrementalWatchEmit({
331                subScenario: "jsxImportSource backing types removed",
332                files: () => [
333                    { path: libFile.path, content: libContent },
334                    { path: `${project}/node_modules/react/jsx-runtime/index.d.ts`, content: jsxLibraryContent },
335                    { path: `${project}/node_modules/react/package.json`, content: JSON.stringify({ name: "react", version: "0.0.1" }) },
336                    { path: `${project}/index.tsx`, content: `export const App = () => <div propA={true}></div>;` },
337                    { path: configFile.path, content: JSON.stringify({ compilerOptions: jsxImportSourceOptions }) }
338                ],
339                modifyFs: host => {
340                    host.deleteFile(`${project}/node_modules/react/jsx-runtime/index.d.ts`);
341                    host.deleteFile(`${project}/node_modules/react/package.json`);
342                }
343            });
344
345            verifyIncrementalWatchEmit({
346                subScenario: "importHelpers backing types removed",
347                files: () => [
348                    { path: libFile.path, content: libContent },
349                    { path: `${project}/node_modules/tslib/index.d.ts`, content: "export function __assign(...args: any[]): any;" },
350                    { path: `${project}/node_modules/tslib/package.json`, content: JSON.stringify({ name: "tslib", version: "0.0.1" }) },
351                    { path: `${project}/index.tsx`, content: `export const x = {...{}};` },
352                    { path: configFile.path, content: JSON.stringify({ compilerOptions: { importHelpers: true } }) }
353                ],
354                modifyFs: host => {
355                    host.deleteFile(`${project}/node_modules/tslib/index.d.ts`);
356                    host.deleteFile(`${project}/node_modules/tslib/package.json`);
357                }
358            });
359        });
360
361        describe("editing module augmentation", () => {
362            verifyIncrementalWatchEmit({
363                subScenario: "editing module augmentation",
364                files: () => [
365                    { path: libFile.path, content: libContent },
366                    { path: `${project}/node_modules/classnames/index.d.ts`, content: `export interface Result {} export default function classNames(): Result;` },
367                    { path: `${project}/src/types/classnames.d.ts`, content: `export {}; declare module "classnames" { interface Result { foo } }` },
368                    { path: `${project}/src/index.ts`, content: `import classNames from "classnames"; classNames().foo;` },
369                    { path: configFile.path, content: JSON.stringify({ compilerOptions: { module: "commonjs", incremental: true } }) },
370                ],
371                modifyFs: host => {
372                    // delete 'foo'
373                    host.writeFile(`${project}/src/types/classnames.d.ts`, `export {}; declare module "classnames" { interface Result {} }`);
374                },
375            });
376        });
377
378        verifyTscWatch({
379            scenario: "incremental",
380            subScenario: "tsbuildinfo has error",
381            sys: () => createWatchedSystem({
382                "/src/project/main.ts": "export const x = 10;",
383                "/src/project/tsconfig.json": "{}",
384                "/src/project/tsconfig.tsbuildinfo": "Some random string",
385                [libFile.path]: libFile.content,
386            }),
387            commandLineArgs: ["--p", "src/project", "-i", "-w"],
388            changes: emptyArray
389        });
390    });
391}
392