• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts {
2    describe("unittests:: tsbuild:: on 'sample1' project", () => {
3        let projFs: vfs.FileSystem;
4        const testsOutputs = ["/src/tests/index.js", "/src/tests/index.d.ts", "/src/tests/tsconfig.tsbuildinfo"];
5        const logicOutputs = ["/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts", "/src/logic/tsconfig.tsbuildinfo"];
6        const coreOutputs = ["/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map", "/src/core/tsconfig.tsbuildinfo"];
7        const allExpectedOutputs = [...testsOutputs, ...logicOutputs, ...coreOutputs];
8
9        before(() => {
10            projFs = loadProjectFromDisk("tests/projects/sample1");
11        });
12
13        after(() => {
14            projFs = undefined!; // Release the contents
15        });
16
17        function getSampleFsAfterBuild() {
18            const fs = projFs.shadow();
19            const host = fakes.SolutionBuilderHost.create(fs);
20            const builder = createSolutionBuilder(host, ["/src/tests"], {});
21            builder.build();
22            fs.makeReadonly();
23            return fs;
24        }
25
26        describe("sanity check of clean build of 'sample1' project", () => {
27            verifyTsc({
28                scenario: "sample1",
29                subScenario: "builds correctly when outDir is specified",
30                fs: () => projFs,
31                commandLineArgs: ["--b", "/src/tests"],
32                modifyFs: fs => fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({
33                    compilerOptions: { composite: true, declaration: true, sourceMap: true, outDir: "outDir" },
34                    references: [{ path: "../core" }]
35                })),
36            });
37
38            verifyTsc({
39                scenario: "sample1",
40                subScenario: "builds correctly when declarationDir is specified",
41                fs: () => projFs,
42                commandLineArgs: ["--b", "/src/tests"],
43                modifyFs: fs => fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({
44                    compilerOptions: { composite: true, declaration: true, sourceMap: true, declarationDir: "out/decls" },
45                    references: [{ path: "../core" }]
46                })),
47            });
48
49            verifyTsc({
50                scenario: "sample1",
51                subScenario: "builds correctly when project is not composite or doesnt have any references",
52                fs: () => projFs,
53                commandLineArgs: ["--b", "/src/core", "--verbose"],
54                modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"composite": true,`, ""),
55            });
56        });
57
58        describe("dry builds", () => {
59            verifyTsc({
60                scenario: "sample1",
61                subScenario: "does not write any files in a dry build",
62                fs: () => projFs,
63                commandLineArgs: ["--b", "/src/tests", "--dry"],
64            });
65        });
66
67        describe("clean builds", () => {
68            verifyTscSerializedIncrementalEdits({
69                scenario: "sample1",
70                subScenario: "removes all files it built",
71                fs: getSampleFsAfterBuild,
72                commandLineArgs: ["--b", "/src/tests", "--clean"],
73                incrementalScenarios: noChangeOnlyRuns
74            });
75
76            it("cleans till project specified", () => {
77                const fs = projFs.shadow();
78                const host = fakes.SolutionBuilderHost.create(fs);
79                const builder = createSolutionBuilder(host, ["/src/tests"], {});
80                builder.build();
81                const result = builder.clean("/src/logic");
82                host.assertDiagnosticMessages(/*empty*/);
83                verifyOutputsPresent(fs, testsOutputs);
84                verifyOutputsAbsent(fs, [...logicOutputs, ...coreOutputs]);
85                assert.equal(result, ExitStatus.Success);
86            });
87
88            it("cleaning project in not build order doesnt throw error", () => {
89                const fs = projFs.shadow();
90                const host = fakes.SolutionBuilderHost.create(fs);
91                const builder = createSolutionBuilder(host, ["/src/tests"], {});
92                builder.build();
93                const result = builder.clean("/src/logic2");
94                host.assertDiagnosticMessages(/*empty*/);
95                verifyOutputsPresent(fs, allExpectedOutputs);
96                assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped);
97            });
98        });
99
100        describe("force builds", () => {
101            verifyTscSerializedIncrementalEdits({
102                scenario: "sample1",
103                subScenario: "always builds under with force option",
104                fs: () => projFs,
105                commandLineArgs: ["--b", "/src/tests", "--force"],
106                incrementalScenarios: noChangeOnlyRuns
107            });
108        });
109
110        describe("can detect when and what to rebuild", () => {
111            function initializeWithBuild(opts?: BuildOptions) {
112                const { fs, tick } = getFsWithTime(projFs);
113                const host = fakes.SolutionBuilderHost.create(fs);
114                let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true });
115                builder.build();
116                host.clearDiagnostics();
117                tick();
118                builder = createSolutionBuilder(host, ["/src/tests"], { ...(opts || {}), verbose: true });
119                return { fs, host, builder };
120            }
121
122            verifyTscIncrementalEdits({
123                scenario: "sample1",
124                subScenario: "can detect when and what to rebuild",
125                fs: getSampleFsAfterBuild,
126                commandLineArgs: ["--b", "/src/tests", "--verbose"],
127                incrementalScenarios: [
128                    // Update a file in the leaf node (tests), only it should rebuild the last one
129                    {
130                        subScenario: "Only builds the leaf node project",
131                        buildKind: BuildKind.IncrementalDtsUnchanged,
132                        modifyFs: fs => fs.writeFileSync("/src/tests/index.ts", "const m = 10;"),
133                    },
134                    // Update a file in the parent (without affecting types), should get fast downstream builds
135                    {
136                        subScenario: "Detects type-only changes in upstream projects",
137                        buildKind: BuildKind.IncrementalDtsChange,
138                        modifyFs: fs => replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"),
139                    },
140                    {
141                        subScenario: "indicates that it would skip builds during a dry build",
142                        buildKind: BuildKind.IncrementalDtsUnchanged,
143                        modifyFs: noop,
144                        commandLineArgs: ["--b", "/src/tests", "--dry"],
145                    },
146                    {
147                        subScenario: "rebuilds from start if force option is set",
148                        buildKind: BuildKind.IncrementalDtsChange,
149                        modifyFs: noop,
150                        commandLineArgs: ["--b", "/src/tests", "--verbose", "--force"],
151                    },
152                    {
153                        subScenario: "rebuilds when tsconfig changes",
154                        buildKind: BuildKind.IncrementalDtsChange,
155                        modifyFs: fs => replaceText(fs, "/src/tests/tsconfig.json", `"composite": true`, `"composite": true, "target": "es3"`),
156                    },
157                ]
158            });
159
160            it("rebuilds completely when version in tsbuildinfo doesnt match ts version", () => {
161                const { host, builder } = initializeWithBuild();
162                changeCompilerVersion(host);
163                builder.build();
164                host.assertDiagnosticMessages(
165                    getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"),
166                    [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/core/tsconfig.json", fakes.version, version],
167                    [Diagnostics.Building_project_0, "/src/core/tsconfig.json"],
168                    [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/logic/tsconfig.json", fakes.version, version],
169                    [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"],
170                    [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/tests/tsconfig.json", fakes.version, version],
171                    [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"],
172                );
173            });
174
175            it("does not rebuild if there is no program and bundle in the ts build info event if version doesnt match ts version", () => {
176                const { fs, tick } = getFsWithTime(projFs);
177                const host = fakes.SolutionBuilderHost.create(fs, /*options*/ undefined, /*setParentNodes*/ undefined, createAbstractBuilder);
178                let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true });
179                builder.build();
180                host.assertDiagnosticMessages(
181                    getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"),
182                    [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"],
183                    [Diagnostics.Building_project_0, "/src/core/tsconfig.json"],
184                    [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"],
185                    [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"],
186                    [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/tests/tsconfig.json", "src/tests/index.js"],
187                    [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"]
188                );
189                verifyOutputsPresent(fs, allExpectedOutputs);
190
191                host.clearDiagnostics();
192                tick();
193                builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true });
194                changeCompilerVersion(host);
195                builder.build();
196                host.assertDiagnosticMessages(
197                    getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"),
198                    [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/core/tsconfig.json", "src/core/anotherModule.ts", "src/core/anotherModule.js"],
199                    [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/logic/tsconfig.json", "src/logic/index.ts", "src/logic/index.js"],
200                    [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/tests/tsconfig.json", "src/tests/index.ts", "src/tests/index.js"]
201                );
202            });
203
204            verifyTscSerializedIncrementalEdits({
205                scenario: "sample1",
206                subScenario: "rebuilds when extended config file changes",
207                fs: () => projFs,
208                commandLineArgs: ["--b", "/src/tests", "--verbose"],
209                modifyFs: fs => {
210                    fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: { target: "es3" } }));
211                    replaceText(fs, "/src/tests/tsconfig.json", `"references": [`, `"extends": "./tsconfig.base.json", "references": [`);
212                },
213                incrementalScenarios: [{
214                    buildKind: BuildKind.IncrementalDtsChange,
215                    modifyFs: fs => fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: {} }))
216                }]
217            });
218
219            it("builds till project specified", () => {
220                const fs = projFs.shadow();
221                const host = fakes.SolutionBuilderHost.create(fs);
222                const builder = createSolutionBuilder(host, ["/src/tests"], {});
223                const result = builder.build("/src/logic");
224                host.assertDiagnosticMessages(/*empty*/);
225                verifyOutputsAbsent(fs, testsOutputs);
226                verifyOutputsPresent(fs, [...logicOutputs, ...coreOutputs]);
227                assert.equal(result, ExitStatus.Success);
228            });
229
230            it("building project in not build order doesnt throw error", () => {
231                const fs = projFs.shadow();
232                const host = fakes.SolutionBuilderHost.create(fs);
233                const builder = createSolutionBuilder(host, ["/src/tests"], {});
234                const result = builder.build("/src/logic2");
235                host.assertDiagnosticMessages(/*empty*/);
236                verifyOutputsAbsent(fs, allExpectedOutputs);
237                assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped);
238            });
239
240            it("building using getNextInvalidatedProject", () => {
241                interface SolutionBuilderResult<T> {
242                    project: ResolvedConfigFileName;
243                    result: T;
244                }
245
246                const fs = projFs.shadow();
247                const host = fakes.SolutionBuilderHost.create(fs);
248                const builder = createSolutionBuilder(host, ["/src/tests"], {});
249                verifyBuildNextResult({
250                    project: "/src/core/tsconfig.json" as ResolvedConfigFileName,
251                    result: ExitStatus.Success
252                }, coreOutputs, [...logicOutputs, ...testsOutputs]);
253
254                verifyBuildNextResult({
255                    project: "/src/logic/tsconfig.json" as ResolvedConfigFileName,
256                    result: ExitStatus.Success
257                }, [...coreOutputs, ...logicOutputs], testsOutputs);
258
259                verifyBuildNextResult({
260                    project: "/src/tests/tsconfig.json" as ResolvedConfigFileName,
261                    result: ExitStatus.Success
262                }, allExpectedOutputs, emptyArray);
263
264                verifyBuildNextResult(/*expected*/ undefined, allExpectedOutputs, emptyArray);
265
266                function verifyBuildNextResult(
267                    expected: SolutionBuilderResult<ExitStatus> | undefined,
268                    presentOutputs: readonly string[],
269                    absentOutputs: readonly string[]
270                ) {
271                    const project = builder.getNextInvalidatedProject();
272                    const result = project && project.done();
273                    assert.deepEqual(project && { project: project.project, result }, expected);
274                    verifyOutputsPresent(fs, presentOutputs);
275                    verifyOutputsAbsent(fs, absentOutputs);
276                }
277            });
278
279            it("building using buildReferencedProject", () => {
280                const fs = projFs.shadow();
281                const host = fakes.SolutionBuilderHost.create(fs);
282                const builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true });
283                builder.buildReferences("/src/tests");
284                host.assertDiagnosticMessages(
285                    getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json"),
286                    [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"],
287                    [Diagnostics.Building_project_0, "/src/core/tsconfig.json"],
288                    [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"],
289                    [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"],
290                );
291                verifyOutputsPresent(fs, [...coreOutputs, ...logicOutputs]);
292                verifyOutputsAbsent(fs, testsOutputs);
293            });
294        });
295
296        describe("downstream-blocked compilations", () => {
297            verifyTsc({
298                scenario: "sample1",
299                subScenario: "does not build downstream projects if upstream projects have errors",
300                fs: () => projFs,
301                commandLineArgs: ["--b", "/src/tests", "--verbose"],
302                modifyFs: fs => replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`)
303            });
304        });
305
306        describe("project invalidation", () => {
307            it("invalidates projects correctly", () => {
308                const { fs, time, tick } = getFsWithTime(projFs);
309                const host = fakes.SolutionBuilderHost.create(fs);
310                const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false });
311
312                builder.build();
313                host.assertDiagnosticMessages(/*empty*/);
314
315                // Update a timestamp in the middle project
316                tick();
317                appendText(fs, "/src/logic/index.ts", "function foo() {}");
318                const originalWriteFile = fs.writeFileSync;
319                const writtenFiles = new Map<string, true>();
320                fs.writeFileSync = (path, data, encoding) => {
321                    writtenFiles.set(path, true);
322                    originalWriteFile.call(fs, path, data, encoding);
323                };
324                // Because we haven't reset the build context, the builder should assume there's nothing to do right now
325                const status = builder.getUpToDateStatusOfProject("/src/logic");
326                assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date");
327                verifyInvalidation(/*expectedToWriteTests*/ false);
328
329                // Rebuild this project
330                fs.writeFileSync("/src/logic/index.ts", `${fs.readFileSync("/src/logic/index.ts")}
331export class cNew {}`);
332                verifyInvalidation(/*expectedToWriteTests*/ true);
333
334                function verifyInvalidation(expectedToWriteTests: boolean) {
335                    // Rebuild this project
336                    tick();
337                    builder.invalidateProject("/src/logic/tsconfig.json" as ResolvedConfigFilePath);
338                    builder.buildNextInvalidatedProject();
339                    // The file should be updated
340                    assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt");
341                    assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt");
342                    assert.isFalse(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should *not* have been rebuilt");
343                    assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt");
344                    writtenFiles.clear();
345
346                    // Build downstream projects should update 'tests', but not 'core'
347                    tick();
348                    builder.buildNextInvalidatedProject();
349                    if (expectedToWriteTests) {
350                        assert.isTrue(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should have been rebuilt");
351                    }
352                    else {
353                        assert.equal(writtenFiles.size, 0, "Should not write any new files");
354                    }
355                    assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have new timestamp");
356                    assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt");
357                }
358            });
359        });
360
361        const coreChanges: TscIncremental[] = [
362            {
363                buildKind: BuildKind.IncrementalDtsChange,
364                modifyFs: fs => appendText(fs, "/src/core/index.ts", `
365export class someClass { }`),
366            },
367            {
368                buildKind: BuildKind.IncrementalDtsUnchanged,
369                modifyFs: fs => appendText(fs, "/src/core/index.ts", `
370class someClass2 { }`),
371            }
372        ];
373
374        describe("lists files", () => {
375            verifyTscSerializedIncrementalEdits({
376                scenario: "sample1",
377                subScenario: "listFiles",
378                fs: () => projFs,
379                commandLineArgs: ["--b", "/src/tests", "--listFiles"],
380                incrementalScenarios: coreChanges
381            });
382            verifyTscSerializedIncrementalEdits({
383                scenario: "sample1",
384                subScenario: "listEmittedFiles",
385                fs: () => projFs,
386                commandLineArgs: ["--b", "/src/tests", "--listEmittedFiles"],
387                incrementalScenarios: coreChanges
388            });
389            verifyTscSerializedIncrementalEdits({
390                scenario: "sample1",
391                subScenario: "explainFiles",
392                fs: () => projFs,
393                commandLineArgs: ["--b", "/src/tests", "--explainFiles", "--v"],
394                incrementalScenarios: coreChanges
395            });
396        });
397
398        describe("emit output", () => {
399            verifyTscSerializedIncrementalEdits({
400                subScenario: "sample",
401                fs: () => projFs,
402                scenario: "sample1",
403                commandLineArgs: ["--b", "/src/tests", "--verbose"],
404                baselineSourceMap: true,
405                baselineReadFileCalls: true,
406                incrementalScenarios: [
407                    ...coreChanges,
408                    {
409                        subScenario: "when logic config changes declaration dir",
410                        buildKind: BuildKind.IncrementalDtsChange,
411                        modifyFs: fs => replaceText(fs, "/src/logic/tsconfig.json", `"declaration": true,`, `"declaration": true,
412        "declarationDir": "decls",`),
413                    },
414                    noChangeRun,
415                ],
416            });
417
418            verifyTsc({
419                scenario: "sample1",
420                subScenario: "when logic specifies tsBuildInfoFile",
421                fs: () => projFs,
422                modifyFs: fs => replaceText(fs, "/src/logic/tsconfig.json", `"composite": true,`, `"composite": true,
423        "tsBuildInfoFile": "ownFile.tsbuildinfo",`),
424                commandLineArgs: ["--b", "/src/tests", "--verbose"],
425                baselineSourceMap: true,
426                baselineReadFileCalls: true
427            });
428
429            verifyTscSerializedIncrementalEdits({
430                subScenario: "when declaration option changes",
431                fs: () => projFs,
432                scenario: "sample1",
433                commandLineArgs: ["--b", "/src/core", "--verbose"],
434                modifyFs: fs => fs.writeFileSync("/src/core/tsconfig.json", `{
435    "compilerOptions": {
436        "incremental": true,
437        "skipDefaultLibCheck": true
438    }
439}`),
440                incrementalScenarios: [{
441                    buildKind: BuildKind.IncrementalDtsChange,
442                    modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"incremental": true,`, `"incremental": true, "declaration": true,`),
443                }],
444            });
445
446            verifyTscSerializedIncrementalEdits({
447                subScenario: "when target option changes",
448                fs: () => projFs,
449                scenario: "sample1",
450                commandLineArgs: ["--b", "/src/core", "--verbose"],
451                modifyFs: fs => {
452                    fs.writeFileSync("/lib/lib.esnext.full.d.ts", `/// <reference no-default-lib="true"/>
453/// <reference lib="esnext" />`);
454                    fs.writeFileSync("/lib/lib.esnext.d.ts", libContent);
455                    fs.writeFileSync("/lib/lib.d.ts", `/// <reference no-default-lib="true"/>
456/// <reference lib="esnext" />`);
457                    fs.writeFileSync("/src/core/tsconfig.json", `{
458    "compilerOptions": {
459        "incremental": true,
460"listFiles": true,
461"listEmittedFiles": true,
462        "target": "esnext",
463    }
464}`);
465                },
466                incrementalScenarios: [{
467                    buildKind: BuildKind.IncrementalDtsChange,
468                    modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", "esnext", "es5"),
469                }],
470            });
471
472            verifyTscSerializedIncrementalEdits({
473                subScenario: "when module option changes",
474                fs: () => projFs,
475                scenario: "sample1",
476                commandLineArgs: ["--b", "/src/core", "--verbose"],
477                modifyFs: fs => fs.writeFileSync("/src/core/tsconfig.json", `{
478    "compilerOptions": {
479        "incremental": true,
480        "module": "commonjs"
481    }
482}`),
483                incrementalScenarios: [{
484                    buildKind: BuildKind.IncrementalDtsChange,
485                    modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"module": "commonjs"`, `"module": "amd"`),
486                }],
487            });
488
489            verifyTscSerializedIncrementalEdits({
490                subScenario: "when esModuleInterop option changes",
491                fs: () => projFs,
492                scenario: "sample1",
493                commandLineArgs: ["--b", "/src/tests", "--verbose"],
494                modifyFs: fs => fs.writeFileSync("/src/tests/tsconfig.json", `{
495    "references": [
496        { "path": "../core" },
497        { "path": "../logic" }
498    ],
499    "files": ["index.ts"],
500    "compilerOptions": {
501        "composite": true,
502        "declaration": true,
503        "forceConsistentCasingInFileNames": true,
504        "skipDefaultLibCheck": true,
505        "esModuleInterop": false
506    }
507}`),
508                incrementalScenarios: [{
509                    buildKind: BuildKind.IncrementalDtsChange,
510                    modifyFs: fs => replaceText(fs, "/src/tests/tsconfig.json", `"esModuleInterop": false`, `"esModuleInterop": true`),
511                }],
512            });
513        });
514    });
515}
516