From db3a4fe25d2e96bcf374a24a39e5d98fd017bfef Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 31 Jan 2019 15:34:23 -0800 Subject: [PATCH] Separate out the tests for tsbuild into its own folder --- src/testRunner/tsconfig.json | 10 +- src/testRunner/unittests/tsbuild.ts | 1053 ----------------- .../unittests/tsbuild/emptyFiles.ts | 41 + .../unittests/tsbuild/graphOrdering.ts | 81 ++ src/testRunner/unittests/tsbuild/helpers.ts | 62 + .../unittests/tsbuild/missingExtendedFile.ts | 17 + src/testRunner/unittests/tsbuild/outFile.ts | 379 ++++++ .../tsbuild/referencesWithRootDirInParent.ts | 20 + .../unittests/tsbuild/resolveJsonModule.ts | 61 + src/testRunner/unittests/tsbuild/sample.ts | 361 ++++++ .../unittests/tsbuild/transitiveReferences.ts | 76 ++ 11 files changed, 1107 insertions(+), 1054 deletions(-) delete mode 100644 src/testRunner/unittests/tsbuild.ts create mode 100644 src/testRunner/unittests/tsbuild/emptyFiles.ts create mode 100644 src/testRunner/unittests/tsbuild/graphOrdering.ts create mode 100644 src/testRunner/unittests/tsbuild/helpers.ts create mode 100644 src/testRunner/unittests/tsbuild/missingExtendedFile.ts create mode 100644 src/testRunner/unittests/tsbuild/outFile.ts create mode 100644 src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts create mode 100644 src/testRunner/unittests/tsbuild/resolveJsonModule.ts create mode 100644 src/testRunner/unittests/tsbuild/sample.ts create mode 100644 src/testRunner/unittests/tsbuild/transitiveReferences.ts diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 849906e24cf..c35106cebf5 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -37,6 +37,7 @@ "runner.ts", "unittests/services/extract/helpers.ts", + "unittests/tsbuild/helpers.ts", "unittests/tscWatch/helpers.ts", "unittests/tsserver/helpers.ts", @@ -59,7 +60,6 @@ "unittests/reuseProgramStructure.ts", "unittests/semver.ts", "unittests/transform.ts", - "unittests/tsbuild.ts", "unittests/tsbuildWatchMode.ts", "unittests/config/commandLineParsing.ts", "unittests/config/configurationExtension.ts", @@ -88,6 +88,14 @@ "unittests/services/preProcessFile.ts", "unittests/services/textChanges.ts", "unittests/services/transpile.ts", + "unittests/tsbuild/emptyFiles.ts", + "unittests/tsbuild/graphOrdering.ts", + "unittests/tsbuild/missingExtendedFile.ts", + "unittests/tsbuild/outFile.ts", + "unittests/tsbuild/referencesWithRootDirInParent.ts", + "unittests/tsbuild/resolveJsonModule.ts", + "unittests/tsbuild/sample.ts", + "unittests/tsbuild/transitiveReferences.ts", "unittests/tscWatch/consoleClearing.ts", "unittests/tscWatch/emit.ts", "unittests/tscWatch/emitAndErrorUpdates.ts", diff --git a/src/testRunner/unittests/tsbuild.ts b/src/testRunner/unittests/tsbuild.ts deleted file mode 100644 index 5855fa120e1..00000000000 --- a/src/testRunner/unittests/tsbuild.ts +++ /dev/null @@ -1,1053 +0,0 @@ -namespace ts { - let currentTime = 100; - - function getExpectedDiagnosticForProjectsInBuild(...projects: string[]): fakes.ExpectedDiagnostic { - return [Diagnostics.Projects_in_this_build_Colon_0, projects.map(p => "\r\n * " + p).join("")]; - } - - export namespace Sample1 { - tick(); - const projFs = loadProjectFromDisk("tests/projects/sample1"); - - const allExpectedOutputs = ["/src/tests/index.js", - "/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map", - "/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts"]; - - describe("unittests:: tsbuild - sanity check of clean build of 'sample1' project", () => { - it("can build the sample project 'sample1' without error", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); - - host.clearDiagnostics(); - builder.buildAllProjects(); - host.assertDiagnosticMessages(/*empty*/); - - // Check for outputs. Not an exhaustive list - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - }); - - it("builds correctly when outDir is specified", () => { - const fs = projFs.shadow(); - fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({ - compilerOptions: { composite: true, declaration: true, sourceMap: true, outDir: "outDir" }, - references: [{ path: "../core" }] - })); - - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], {}); - builder.buildAllProjects(); - host.assertDiagnosticMessages(/*empty*/); - const expectedOutputs = allExpectedOutputs.map(f => f.replace("/logic/", "/logic/outDir/")); - // Check for outputs. Not an exhaustive list - for (const output of expectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - }); - - it("builds correctly when declarationDir is specified", () => { - const fs = projFs.shadow(); - fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({ - compilerOptions: { composite: true, declaration: true, sourceMap: true, declarationDir: "out/decls" }, - references: [{ path: "../core" }] - })); - - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], {}); - builder.buildAllProjects(); - host.assertDiagnosticMessages(/*empty*/); - const expectedOutputs = allExpectedOutputs.map(f => f.replace("/logic/index.d.ts", "/logic/out/decls/index.d.ts")); - // Check for outputs. Not an exhaustive list - for (const output of expectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - }); - }); - - describe("unittests:: tsbuild - dry builds", () => { - it("doesn't write any files in a dry build", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { dry: true, force: false, verbose: false }); - builder.buildAllProjects(); - host.assertDiagnosticMessages( - [Diagnostics.A_non_dry_build_would_build_project_0, "/src/core/tsconfig.json"], - [Diagnostics.A_non_dry_build_would_build_project_0, "/src/logic/tsconfig.json"], - [Diagnostics.A_non_dry_build_would_build_project_0, "/src/tests/tsconfig.json"] - ); - - // Check for outputs to not be written. Not an exhaustive list - for (const output of allExpectedOutputs) { - assert(!fs.existsSync(output), `Expect file ${output} to not exist`); - } - }); - - it("indicates that it would skip builds during a dry build", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - - let builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); - builder.buildAllProjects(); - tick(); - - host.clearDiagnostics(); - builder = createSolutionBuilder(host, ["/src/tests"], { dry: true, force: false, verbose: false }); - builder.buildAllProjects(); - host.assertDiagnosticMessages( - [Diagnostics.Project_0_is_up_to_date, "/src/core/tsconfig.json"], - [Diagnostics.Project_0_is_up_to_date, "/src/logic/tsconfig.json"], - [Diagnostics.Project_0_is_up_to_date, "/src/tests/tsconfig.json"] - ); - }); - }); - - describe("unittests:: tsbuild - clean builds", () => { - it("removes all files it built", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - - const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); - builder.buildAllProjects(); - // Verify they exist - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - builder.cleanAllProjects(); - // Verify they are gone - for (const output of allExpectedOutputs) { - assert(!fs.existsSync(output), `Expect file ${output} to not exist`); - } - // Subsequent clean shouldn't throw / etc - builder.cleanAllProjects(); - }); - }); - - describe("unittests:: tsbuild - force builds", () => { - it("always builds under --force", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - - const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: true, verbose: false }); - builder.buildAllProjects(); - let currentTime = time(); - checkOutputTimestamps(currentTime); - - tick(); - Debug.assert(time() !== currentTime, "Time moves on"); - currentTime = time(); - builder.buildAllProjects(); - checkOutputTimestamps(currentTime); - - function checkOutputTimestamps(expected: number) { - // Check timestamps - for (const output of allExpectedOutputs) { - const actual = fs.statSync(output).mtimeMs; - assert(actual === expected, `File ${output} has timestamp ${actual}, expected ${expected}`); - } - } - }); - }); - - describe("unittests:: tsbuild - can detect when and what to rebuild", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: true }); - - it("Builds the project", () => { - host.clearDiagnostics(); - builder.resetBuildContext(); - builder.buildAllProjects(); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], - [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], - [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/tests/tsconfig.json", "src/tests/index.js"], - [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"] - ); - tick(); - }); - - // All three projects are up to date - it("Detects that all projects are up to date", () => { - host.clearDiagnostics(); - builder.resetBuildContext(); - builder.buildAllProjects(); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), - [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"], - [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"], - [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"] - ); - tick(); - }); - - // Update a file in the leaf node (tests), only it should rebuild the last one - it("Only builds the leaf node project", () => { - host.clearDiagnostics(); - fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); - builder.resetBuildContext(); - builder.buildAllProjects(); - - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), - [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"], - [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"], - [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, "src/tests/tsconfig.json", "src/tests/index.js", "src/tests/index.ts"], - [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"] - ); - tick(); - }); - - // Update a file in the parent (without affecting types), should get fast downstream builds - it("Detects type-only changes in upstream projects", () => { - host.clearDiagnostics(); - replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); - builder.resetBuildContext(); - builder.buildAllProjects(); - - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), - [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, "src/core/tsconfig.json", "src/core/anotherModule.js", "src/core/index.ts"], - [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], - [Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, "src/logic/tsconfig.json"], - [Diagnostics.Updating_output_timestamps_of_project_0, "/src/logic/tsconfig.json"], - [Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, "src/tests/tsconfig.json"], - [Diagnostics.Updating_output_timestamps_of_project_0, "/src/tests/tsconfig.json"] - ); - }); - }); - - describe("unittests:: tsbuild - downstream-blocked compilations", () => { - it("won't build downstream projects if upstream projects have errors", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: true }); - - // Induce an error in the middle project - replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); - builder.buildAllProjects(); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], - [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], - [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], - [Diagnostics.Property_0_does_not_exist_on_type_1, "muitply", `typeof import("/src/core/index")`], - [Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, "src/tests/tsconfig.json", "src/logic"], - [Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, "/src/tests/tsconfig.json", "/src/logic"] - ); - }); - }); - - describe("unittests:: tsbuild - project invalidation", () => { - it("invalidates projects correctly", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); - - builder.buildAllProjects(); - host.assertDiagnosticMessages(/*empty*/); - - // Update a timestamp in the middle project - tick(); - touch(fs, "/src/logic/index.ts"); - const originalWriteFile = fs.writeFileSync; - const writtenFiles = createMap(); - fs.writeFileSync = (path, data, encoding) => { - writtenFiles.set(path, true); - originalWriteFile.call(fs, path, data, encoding); - }; - // Because we haven't reset the build context, the builder should assume there's nothing to do right now - const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic")); - assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); - verifyInvalidation(/*expectedToWriteTests*/ false); - - // Rebuild this project - fs.writeFileSync("/src/logic/index.ts", `${fs.readFileSync("/src/logic/index.ts")} -export class cNew {}`); - verifyInvalidation(/*expectedToWriteTests*/ true); - - function verifyInvalidation(expectedToWriteTests: boolean) { - // Rebuild this project - tick(); - builder.invalidateProject("/src/logic"); - builder.buildInvalidatedProject(); - // The file should be updated - assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt"); - assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); - assert.isFalse(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should *not* have been rebuilt"); - assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); - writtenFiles.clear(); - - // Build downstream projects should update 'tests', but not 'core' - tick(); - builder.buildInvalidatedProject(); - if (expectedToWriteTests) { - assert.isTrue(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should have been rebuilt"); - } - else { - assert.equal(writtenFiles.size, 0, "Should not write any new files"); - } - assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have new timestamp"); - assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); - } - }); - }); - - describe("unittests:: tsbuild - with resolveJsonModule option", () => { - const projFs = loadProjectFromDisk("tests/projects/resolveJsonModuleAndComposite"); - const allExpectedOutputs = ["/src/tests/dist/src/index.js", "/src/tests/dist/src/index.d.ts", "/src/tests/dist/src/hello.json"]; - - function verifyProjectWithResolveJsonModule(configFile: string, ...expectedDiagnosticMessages: fakes.ExpectedDiagnostic[]) { - const fs = projFs.shadow(); - verifyProjectWithResolveJsonModuleWithFs(fs, configFile, allExpectedOutputs, ...expectedDiagnosticMessages); - } - - function verifyProjectWithResolveJsonModuleWithFs(fs: vfs.FileSystem, configFile: string, allExpectedOutputs: ReadonlyArray, ...expectedDiagnosticMessages: fakes.ExpectedDiagnostic[]) { - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, [configFile], { dry: false, force: false, verbose: false }); - builder.buildAllProjects(); - host.assertDiagnosticMessages(...expectedDiagnosticMessages); - if (!expectedDiagnosticMessages.length) { - // Check for outputs. Not an exhaustive list - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - } - } - - it("with resolveJsonModule and include only", () => { - verifyProjectWithResolveJsonModule("/src/tests/tsconfig_withInclude.json", [ - Diagnostics.File_0_is_not_in_project_file_list_Projects_must_list_all_files_or_use_an_include_pattern, - "/src/tests/src/hello.json" - ]); - }); - - it("with resolveJsonModule and include of *.json along with other include", () => { - verifyProjectWithResolveJsonModule("/src/tests/tsconfig_withIncludeOfJson.json"); - }); - - it("with resolveJsonModule and include of *.json along with other include and file name matches ts file", () => { - const fs = projFs.shadow(); - fs.rimrafSync("/src/tests/src/hello.json"); - fs.writeFileSync("/src/tests/src/index.json", JSON.stringify({ hello: "world" })); - fs.writeFileSync("/src/tests/src/index.ts", `import hello from "./index.json" - -export default hello.hello`); - const allExpectedOutputs = ["/src/tests/dist/src/index.js", "/src/tests/dist/src/index.d.ts", "/src/tests/dist/src/index.json"]; - verifyProjectWithResolveJsonModuleWithFs(fs, "/src/tests/tsconfig_withIncludeOfJson.json", allExpectedOutputs); - }); - - it("with resolveJsonModule and files containing json file", () => { - verifyProjectWithResolveJsonModule("/src/tests/tsconfig_withFiles.json"); - }); - - it("with resolveJsonModule and include and files", () => { - verifyProjectWithResolveJsonModule("/src/tests/tsconfig_withIncludeAndFiles.json"); - }); - }); - - describe("unittests:: tsbuild - lists files", () => { - it("listFiles", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { listFiles: true }); - builder.buildAllProjects(); - assert.deepEqual(host.traces, [ - ...getLibs(), - "/src/core/anotherModule.ts", - "/src/core/index.ts", - "/src/core/some_decl.d.ts", - ...getLibs(), - ...getCoreOutputs(), - "/src/logic/index.ts", - ...getLibs(), - ...getCoreOutputs(), - "/src/logic/index.d.ts", - "/src/tests/index.ts" - ]); - - function getCoreOutputs() { - return [ - "/src/core/index.d.ts", - "/src/core/anotherModule.d.ts" - ]; - } - }); - - it("listEmittedFiles", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { listEmittedFiles: true }); - builder.buildAllProjects(); - assert.deepEqual(host.traces, [ - "TSFILE: /src/core/anotherModule.js", - "TSFILE: /src/core/anotherModule.d.ts", - "TSFILE: /src/core/anotherModule.d.ts.map", - "TSFILE: /src/core/index.js", - "TSFILE: /src/core/index.d.ts", - "TSFILE: /src/core/index.d.ts.map", - "TSFILE: /src/logic/index.js", - "TSFILE: /src/logic/index.js.map", - "TSFILE: /src/logic/index.d.ts", - "TSFILE: /src/tests/index.js", - "TSFILE: /src/tests/index.d.ts", - ]); - }); - }); - - describe("unittests:: tsbuild - with rootDir of project reference in parentDirectory", () => { - const projFs = loadProjectFromDisk("tests/projects/projectReferenceWithRootDirInParent"); - const allExpectedOutputs = [ - "/src/dist/other/other.js", "/src/dist/other/other.d.ts", - "/src/dist/main/a.js", "/src/dist/main/a.d.ts", - "/src/dist/main/b.js", "/src/dist/main/b.d.ts" - ]; - it("verify that it builds correctly", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/src/main", "/src/src/other"], {}); - builder.buildAllProjects(); - host.assertDiagnosticMessages(/*empty*/); - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - }); - }); - - describe("unittests:: tsbuild - when project reference is referenced transitively", () => { - const projFs = loadProjectFromDisk("tests/projects/transitiveReferences"); - const allExpectedOutputs = [ - "/src/a.js", "/src/a.d.ts", - "/src/b.js", "/src/b.d.ts", - "/src/c.js" - ]; - const expectedFileTraces = [ - ...getLibs(), - "/src/a.ts", - ...getLibs(), - "/src/a.d.ts", - "/src/b.ts", - ...getLibs(), - "/src/a.d.ts", - "/src/b.d.ts", - "/src/refs/a.d.ts", - "/src/c.ts" - ]; - - function verifyBuild(modifyDiskLayout: (fs: vfs.FileSystem) => void, allExpectedOutputs: ReadonlyArray, expectedFileTraces: ReadonlyArray, ...expectedDiagnostics: fakes.ExpectedDiagnostic[]) { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - modifyDiskLayout(fs); - const builder = createSolutionBuilder(host, ["/src/tsconfig.c.json"], { listFiles: true }); - builder.buildAllProjects(); - host.assertDiagnosticMessages(...expectedDiagnostics); - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - assert.deepEqual(host.traces, expectedFileTraces); - } - - function modifyFsBTsToNonRelativeImport(fs: vfs.FileSystem, moduleResolution: "node" | "classic") { - fs.writeFileSync("/src/b.ts", `import {A} from 'a'; -export const b = new A();`); - fs.writeFileSync("/src/tsconfig.b.json", JSON.stringify({ - compilerOptions: { - composite: true, - moduleResolution - }, - files: ["b.ts"], - references: [{ path: "tsconfig.a.json" }] - })); - } - - it("verify that it builds correctly", () => { - verifyBuild(noop, allExpectedOutputs, expectedFileTraces); - }); - - it("verify that it builds correctly when the referenced project uses different module resolution", () => { - verifyBuild(fs => modifyFsBTsToNonRelativeImport(fs, "classic"), allExpectedOutputs, expectedFileTraces); - }); - - it("verify that it build reports error about module not found with node resolution with external module name", () => { - // Error in b build only a - const allExpectedOutputs = ["/src/a.js", "/src/a.d.ts"]; - const expectedFileTraces = [ - ...getLibs(), - "/src/a.ts", - ]; - verifyBuild(fs => modifyFsBTsToNonRelativeImport(fs, "node"), - allExpectedOutputs, - expectedFileTraces, - [Diagnostics.Cannot_find_module_0, "a"], - ); - }); - }); - - it("unittests:: tsbuild - when tsconfig extends the missing file", () => { - const projFs = loadProjectFromDisk("tests/projects/missingExtendedConfig"); - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tsconfig.json"], {}); - builder.buildAllProjects(); - host.assertDiagnosticMessages( - [Diagnostics.The_specified_path_does_not_exist_Colon_0, "/src/foobar.json"], - [Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, "/src/tsconfig.first.json", "[\"**/*\"]", "[]"], - [Diagnostics.The_specified_path_does_not_exist_Colon_0, "/src/foobar.json"], - [Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, "/src/tsconfig.second.json", "[\"**/*\"]", "[]"] - ); - }); - } - - export namespace OutFile { - describe("unittests:: tsbuild - outFile::", () => { - const outFileFs = loadProjectFromDisk("tests/projects/outfile-concat"); - const outputFiles: [ReadonlyArray, ReadonlyArray, ReadonlyArray] = [ - [ - "/src/first/bin/first-output.js", - "/src/first/bin/first-output.js.map", - "/src/first/bin/first-output.d.ts", - "/src/first/bin/first-output.d.ts.map", - "/src/first/bin/.tsbuildinfo" - ], - [ - "/src/2/second-output.js", - "/src/2/second-output.js.map", - "/src/2/second-output.d.ts", - "/src/2/second-output.d.ts.map", - "/src/2/.tsbuildinfo" - ], - [ - "/src/third/thirdjs/output/third-output.js", - "/src/third/thirdjs/output/third-output.js.map", - "/src/third/thirdjs/output/third-output.d.ts", - "/src/third/thirdjs/output/third-output.d.ts.map", - "/src/third/thirdjs/output/.tsbuildinfo" - ] - ]; - - function createSolutionBuilder(host: fakes.SolutionBuilderHost) { - return ts.createSolutionBuilder(host, ["/src/third"], { dry: false, force: false, verbose: true }); - } - - function verifyOutFileScenarioWorker(scenario: string, modifyFs: (fs: vfs.FileSystem) => void | ReadonlyArray, withoutBuildInfo: boolean) { - describe(`${scenario}${withoutBuildInfo ? " without build info" : ""}`, () => { - let fs: vfs.FileSystem | undefined; - const actualReadFileMap = createMap(); - let additionalSourceFiles: ReadonlyArray | void; - before(() => { - fs = outFileFs.shadow(); - additionalSourceFiles = modifyFs(fs); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host); - host.clearDiagnostics(); - const originalReadFile = host.readFile; - host.readFile = path => { - // Dont record libs - if (path.startsWith("/src/")) { - actualReadFileMap.set(path, (actualReadFileMap.get(path) || 0) + 1); - } - if (withoutBuildInfo && getBaseFileName(path) === infoFile) { - return undefined; - } - return originalReadFile.call(host, path); - }; - if (withoutBuildInfo) { - const originalWriteFile = host.writeFile; - host.writeFile = (fileName, content, writeByteOrder) => { - return getBaseFileName(fileName) !== infoFile && - originalWriteFile.call(host, fileName, content, writeByteOrder); - }; - } - builder.buildAllProjects(); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/first/tsconfig.json", "src/first/bin/first-output.js"], - [Diagnostics.Building_project_0, "/src/first/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/second/tsconfig.json", "src/2/second-output.js"], - [Diagnostics.Building_project_0, "/src/second/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/third/tsconfig.json", "src/third/thirdjs/output/third-output.js"], - [Diagnostics.Building_project_0, "/src/third/tsconfig.json"] - ); - }); - after(() => { - fs = undefined; - }); - it(`Generates files matching the baseline`, () => { - for (const mapFile of [ - "/src/first/bin/first-output.js.map", - "/src/first/bin/first-output.d.ts.map", - "/src/2/second-output.js.map", - "/src/2/second-output.d.ts.map", - "/src/third/thirdjs/output/third-output.js.map", - "/src/third/thirdjs/output/third-output.d.ts.map" - ]) { - const text = Harness.SourceMapRecorder.getSourceMapRecordWithVFS(fs!, mapFile); - fs!.writeFileSync(`${mapFile}.baseline.txt`, text); - } - - const patch = fs!.diff(); - // tslint:disable-next-line:no-null-keyword - Harness.Baseline.runBaseline(`outFile-${scenario.split(" ").join("-")}${withoutBuildInfo ? "-no-buildInfo" : ""}.js`, patch ? vfs.formatPatch(patch) : null); - }); - it("verify readFile calls", () => { - const expected = [ - // Configs - "/src/third/tsconfig.json", - "/src/second/tsconfig.json", - "/src/first/tsconfig.json", - - // Source files - "/src/third/third_part1.ts", - "/src/second/second_part1.ts", - "/src/second/second_part2.ts", - "/src/first/first_PART1.ts", - "/src/first/first_part2.ts", - "/src/first/first_part3.ts", - - // Additional source Files - ...(additionalSourceFiles || emptyArray), - - // outputs - ...outputFiles[0], - ...outputFiles[1] - ]; - - assert.equal(actualReadFileMap.size, expected.length, `Expected: ${JSON.stringify(expected)} \nActual: ${JSON.stringify(arrayFrom(actualReadFileMap.entries()))}`); - expected.forEach(expectedValue => { - const actual = actualReadFileMap.get(expectedValue); - assert.equal(actual, 1, `Mismatch in read file call number for: ${expectedValue}\nExpected: ${JSON.stringify(expected)} \nActual: ${JSON.stringify(arrayFrom(actualReadFileMap.entries()))}`); - }); - }); - }); - } - - function verifyOutFileScenario(scenario: string, modifyFs: (fs: vfs.FileSystem) => void | ReadonlyArray) { - verifyOutFileScenarioWorker(scenario, modifyFs, /*withoutBuildInfo*/ false); - verifyOutFileScenarioWorker(scenario, modifyFs, /*withoutBuildInfo*/ true); - } - - verifyOutFileScenario("baseline sectioned sourcemaps", noop); - - verifyOutFileScenario("when final project is not composite but uses project references", fs => replaceFileContent(fs, "/src/third/tsconfig.json", `"composite": true,`, "")); - - describe("downstream prepend projects always get rebuilt", () => { - function verify(modifyFs: (fs: vfs.FileSystem) => void, ...expectedDiagnostics: fakes.ExpectedDiagnostic[]) { - const fs = outFileFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host); - builder.buildAllProjects(); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/first/tsconfig.json", "src/first/bin/first-output.js"], - [Diagnostics.Building_project_0, "/src/first/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/second/tsconfig.json", "src/2/second-output.js"], - [Diagnostics.Building_project_0, "/src/second/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/third/tsconfig.json", "src/third/thirdjs/output/third-output.js"], - [Diagnostics.Building_project_0, "/src/third/tsconfig.json"] - - ); - assert.equal(fs.statSync("src/third/thirdjs/output/third-output.js").mtimeMs, time(), "First build timestamp is correct"); - tick(); - host.clearDiagnostics(); - modifyFs(fs); - tick(); - builder.resetBuildContext(); - builder.buildAllProjects(); - host.assertDiagnosticMessages(...expectedDiagnostics); - assert.equal(fs.statSync("src/third/thirdjs/output/third-output.js").mtimeMs, time(), "Second build timestamp is correct"); - } - it("when declaration changes", () => { - verify(fs => replaceText(fs, "src/first/first_PART1.ts", "Hello", "Hola"), - getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), - [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, "src/first/tsconfig.json", "src/first/bin/first-output.js", "src/first/first_PART1.ts"], - [Diagnostics.Building_project_0, "/src/first/tsconfig.json"], - [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/second/tsconfig.json", "src/second/second_part1.ts", "src/2/second-output.js"], - [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, "src/third/tsconfig.json", "src/third/thirdjs/output/third-output.js", "src/first"], - [Diagnostics.Building_project_0, "/src/third/tsconfig.json"] - ); - }); - it("when declaration doesnt change", () => { - verify(fs => appendFileContent(fs, "src/first/first_PART1.ts", "console.log(s);"), - getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), - [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, "src/first/tsconfig.json", "src/first/bin/first-output.js", "src/first/first_PART1.ts"], - [Diagnostics.Building_project_0, "/src/first/tsconfig.json"], - [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/second/tsconfig.json", "src/second/second_part1.ts", "src/2/second-output.js"], - [Diagnostics.Project_0_is_out_of_date_because_output_to_prepend_from_its_dependency_1_has_changed, "src/third/tsconfig.json", "src/first"], - [Diagnostics.Building_project_0, "/src/third/tsconfig.json"] - ); - }); - }); - - it("clean projects", () => { - const fs = outFileFs.shadow(); - const expectedOutputs = [ - ...outputFiles[0], - ...outputFiles[1], - ...outputFiles[2] - ]; - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host); - builder.buildAllProjects(); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/first/tsconfig.json", "src/first/bin/first-output.js"], - [Diagnostics.Building_project_0, "/src/first/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/second/tsconfig.json", "src/2/second-output.js"], - [Diagnostics.Building_project_0, "/src/second/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/third/tsconfig.json", "src/third/thirdjs/output/third-output.js"], - [Diagnostics.Building_project_0, "/src/third/tsconfig.json"] - ); - // Verify they exist - for (const output of expectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - host.clearDiagnostics(); - builder.cleanAllProjects(); - host.assertDiagnosticMessages(/*none*/); - // Verify they are gone - for (const output of expectedOutputs) { - assert(!fs.existsSync(output), `Expect file ${output} to not exist`); - } - // Subsequent clean shouldn't throw / etc - builder.cleanAllProjects(); - }); - - it("verify buildInfo presence or absence does not result in new build", () => { - const fs = outFileFs.shadow(); - const expectedOutputs = [ - ...outputFiles[0], - ...outputFiles[1], - ...outputFiles[2] - ]; - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host); - builder.buildAllProjects(); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/first/tsconfig.json", "src/first/bin/first-output.js"], - [Diagnostics.Building_project_0, "/src/first/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/second/tsconfig.json", "src/2/second-output.js"], - [Diagnostics.Building_project_0, "/src/second/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/third/tsconfig.json", "src/third/thirdjs/output/third-output.js"], - [Diagnostics.Building_project_0, "/src/third/tsconfig.json"] - ); - // Verify they exist - for (const output of expectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - // Delete bundle info - host.clearDiagnostics(); - host.deleteFile(last(outputFiles[0])); - builder.resetBuildContext(); - builder.buildAllProjects(); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), - [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/first/tsconfig.json", "src/first/first_PART1.ts", "src/first/bin/first-output.js"], - [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/second/tsconfig.json", "src/second/second_part1.ts", "src/2/second-output.js"], - [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/third/tsconfig.json", "src/third/third_part1.ts", "src/third/thirdjs/output/third-output.js"], - ); - }); - - function replaceFileContent(fs: vfs.FileSystem, path: string, searchValue: string, replaceValue: string) { - const content = fs.readFileSync(path, "utf8"); - fs.writeFileSync(path, content.replace(searchValue, replaceValue)); - } - - function prependFileContent(fs: vfs.FileSystem, path: string, additionalContent: string) { - const content = fs.readFileSync(path, "utf8"); - fs.writeFileSync(path, `${additionalContent}${content}`); - } - - function appendFileContent(fs: vfs.FileSystem, path: string, additionalContent: string) { - const content = fs.readFileSync(path, "utf8"); - fs.writeFileSync(path, `${content}${additionalContent}`); - } - - // Prologues - function enableStrict(fs: vfs.FileSystem, path: string) { - replaceFileContent(fs, path, `"strict": false`, `"strict": true`); - } - verifyOutFileScenario("strict in all projects", fs => { - enableStrict(fs, "src/first/tsconfig.json"); - enableStrict(fs, "src/second/tsconfig.json"); - enableStrict(fs, "src/third/tsconfig.json"); - }); - verifyOutFileScenario("strict in one dependency", fs => { - enableStrict(fs, "src/second/tsconfig.json"); - }); - - function addPrologue(fs: vfs.FileSystem, path: string, prologue: string) { - prependFileContent(fs, path, `${prologue} -`); - } - verifyOutFileScenario("multiple prologues in all projects", fs => { - enableStrict(fs, "src/first/tsconfig.json"); - addPrologue(fs, "src/first/first_PART1.ts", `"myPrologue"`); - enableStrict(fs, "src/second/tsconfig.json"); - addPrologue(fs, "src/second/second_part1.ts", `"myPrologue"`); - addPrologue(fs, "src/second/second_part2.ts", `"myPrologue2";`); - enableStrict(fs, "src/third/tsconfig.json"); - addPrologue(fs, "src/third/third_part1.ts", `"myPrologue";`); - addPrologue(fs, "src/third/third_part1.ts", `"myPrologue3";`); - }); - verifyOutFileScenario("multiple prologues in different projects", fs => { - enableStrict(fs, "src/first/tsconfig.json"); - addPrologue(fs, "src/second/second_part1.ts", `"myPrologue"`); - addPrologue(fs, "src/second/second_part2.ts", `"myPrologue2";`); - enableStrict(fs, "src/third/tsconfig.json"); - }); - - // Shebang - function addShebang(fs: vfs.FileSystem, project: string, file: string) { - prependFileContent(fs, `src/${project}/${file}.ts`, `#!someshebang ${project} ${file} -`); - } - verifyOutFileScenario("shebang in all projects", fs => { - addShebang(fs, "first", "first_PART1"); - addShebang(fs, "first", "first_part2"); - addShebang(fs, "second", "second_part1"); - addShebang(fs, "third", "third_part1"); - }); - verifyOutFileScenario("shebang in only one dependency project", fs => { - addShebang(fs, "second", "second_part1"); - }); - - // emitHelpers - function addExtendsClause(fs: vfs.FileSystem, project: string, file: string) { - appendFileContent(fs, `src/${project}/${file}.ts`, ` -class ${project}1 { } -class ${project}2 extends ${project}1 { }`); - } - verifyOutFileScenario("emitHelpers in all projects", fs => { - addExtendsClause(fs, "first", "first_part2"); - addExtendsClause(fs, "second", "second_part1"); - addExtendsClause(fs, "third", "third_part1"); - }); - verifyOutFileScenario("emitHelpers in only one dependency project", fs => { - addExtendsClause(fs, "second", "second_part1"); - }); - function addSpread(fs: vfs.FileSystem, project: string, file: string) { - const path = `src/${project}/${file}.ts`; - const content = fs.readFileSync(path, "utf8"); - fs.writeFileSync(path, `${content} -function ${project}${file}Spread(...b: number[]) { } -${project}${file}Spread(...[10, 20, 30]);`); - - replaceFileContent(fs, `src/${project}/tsconfig.json`, `"strict": false,`, `"strict": false, - "downlevelIteration": true,`); - } - verifyOutFileScenario("multiple emitHelpers in all projects", fs => { - addExtendsClause(fs, "first", "first_part2"); - addSpread(fs, "first", "first_part3"); - addExtendsClause(fs, "second", "second_part1"); - addSpread(fs, "second", "second_part2"); - addExtendsClause(fs, "third", "third_part1"); - addSpread(fs, "third", "third_part1"); - }); - verifyOutFileScenario("multiple emitHelpers in different projects", fs => { - addSpread(fs, "first", "first_part3"); - addExtendsClause(fs, "second", "second_part1"); - addSpread(fs, "third", "third_part1"); - }); - - - // triple slash refs - function addTripleSlashRef(fs: vfs.FileSystem, project: string, file: string) { - const tripleSlashRef = `/src/${project}/tripleRef.d.ts`; - fs.writeFileSync(tripleSlashRef, `declare class ${project}${file} { }`); - prependFileContent(fs, `src/${project}/${file}.ts`, `/// -const ${file}Const = new ${project}${file}(); -`); - return tripleSlashRef; - } - verifyOutFileScenario("triple slash refs in all projects", fs => [ - addTripleSlashRef(fs, "first", "first_part2"), - addTripleSlashRef(fs, "second", "second_part1"), - addTripleSlashRef(fs, "third", "third_part1") - ]); - verifyOutFileScenario("triple slash refs in one project", fs => [ - addTripleSlashRef(fs, "second", "second_part1") - ]); - }); - } - - export namespace EmptyFiles { - const projFs = loadProjectFromDisk("tests/projects/empty-files"); - - const allExpectedOutputs = [ - "/src/core/index.js", - "/src/core/index.d.ts", - "/src/core/index.d.ts.map", - ]; - - describe("unittests:: tsbuild - empty files option in tsconfig", () => { - it("has empty files diagnostic when files is empty and no references are provided", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/no-references"], { dry: false, force: false, verbose: false }); - - host.clearDiagnostics(); - builder.buildAllProjects(); - host.assertDiagnosticMessages([Diagnostics.The_files_list_in_config_file_0_is_empty, "/src/no-references/tsconfig.json"]); - - // Check for outputs to not be written. - for (const output of allExpectedOutputs) { - assert(!fs.existsSync(output), `Expect file ${output} to not exist`); - } - }); - - it("does not have empty files diagnostic when files is empty and references are provided", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/with-references"], { dry: false, force: false, verbose: false }); - - host.clearDiagnostics(); - builder.buildAllProjects(); - host.assertDiagnosticMessages(/*empty*/); - - // Check for outputs to be written. - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - }); - }); - } - - describe("unittests:: tsbuild - graph-ordering", () => { - let host: fakes.SolutionBuilderHost | undefined; - const deps: [string, string][] = [ - ["A", "B"], - ["B", "C"], - ["A", "C"], - ["B", "D"], - ["C", "D"], - ["C", "E"], - ["F", "E"] - ]; - - before(() => { - const fs = new vfs.FileSystem(false); - host = new fakes.SolutionBuilderHost(fs); - writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); - }); - - after(() => { - host = undefined; - }); - - it("orders the graph correctly - specify two roots", () => { - checkGraphOrdering(["A", "G"], ["A", "B", "C", "D", "E", "G"]); - }); - - it("orders the graph correctly - multiple parts of the same graph in various orders", () => { - checkGraphOrdering(["A"], ["A", "B", "C", "D", "E"]); - checkGraphOrdering(["A", "C", "D"], ["A", "B", "C", "D", "E"]); - checkGraphOrdering(["D", "C", "A"], ["A", "B", "C", "D", "E"]); - }); - - it("orders the graph correctly - other orderings", () => { - checkGraphOrdering(["F"], ["F", "E"]); - checkGraphOrdering(["E"], ["E"]); - checkGraphOrdering(["F", "C", "A"], ["A", "B", "C", "D", "E", "F"]); - }); - - function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { - const builder = createSolutionBuilder(host!, rootNames, { dry: true, force: false, verbose: false }); - - const projFileNames = rootNames.map(getProjectFileName); - const graph = builder.getBuildGraph(projFileNames); - - assert.sameMembers(graph.buildQueue, expectedBuildSet.map(getProjectFileName)); - - for (const dep of deps) { - const child = getProjectFileName(dep[0]); - if (graph.buildQueue.indexOf(child) < 0) continue; - const parent = getProjectFileName(dep[1]); - assert.isAbove(graph.buildQueue.indexOf(child), graph.buildQueue.indexOf(parent), `Expecting child ${child} to be built after parent ${parent}`); - } - } - - function getProjectFileName(proj: string) { - return `/project/${proj}/tsconfig.json` as ResolvedConfigFileName; - } - - function writeProjects(fileSystem: vfs.FileSystem, projectNames: string[], deps: [string, string][]): string[] { - const projFileNames: string[] = []; - for (const dep of deps) { - if (projectNames.indexOf(dep[0]) < 0) throw new Error(`Invalid dependency - project ${dep[0]} does not exist`); - if (projectNames.indexOf(dep[1]) < 0) throw new Error(`Invalid dependency - project ${dep[1]} does not exist`); - } - for (const proj of projectNames) { - fileSystem.mkdirpSync(`/project/${proj}`); - fileSystem.writeFileSync(`/project/${proj}/${proj}.ts`, "export {}"); - const configFileName = getProjectFileName(proj); - const configContent = JSON.stringify({ - compilerOptions: { composite: true }, - files: [`./${proj}.ts`], - references: deps.filter(d => d[0] === proj).map(d => ({ path: `../${d[1]}` })) - }, undefined, 2); - fileSystem.writeFileSync(configFileName, configContent); - projFileNames.push(configFileName); - } - return projFileNames; - } - }); - - - function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { - if (!fs.statSync(path).isFile()) { - throw new Error(`File ${path} does not exist`); - } - const old = fs.readFileSync(path, "utf-8"); - if (old.indexOf(oldText) < 0) { - throw new Error(`Text "${oldText}" does not exist in file ${path}`); - } - const newContent = old.replace(oldText, newText); - fs.writeFileSync(path, newContent, "utf-8"); - } - - function tick() { - currentTime += 60_000; - } - - function time() { - return currentTime; - } - - function touch(fs: vfs.FileSystem, path: string) { - if (!fs.statSync(path).isFile()) { - throw new Error(`File ${path} does not exist`); - } - fs.utimesSync(path, new Date(time()), new Date(time())); - } - - function loadProjectFromDisk(root: string): vfs.FileSystem { - const resolver = vfs.createResolver(Harness.IO); - const fs = new vfs.FileSystem(/*ignoreCase*/ true, { - files: { - ["/lib"]: new vfs.Mount(vpath.resolve(Harness.IO.getWorkspaceRoot(), "built/local"), resolver), - ["/src"]: new vfs.Mount(vpath.resolve(Harness.IO.getWorkspaceRoot(), root), resolver) - }, - cwd: "/", - meta: { defaultLibLocation: "/lib" }, - time - }); - fs.makeReadonly(); - return fs; - } - - function getLibs() { - return [ - "/lib/lib.d.ts", - "/lib/lib.es5.d.ts", - "/lib/lib.dom.d.ts", - "/lib/lib.webworker.importscripts.d.ts", - "/lib/lib.scripthost.d.ts" - ]; - } -} diff --git a/src/testRunner/unittests/tsbuild/emptyFiles.ts b/src/testRunner/unittests/tsbuild/emptyFiles.ts new file mode 100644 index 00000000000..17c6644ccc6 --- /dev/null +++ b/src/testRunner/unittests/tsbuild/emptyFiles.ts @@ -0,0 +1,41 @@ +namespace ts { + const projFs = loadProjectFromDisk("tests/projects/empty-files"); + + const allExpectedOutputs = [ + "/src/core/index.js", + "/src/core/index.d.ts", + "/src/core/index.d.ts.map", + ]; + + describe("unittests:: tsbuild - empty files option in tsconfig", () => { + it("has empty files diagnostic when files is empty and no references are provided", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/no-references"], { dry: false, force: false, verbose: false }); + + host.clearDiagnostics(); + builder.buildAllProjects(); + host.assertDiagnosticMessages([Diagnostics.The_files_list_in_config_file_0_is_empty, "/src/no-references/tsconfig.json"]); + + // Check for outputs to not be written. + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + }); + + it("does not have empty files diagnostic when files is empty and references are provided", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/with-references"], { dry: false, force: false, verbose: false }); + + host.clearDiagnostics(); + builder.buildAllProjects(); + host.assertDiagnosticMessages(/*empty*/); + + // Check for outputs to be written. + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + }); + }); +} diff --git a/src/testRunner/unittests/tsbuild/graphOrdering.ts b/src/testRunner/unittests/tsbuild/graphOrdering.ts new file mode 100644 index 00000000000..bc1af3f71da --- /dev/null +++ b/src/testRunner/unittests/tsbuild/graphOrdering.ts @@ -0,0 +1,81 @@ +namespace ts { + describe("unittests:: tsbuild - graph-ordering", () => { + let host: fakes.SolutionBuilderHost | undefined; + const deps: [string, string][] = [ + ["A", "B"], + ["B", "C"], + ["A", "C"], + ["B", "D"], + ["C", "D"], + ["C", "E"], + ["F", "E"] + ]; + + before(() => { + const fs = new vfs.FileSystem(false); + host = new fakes.SolutionBuilderHost(fs); + writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); + }); + + after(() => { + host = undefined; + }); + + it("orders the graph correctly - specify two roots", () => { + checkGraphOrdering(["A", "G"], ["A", "B", "C", "D", "E", "G"]); + }); + + it("orders the graph correctly - multiple parts of the same graph in various orders", () => { + checkGraphOrdering(["A"], ["A", "B", "C", "D", "E"]); + checkGraphOrdering(["A", "C", "D"], ["A", "B", "C", "D", "E"]); + checkGraphOrdering(["D", "C", "A"], ["A", "B", "C", "D", "E"]); + }); + + it("orders the graph correctly - other orderings", () => { + checkGraphOrdering(["F"], ["F", "E"]); + checkGraphOrdering(["E"], ["E"]); + checkGraphOrdering(["F", "C", "A"], ["A", "B", "C", "D", "E", "F"]); + }); + + function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { + const builder = createSolutionBuilder(host!, rootNames, { dry: true, force: false, verbose: false }); + + const projFileNames = rootNames.map(getProjectFileName); + const graph = builder.getBuildGraph(projFileNames); + + assert.sameMembers(graph.buildQueue, expectedBuildSet.map(getProjectFileName)); + + for (const dep of deps) { + const child = getProjectFileName(dep[0]); + if (graph.buildQueue.indexOf(child) < 0) continue; + const parent = getProjectFileName(dep[1]); + assert.isAbove(graph.buildQueue.indexOf(child), graph.buildQueue.indexOf(parent), `Expecting child ${child} to be built after parent ${parent}`); + } + } + + function getProjectFileName(proj: string) { + return `/project/${proj}/tsconfig.json` as ResolvedConfigFileName; + } + + function writeProjects(fileSystem: vfs.FileSystem, projectNames: string[], deps: [string, string][]): string[] { + const projFileNames: string[] = []; + for (const dep of deps) { + if (projectNames.indexOf(dep[0]) < 0) throw new Error(`Invalid dependency - project ${dep[0]} does not exist`); + if (projectNames.indexOf(dep[1]) < 0) throw new Error(`Invalid dependency - project ${dep[1]} does not exist`); + } + for (const proj of projectNames) { + fileSystem.mkdirpSync(`/project/${proj}`); + fileSystem.writeFileSync(`/project/${proj}/${proj}.ts`, "export {}"); + const configFileName = getProjectFileName(proj); + const configContent = JSON.stringify({ + compilerOptions: { composite: true }, + files: [`./${proj}.ts`], + references: deps.filter(d => d[0] === proj).map(d => ({ path: `../${d[1]}` })) + }, undefined, 2); + fileSystem.writeFileSync(configFileName, configContent); + projFileNames.push(configFileName); + } + return projFileNames; + } + }); +} diff --git a/src/testRunner/unittests/tsbuild/helpers.ts b/src/testRunner/unittests/tsbuild/helpers.ts new file mode 100644 index 00000000000..ae60bc1a44a --- /dev/null +++ b/src/testRunner/unittests/tsbuild/helpers.ts @@ -0,0 +1,62 @@ +namespace ts { + export function getExpectedDiagnosticForProjectsInBuild(...projects: string[]): fakes.ExpectedDiagnostic { + return [Diagnostics.Projects_in_this_build_Colon_0, projects.map(p => "\r\n * " + p).join("")]; + } + + export function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); + } + const old = fs.readFileSync(path, "utf-8"); + if (old.indexOf(oldText) < 0) { + throw new Error(`Text "${oldText}" does not exist in file ${path}`); + } + const newContent = old.replace(oldText, newText); + fs.writeFileSync(path, newContent, "utf-8"); + } + + export function getTime() { + let currentTime = 100; + return { tick, time, touch }; + + function tick() { + currentTime += 60_000; + } + + function time() { + return currentTime; + } + + function touch(fs: vfs.FileSystem, path: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); + } + fs.utimesSync(path, new Date(time()), new Date(time())); + } + } + + export function loadProjectFromDisk(root: string, time?: vfs.FileSystemOptions["time"]): vfs.FileSystem { + const resolver = vfs.createResolver(Harness.IO); + const fs = new vfs.FileSystem(/*ignoreCase*/ true, { + files: { + ["/lib"]: new vfs.Mount(vpath.resolve(Harness.IO.getWorkspaceRoot(), "built/local"), resolver), + ["/src"]: new vfs.Mount(vpath.resolve(Harness.IO.getWorkspaceRoot(), root), resolver) + }, + cwd: "/", + meta: { defaultLibLocation: "/lib" }, + time + }); + fs.makeReadonly(); + return fs; + } + + export function getLibs() { + return [ + "/lib/lib.d.ts", + "/lib/lib.es5.d.ts", + "/lib/lib.dom.d.ts", + "/lib/lib.webworker.importscripts.d.ts", + "/lib/lib.scripthost.d.ts" + ]; + } +} diff --git a/src/testRunner/unittests/tsbuild/missingExtendedFile.ts b/src/testRunner/unittests/tsbuild/missingExtendedFile.ts new file mode 100644 index 00000000000..00e8bfec1e3 --- /dev/null +++ b/src/testRunner/unittests/tsbuild/missingExtendedFile.ts @@ -0,0 +1,17 @@ +namespace ts { + describe("unittests:: tsbuild:: when tsconfig extends the missing file", () => { + it("unittests:: tsbuild - when tsconfig extends the missing file", () => { + const projFs = loadProjectFromDisk("tests/projects/missingExtendedConfig"); + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tsconfig.json"], {}); + builder.buildAllProjects(); + host.assertDiagnosticMessages( + [Diagnostics.The_specified_path_does_not_exist_Colon_0, "/src/foobar.json"], + [Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, "/src/tsconfig.first.json", "[\"**/*\"]", "[]"], + [Diagnostics.The_specified_path_does_not_exist_Colon_0, "/src/foobar.json"], + [Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, "/src/tsconfig.second.json", "[\"**/*\"]", "[]"] + ); + }); + }); +} diff --git a/src/testRunner/unittests/tsbuild/outFile.ts b/src/testRunner/unittests/tsbuild/outFile.ts new file mode 100644 index 00000000000..c16072d3577 --- /dev/null +++ b/src/testRunner/unittests/tsbuild/outFile.ts @@ -0,0 +1,379 @@ +namespace ts { + describe("unittests:: tsbuild:: outFile::", () => { + let outFileFs: vfs.FileSystem; + const outputFiles: [ReadonlyArray, ReadonlyArray, ReadonlyArray] = [ + [ + "/src/first/bin/first-output.js", + "/src/first/bin/first-output.js.map", + "/src/first/bin/first-output.d.ts", + "/src/first/bin/first-output.d.ts.map", + "/src/first/bin/.tsbuildinfo" + ], + [ + "/src/2/second-output.js", + "/src/2/second-output.js.map", + "/src/2/second-output.d.ts", + "/src/2/second-output.d.ts.map", + "/src/2/.tsbuildinfo" + ], + [ + "/src/third/thirdjs/output/third-output.js", + "/src/third/thirdjs/output/third-output.js.map", + "/src/third/thirdjs/output/third-output.d.ts", + "/src/third/thirdjs/output/third-output.d.ts.map", + "/src/third/thirdjs/output/.tsbuildinfo" + ] + ]; + const { time, tick } = getTime(); + before(() => { + outFileFs = loadProjectFromDisk("tests/projects/outfile-concat", time); + }); + after(() => { + outFileFs = undefined!; + }); + + function createSolutionBuilder(host: fakes.SolutionBuilderHost) { + return ts.createSolutionBuilder(host, ["/src/third"], { dry: false, force: false, verbose: true }); + } + + function verifyOutFileScenarioWorker(scenario: string, modifyFs: (fs: vfs.FileSystem) => void | ReadonlyArray, withoutBuildInfo: boolean) { + describe(`${scenario}${withoutBuildInfo ? " without build info" : ""}`, () => { + let fs: vfs.FileSystem | undefined; + const actualReadFileMap = createMap(); + let additionalSourceFiles: ReadonlyArray | void; + before(() => { + fs = outFileFs.shadow(); + additionalSourceFiles = modifyFs(fs); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host); + host.clearDiagnostics(); + const originalReadFile = host.readFile; + host.readFile = path => { + // Dont record libs + if (path.startsWith("/src/")) { + actualReadFileMap.set(path, (actualReadFileMap.get(path) || 0) + 1); + } + if (withoutBuildInfo && getBaseFileName(path) === infoFile) { + return undefined; + } + return originalReadFile.call(host, path); + }; + if (withoutBuildInfo) { + const originalWriteFile = host.writeFile; + host.writeFile = (fileName, content, writeByteOrder) => { + return getBaseFileName(fileName) !== infoFile && + originalWriteFile.call(host, fileName, content, writeByteOrder); + }; + } + builder.buildAllProjects(); + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/first/tsconfig.json", "src/first/bin/first-output.js"], + [Diagnostics.Building_project_0, "/src/first/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/second/tsconfig.json", "src/2/second-output.js"], + [Diagnostics.Building_project_0, "/src/second/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/third/tsconfig.json", "src/third/thirdjs/output/third-output.js"], + [Diagnostics.Building_project_0, "/src/third/tsconfig.json"] + ); + }); + after(() => { + fs = undefined; + }); + it(`Generates files matching the baseline`, () => { + for (const mapFile of [ + "/src/first/bin/first-output.js.map", + "/src/first/bin/first-output.d.ts.map", + "/src/2/second-output.js.map", + "/src/2/second-output.d.ts.map", + "/src/third/thirdjs/output/third-output.js.map", + "/src/third/thirdjs/output/third-output.d.ts.map" + ]) { + const text = Harness.SourceMapRecorder.getSourceMapRecordWithVFS(fs!, mapFile); + fs!.writeFileSync(`${mapFile}.baseline.txt`, text); + } + + const patch = fs!.diff(); + // tslint:disable-next-line:no-null-keyword + Harness.Baseline.runBaseline(`outFile-${scenario.split(" ").join("-")}${withoutBuildInfo ? "-no-buildInfo" : ""}.js`, patch ? vfs.formatPatch(patch) : null); + }); + it("verify readFile calls", () => { + const expected = [ + // Configs + "/src/third/tsconfig.json", + "/src/second/tsconfig.json", + "/src/first/tsconfig.json", + + // Source files + "/src/third/third_part1.ts", + "/src/second/second_part1.ts", + "/src/second/second_part2.ts", + "/src/first/first_PART1.ts", + "/src/first/first_part2.ts", + "/src/first/first_part3.ts", + + // Additional source Files + ...(additionalSourceFiles || emptyArray), + + // outputs + ...outputFiles[0], + ...outputFiles[1] + ]; + + assert.equal(actualReadFileMap.size, expected.length, `Expected: ${JSON.stringify(expected)} \nActual: ${JSON.stringify(arrayFrom(actualReadFileMap.entries()))}`); + expected.forEach(expectedValue => { + const actual = actualReadFileMap.get(expectedValue); + assert.equal(actual, 1, `Mismatch in read file call number for: ${expectedValue}\nExpected: ${JSON.stringify(expected)} \nActual: ${JSON.stringify(arrayFrom(actualReadFileMap.entries()))}`); + }); + }); + }); + } + + function verifyOutFileScenario(scenario: string, modifyFs: (fs: vfs.FileSystem) => void | ReadonlyArray) { + verifyOutFileScenarioWorker(scenario, modifyFs, /*withoutBuildInfo*/ false); + verifyOutFileScenarioWorker(scenario, modifyFs, /*withoutBuildInfo*/ true); + } + + verifyOutFileScenario("baseline sectioned sourcemaps", noop); + + verifyOutFileScenario("when final project is not composite but uses project references", fs => replaceFileContent(fs, "/src/third/tsconfig.json", `"composite": true,`, "")); + + describe("downstream prepend projects always get rebuilt", () => { + function verify(modifyFs: (fs: vfs.FileSystem) => void, ...expectedDiagnostics: fakes.ExpectedDiagnostic[]) { + const fs = outFileFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host); + builder.buildAllProjects(); + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/first/tsconfig.json", "src/first/bin/first-output.js"], + [Diagnostics.Building_project_0, "/src/first/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/second/tsconfig.json", "src/2/second-output.js"], + [Diagnostics.Building_project_0, "/src/second/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/third/tsconfig.json", "src/third/thirdjs/output/third-output.js"], + [Diagnostics.Building_project_0, "/src/third/tsconfig.json"] + + ); + assert.equal(fs.statSync("src/third/thirdjs/output/third-output.js").mtimeMs, time(), "First build timestamp is correct"); + tick(); + host.clearDiagnostics(); + modifyFs(fs); + tick(); + builder.resetBuildContext(); + builder.buildAllProjects(); + host.assertDiagnosticMessages(...expectedDiagnostics); + assert.equal(fs.statSync("src/third/thirdjs/output/third-output.js").mtimeMs, time(), "Second build timestamp is correct"); + } + it("when declaration changes", () => { + verify(fs => replaceText(fs, "src/first/first_PART1.ts", "Hello", "Hola"), + getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), + [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, "src/first/tsconfig.json", "src/first/bin/first-output.js", "src/first/first_PART1.ts"], + [Diagnostics.Building_project_0, "/src/first/tsconfig.json"], + [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/second/tsconfig.json", "src/second/second_part1.ts", "src/2/second-output.js"], + [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, "src/third/tsconfig.json", "src/third/thirdjs/output/third-output.js", "src/first"], + [Diagnostics.Building_project_0, "/src/third/tsconfig.json"] + ); + }); + it("when declaration doesnt change", () => { + verify(fs => appendFileContent(fs, "src/first/first_PART1.ts", "console.log(s);"), + getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), + [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, "src/first/tsconfig.json", "src/first/bin/first-output.js", "src/first/first_PART1.ts"], + [Diagnostics.Building_project_0, "/src/first/tsconfig.json"], + [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/second/tsconfig.json", "src/second/second_part1.ts", "src/2/second-output.js"], + [Diagnostics.Project_0_is_out_of_date_because_output_to_prepend_from_its_dependency_1_has_changed, "src/third/tsconfig.json", "src/first"], + [Diagnostics.Building_project_0, "/src/third/tsconfig.json"] + ); + }); + }); + + it("clean projects", () => { + const fs = outFileFs.shadow(); + const expectedOutputs = [ + ...outputFiles[0], + ...outputFiles[1], + ...outputFiles[2] + ]; + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host); + builder.buildAllProjects(); + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/first/tsconfig.json", "src/first/bin/first-output.js"], + [Diagnostics.Building_project_0, "/src/first/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/second/tsconfig.json", "src/2/second-output.js"], + [Diagnostics.Building_project_0, "/src/second/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/third/tsconfig.json", "src/third/thirdjs/output/third-output.js"], + [Diagnostics.Building_project_0, "/src/third/tsconfig.json"] + ); + // Verify they exist + for (const output of expectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + host.clearDiagnostics(); + builder.cleanAllProjects(); + host.assertDiagnosticMessages(/*none*/); + // Verify they are gone + for (const output of expectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + // Subsequent clean shouldn't throw / etc + builder.cleanAllProjects(); + }); + + it("verify buildInfo presence or absence does not result in new build", () => { + const fs = outFileFs.shadow(); + const expectedOutputs = [ + ...outputFiles[0], + ...outputFiles[1], + ...outputFiles[2] + ]; + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host); + builder.buildAllProjects(); + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/first/tsconfig.json", "src/first/bin/first-output.js"], + [Diagnostics.Building_project_0, "/src/first/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/second/tsconfig.json", "src/2/second-output.js"], + [Diagnostics.Building_project_0, "/src/second/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/third/tsconfig.json", "src/third/thirdjs/output/third-output.js"], + [Diagnostics.Building_project_0, "/src/third/tsconfig.json"] + ); + // Verify they exist + for (const output of expectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + // Delete bundle info + host.clearDiagnostics(); + host.deleteFile(last(outputFiles[0])); + builder.resetBuildContext(); + builder.buildAllProjects(); + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/first/tsconfig.json", "src/second/tsconfig.json", "src/third/tsconfig.json"), + [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/first/tsconfig.json", "src/first/first_PART1.ts", "src/first/bin/first-output.js"], + [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/second/tsconfig.json", "src/second/second_part1.ts", "src/2/second-output.js"], + [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/third/tsconfig.json", "src/third/third_part1.ts", "src/third/thirdjs/output/third-output.js"], + ); + }); + + function replaceFileContent(fs: vfs.FileSystem, path: string, searchValue: string, replaceValue: string) { + const content = fs.readFileSync(path, "utf8"); + fs.writeFileSync(path, content.replace(searchValue, replaceValue)); + } + + function prependFileContent(fs: vfs.FileSystem, path: string, additionalContent: string) { + const content = fs.readFileSync(path, "utf8"); + fs.writeFileSync(path, `${additionalContent}${content}`); + } + + function appendFileContent(fs: vfs.FileSystem, path: string, additionalContent: string) { + const content = fs.readFileSync(path, "utf8"); + fs.writeFileSync(path, `${content}${additionalContent}`); + } + + // Prologues + function enableStrict(fs: vfs.FileSystem, path: string) { + replaceFileContent(fs, path, `"strict": false`, `"strict": true`); + } + verifyOutFileScenario("strict in all projects", fs => { + enableStrict(fs, "src/first/tsconfig.json"); + enableStrict(fs, "src/second/tsconfig.json"); + enableStrict(fs, "src/third/tsconfig.json"); + }); + verifyOutFileScenario("strict in one dependency", fs => { + enableStrict(fs, "src/second/tsconfig.json"); + }); + + function addPrologue(fs: vfs.FileSystem, path: string, prologue: string) { + prependFileContent(fs, path, `${prologue} +`); + } + verifyOutFileScenario("multiple prologues in all projects", fs => { + enableStrict(fs, "src/first/tsconfig.json"); + addPrologue(fs, "src/first/first_PART1.ts", `"myPrologue"`); + enableStrict(fs, "src/second/tsconfig.json"); + addPrologue(fs, "src/second/second_part1.ts", `"myPrologue"`); + addPrologue(fs, "src/second/second_part2.ts", `"myPrologue2";`); + enableStrict(fs, "src/third/tsconfig.json"); + addPrologue(fs, "src/third/third_part1.ts", `"myPrologue";`); + addPrologue(fs, "src/third/third_part1.ts", `"myPrologue3";`); + }); + verifyOutFileScenario("multiple prologues in different projects", fs => { + enableStrict(fs, "src/first/tsconfig.json"); + addPrologue(fs, "src/second/second_part1.ts", `"myPrologue"`); + addPrologue(fs, "src/second/second_part2.ts", `"myPrologue2";`); + enableStrict(fs, "src/third/tsconfig.json"); + }); + + // Shebang + function addShebang(fs: vfs.FileSystem, project: string, file: string) { + prependFileContent(fs, `src/${project}/${file}.ts`, `#!someshebang ${project} ${file} +`); + } + verifyOutFileScenario("shebang in all projects", fs => { + addShebang(fs, "first", "first_PART1"); + addShebang(fs, "first", "first_part2"); + addShebang(fs, "second", "second_part1"); + addShebang(fs, "third", "third_part1"); + }); + verifyOutFileScenario("shebang in only one dependency project", fs => { + addShebang(fs, "second", "second_part1"); + }); + + // emitHelpers + function addExtendsClause(fs: vfs.FileSystem, project: string, file: string) { + appendFileContent(fs, `src/${project}/${file}.ts`, ` +class ${project}1 { } +class ${project}2 extends ${project}1 { }`); + } + verifyOutFileScenario("emitHelpers in all projects", fs => { + addExtendsClause(fs, "first", "first_part2"); + addExtendsClause(fs, "second", "second_part1"); + addExtendsClause(fs, "third", "third_part1"); + }); + verifyOutFileScenario("emitHelpers in only one dependency project", fs => { + addExtendsClause(fs, "second", "second_part1"); + }); + function addSpread(fs: vfs.FileSystem, project: string, file: string) { + const path = `src/${project}/${file}.ts`; + const content = fs.readFileSync(path, "utf8"); + fs.writeFileSync(path, `${content} +function ${project}${file}Spread(...b: number[]) { } +${project}${file}Spread(...[10, 20, 30]);`); + + replaceFileContent(fs, `src/${project}/tsconfig.json`, `"strict": false,`, `"strict": false, + "downlevelIteration": true,`); + } + verifyOutFileScenario("multiple emitHelpers in all projects", fs => { + addExtendsClause(fs, "first", "first_part2"); + addSpread(fs, "first", "first_part3"); + addExtendsClause(fs, "second", "second_part1"); + addSpread(fs, "second", "second_part2"); + addExtendsClause(fs, "third", "third_part1"); + addSpread(fs, "third", "third_part1"); + }); + verifyOutFileScenario("multiple emitHelpers in different projects", fs => { + addSpread(fs, "first", "first_part3"); + addExtendsClause(fs, "second", "second_part1"); + addSpread(fs, "third", "third_part1"); + }); + + + // triple slash refs + function addTripleSlashRef(fs: vfs.FileSystem, project: string, file: string) { + const tripleSlashRef = `/src/${project}/tripleRef.d.ts`; + fs.writeFileSync(tripleSlashRef, `declare class ${project}${file} { }`); + prependFileContent(fs, `src/${project}/${file}.ts`, `/// +const ${file}Const = new ${project}${file}(); +`); + return tripleSlashRef; + } + verifyOutFileScenario("triple slash refs in all projects", fs => [ + addTripleSlashRef(fs, "first", "first_part2"), + addTripleSlashRef(fs, "second", "second_part1"), + addTripleSlashRef(fs, "third", "third_part1") + ]); + verifyOutFileScenario("triple slash refs in one project", fs => [ + addTripleSlashRef(fs, "second", "second_part1") + ]); + }); +} diff --git a/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts new file mode 100644 index 00000000000..e10c36a4f21 --- /dev/null +++ b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts @@ -0,0 +1,20 @@ +namespace ts { + describe("unittests:: tsbuild:: with rootDir of project reference in parentDirectory", () => { + it("verify that it builds correctly", () => { + const projFs = loadProjectFromDisk("tests/projects/projectReferenceWithRootDirInParent"); + const allExpectedOutputs = [ + "/src/dist/other/other.js", "/src/dist/other/other.d.ts", + "/src/dist/main/a.js", "/src/dist/main/a.d.ts", + "/src/dist/main/b.js", "/src/dist/main/b.d.ts" + ]; + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/src/main", "/src/src/other"], {}); + builder.buildAllProjects(); + host.assertDiagnosticMessages(/*empty*/); + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + }); + }); +} diff --git a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts new file mode 100644 index 00000000000..3ec4f8aff9c --- /dev/null +++ b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts @@ -0,0 +1,61 @@ +namespace ts { + describe("unittests:: tsbuild:: with resolveJsonModule option", () => { + let projFs: vfs.FileSystem; + const allExpectedOutputs = ["/src/tests/dist/src/index.js", "/src/tests/dist/src/index.d.ts", "/src/tests/dist/src/hello.json"]; + before(() => { + projFs = loadProjectFromDisk("tests/projects/resolveJsonModuleAndComposite"); + }); + + after(() => { + projFs = undefined!; // Release the contents + }); + + function verifyProjectWithResolveJsonModule(configFile: string, ...expectedDiagnosticMessages: fakes.ExpectedDiagnostic[]) { + const fs = projFs.shadow(); + verifyProjectWithResolveJsonModuleWithFs(fs, configFile, allExpectedOutputs, ...expectedDiagnosticMessages); + } + + function verifyProjectWithResolveJsonModuleWithFs(fs: vfs.FileSystem, configFile: string, allExpectedOutputs: ReadonlyArray, ...expectedDiagnosticMessages: fakes.ExpectedDiagnostic[]) { + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, [configFile], { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); + host.assertDiagnosticMessages(...expectedDiagnosticMessages); + if (!expectedDiagnosticMessages.length) { + // Check for outputs. Not an exhaustive list + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + } + } + + it("with resolveJsonModule and include only", () => { + verifyProjectWithResolveJsonModule("/src/tests/tsconfig_withInclude.json", [ + Diagnostics.File_0_is_not_in_project_file_list_Projects_must_list_all_files_or_use_an_include_pattern, + "/src/tests/src/hello.json" + ]); + }); + + it("with resolveJsonModule and include of *.json along with other include", () => { + verifyProjectWithResolveJsonModule("/src/tests/tsconfig_withIncludeOfJson.json"); + }); + + it("with resolveJsonModule and include of *.json along with other include and file name matches ts file", () => { + const fs = projFs.shadow(); + fs.rimrafSync("/src/tests/src/hello.json"); + fs.writeFileSync("/src/tests/src/index.json", JSON.stringify({ hello: "world" })); + fs.writeFileSync("/src/tests/src/index.ts", `import hello from "./index.json" + +export default hello.hello`); + const allExpectedOutputs = ["/src/tests/dist/src/index.js", "/src/tests/dist/src/index.d.ts", "/src/tests/dist/src/index.json"]; + verifyProjectWithResolveJsonModuleWithFs(fs, "/src/tests/tsconfig_withIncludeOfJson.json", allExpectedOutputs); + }); + + it("with resolveJsonModule and files containing json file", () => { + verifyProjectWithResolveJsonModule("/src/tests/tsconfig_withFiles.json"); + }); + + it("with resolveJsonModule and include and files", () => { + verifyProjectWithResolveJsonModule("/src/tests/tsconfig_withIncludeAndFiles.json"); + }); + }); +} diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts new file mode 100644 index 00000000000..0efb413739f --- /dev/null +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -0,0 +1,361 @@ +namespace ts { + describe("unittests:: tsbuild:: on 'sample1' project", () => { + let projFs: vfs.FileSystem; + const { time, tick, touch } = getTime(); + const allExpectedOutputs = ["/src/tests/index.js", + "/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map", + "/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts"]; + + before(() => { + projFs = loadProjectFromDisk("tests/projects/sample1", time); + }); + + after(() => { + projFs = undefined!; // Release the contents + }); + + describe("sanity check of clean build of 'sample1' project", () => { + it("can build the sample project 'sample1' without error", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); + + host.clearDiagnostics(); + builder.buildAllProjects(); + host.assertDiagnosticMessages(/*empty*/); + + // Check for outputs. Not an exhaustive list + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + }); + + it("builds correctly when outDir is specified", () => { + const fs = projFs.shadow(); + fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({ + compilerOptions: { composite: true, declaration: true, sourceMap: true, outDir: "outDir" }, + references: [{ path: "../core" }] + })); + + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], {}); + builder.buildAllProjects(); + host.assertDiagnosticMessages(/*empty*/); + const expectedOutputs = allExpectedOutputs.map(f => f.replace("/logic/", "/logic/outDir/")); + // Check for outputs. Not an exhaustive list + for (const output of expectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + }); + + it("builds correctly when declarationDir is specified", () => { + const fs = projFs.shadow(); + fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({ + compilerOptions: { composite: true, declaration: true, sourceMap: true, declarationDir: "out/decls" }, + references: [{ path: "../core" }] + })); + + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], {}); + builder.buildAllProjects(); + host.assertDiagnosticMessages(/*empty*/); + const expectedOutputs = allExpectedOutputs.map(f => f.replace("/logic/index.d.ts", "/logic/out/decls/index.d.ts")); + // Check for outputs. Not an exhaustive list + for (const output of expectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + }); + }); + + describe("dry builds", () => { + it("doesn't write any files in a dry build", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: true, force: false, verbose: false }); + builder.buildAllProjects(); + host.assertDiagnosticMessages( + [Diagnostics.A_non_dry_build_would_build_project_0, "/src/core/tsconfig.json"], + [Diagnostics.A_non_dry_build_would_build_project_0, "/src/logic/tsconfig.json"], + [Diagnostics.A_non_dry_build_would_build_project_0, "/src/tests/tsconfig.json"] + ); + + // Check for outputs to not be written. Not an exhaustive list + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + }); + + it("indicates that it would skip builds during a dry build", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + + let builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); + tick(); + + host.clearDiagnostics(); + builder = createSolutionBuilder(host, ["/src/tests"], { dry: true, force: false, verbose: false }); + builder.buildAllProjects(); + host.assertDiagnosticMessages( + [Diagnostics.Project_0_is_up_to_date, "/src/core/tsconfig.json"], + [Diagnostics.Project_0_is_up_to_date, "/src/logic/tsconfig.json"], + [Diagnostics.Project_0_is_up_to_date, "/src/tests/tsconfig.json"] + ); + }); + }); + + describe("clean builds", () => { + it("removes all files it built", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); + builder.buildAllProjects(); + // Verify they exist + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + builder.cleanAllProjects(); + // Verify they are gone + for (const output of allExpectedOutputs) { + assert(!fs.existsSync(output), `Expect file ${output} to not exist`); + } + // Subsequent clean shouldn't throw / etc + builder.cleanAllProjects(); + }); + }); + + describe("force builds", () => { + it("always builds under --force", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: true, verbose: false }); + builder.buildAllProjects(); + let currentTime = time(); + checkOutputTimestamps(currentTime); + + tick(); + Debug.assert(time() !== currentTime, "Time moves on"); + currentTime = time(); + builder.buildAllProjects(); + checkOutputTimestamps(currentTime); + + function checkOutputTimestamps(expected: number) { + // Check timestamps + for (const output of allExpectedOutputs) { + const actual = fs.statSync(output).mtimeMs; + assert(actual === expected, `File ${output} has timestamp ${actual}, expected ${expected}`); + } + } + }); + }); + + describe("can detect when and what to rebuild", () => { + let fs: vfs.FileSystem; + let host: fakes.SolutionBuilderHost; + let builder: SolutionBuilder; + before(() => { + fs = projFs.shadow(); + host = new fakes.SolutionBuilderHost(fs); + builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: true }); + }); + after(() => { + fs = undefined!; + host = undefined!; + builder = undefined!; + }); + + it("Builds the project", () => { + host.clearDiagnostics(); + builder.resetBuildContext(); + builder.buildAllProjects(); + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], + [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], + [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/tests/tsconfig.json", "src/tests/index.js"], + [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"] + ); + tick(); + }); + + // All three projects are up to date + it("Detects that all projects are up to date", () => { + host.clearDiagnostics(); + builder.resetBuildContext(); + builder.buildAllProjects(); + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), + [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"], + [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"], + [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"] + ); + tick(); + }); + + // Update a file in the leaf node (tests), only it should rebuild the last one + it("Only builds the leaf node project", () => { + host.clearDiagnostics(); + fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); + builder.resetBuildContext(); + builder.buildAllProjects(); + + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), + [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"], + [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"], + [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, "src/tests/tsconfig.json", "src/tests/index.js", "src/tests/index.ts"], + [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"] + ); + tick(); + }); + + // Update a file in the parent (without affecting types), should get fast downstream builds + it("Detects type-only changes in upstream projects", () => { + host.clearDiagnostics(); + replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); + builder.resetBuildContext(); + builder.buildAllProjects(); + + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), + [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, "src/core/tsconfig.json", "src/core/anotherModule.js", "src/core/index.ts"], + [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], + [Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, "src/logic/tsconfig.json"], + [Diagnostics.Updating_output_timestamps_of_project_0, "/src/logic/tsconfig.json"], + [Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, "src/tests/tsconfig.json"], + [Diagnostics.Updating_output_timestamps_of_project_0, "/src/tests/tsconfig.json"] + ); + }); + }); + + describe("downstream-blocked compilations", () => { + it("won't build downstream projects if upstream projects have errors", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: true }); + + // Induce an error in the middle project + replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); + builder.buildAllProjects(); + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], + [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], + [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], + [Diagnostics.Property_0_does_not_exist_on_type_1, "muitply", `typeof import("/src/core/index")`], + [Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, "src/tests/tsconfig.json", "src/logic"], + [Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, "/src/tests/tsconfig.json", "/src/logic"] + ); + }); + }); + + describe("project invalidation", () => { + it("invalidates projects correctly", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); + + builder.buildAllProjects(); + host.assertDiagnosticMessages(/*empty*/); + + // Update a timestamp in the middle project + tick(); + touch(fs, "/src/logic/index.ts"); + const originalWriteFile = fs.writeFileSync; + const writtenFiles = createMap(); + fs.writeFileSync = (path, data, encoding) => { + writtenFiles.set(path, true); + originalWriteFile.call(fs, path, data, encoding); + }; + // Because we haven't reset the build context, the builder should assume there's nothing to do right now + const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic")); + assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); + verifyInvalidation(/*expectedToWriteTests*/ false); + + // Rebuild this project + fs.writeFileSync("/src/logic/index.ts", `${fs.readFileSync("/src/logic/index.ts")} +export class cNew {}`); + verifyInvalidation(/*expectedToWriteTests*/ true); + + function verifyInvalidation(expectedToWriteTests: boolean) { + // Rebuild this project + tick(); + builder.invalidateProject("/src/logic"); + builder.buildInvalidatedProject(); + // The file should be updated + assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt"); + assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); + assert.isFalse(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should *not* have been rebuilt"); + assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); + writtenFiles.clear(); + + // Build downstream projects should update 'tests', but not 'core' + tick(); + builder.buildInvalidatedProject(); + if (expectedToWriteTests) { + assert.isTrue(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should have been rebuilt"); + } + else { + assert.equal(writtenFiles.size, 0, "Should not write any new files"); + } + assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have new timestamp"); + assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); + } + }); + }); + + describe("lists files", () => { + it("listFiles", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { listFiles: true }); + builder.buildAllProjects(); + assert.deepEqual(host.traces, [ + ...getLibs(), + "/src/core/anotherModule.ts", + "/src/core/index.ts", + "/src/core/some_decl.d.ts", + ...getLibs(), + ...getCoreOutputs(), + "/src/logic/index.ts", + ...getLibs(), + ...getCoreOutputs(), + "/src/logic/index.d.ts", + "/src/tests/index.ts" + ]); + + function getCoreOutputs() { + return [ + "/src/core/index.d.ts", + "/src/core/anotherModule.d.ts" + ]; + } + }); + + it("listEmittedFiles", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { listEmittedFiles: true }); + builder.buildAllProjects(); + assert.deepEqual(host.traces, [ + "TSFILE: /src/core/anotherModule.js", + "TSFILE: /src/core/anotherModule.d.ts", + "TSFILE: /src/core/anotherModule.d.ts.map", + "TSFILE: /src/core/index.js", + "TSFILE: /src/core/index.d.ts", + "TSFILE: /src/core/index.d.ts.map", + "TSFILE: /src/logic/index.js", + "TSFILE: /src/logic/index.js.map", + "TSFILE: /src/logic/index.d.ts", + "TSFILE: /src/tests/index.js", + "TSFILE: /src/tests/index.d.ts", + ]); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsbuild/transitiveReferences.ts b/src/testRunner/unittests/tsbuild/transitiveReferences.ts new file mode 100644 index 00000000000..201c89c7b5b --- /dev/null +++ b/src/testRunner/unittests/tsbuild/transitiveReferences.ts @@ -0,0 +1,76 @@ +namespace ts { + describe("unittests:: tsbuild:: when project reference is referenced transitively", () => { + let projFs: vfs.FileSystem; + const allExpectedOutputs = [ + "/src/a.js", "/src/a.d.ts", + "/src/b.js", "/src/b.d.ts", + "/src/c.js" + ]; + const expectedFileTraces = [ + ...getLibs(), + "/src/a.ts", + ...getLibs(), + "/src/a.d.ts", + "/src/b.ts", + ...getLibs(), + "/src/a.d.ts", + "/src/b.d.ts", + "/src/refs/a.d.ts", + "/src/c.ts" + ]; + before(() => { + projFs = loadProjectFromDisk("tests/projects/transitiveReferences"); + }); + after(() => { + projFs = undefined!; // Release the contents + }); + + function verifyBuild(modifyDiskLayout: (fs: vfs.FileSystem) => void, allExpectedOutputs: ReadonlyArray, expectedFileTraces: ReadonlyArray, ...expectedDiagnostics: fakes.ExpectedDiagnostic[]) { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + modifyDiskLayout(fs); + const builder = createSolutionBuilder(host, ["/src/tsconfig.c.json"], { listFiles: true }); + builder.buildAllProjects(); + host.assertDiagnosticMessages(...expectedDiagnostics); + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + assert.deepEqual(host.traces, expectedFileTraces); + } + + function modifyFsBTsToNonRelativeImport(fs: vfs.FileSystem, moduleResolution: "node" | "classic") { + fs.writeFileSync("/src/b.ts", `import {A} from 'a'; +export const b = new A();`); + fs.writeFileSync("/src/tsconfig.b.json", JSON.stringify({ + compilerOptions: { + composite: true, + moduleResolution + }, + files: ["b.ts"], + references: [{ path: "tsconfig.a.json" }] + })); + } + + it("verify that it builds correctly", () => { + verifyBuild(noop, allExpectedOutputs, expectedFileTraces); + }); + + it("verify that it builds correctly when the referenced project uses different module resolution", () => { + verifyBuild(fs => modifyFsBTsToNonRelativeImport(fs, "classic"), allExpectedOutputs, expectedFileTraces); + }); + + it("verify that it build reports error about module not found with node resolution with external module name", () => { + // Error in b build only a + const allExpectedOutputs = ["/src/a.js", "/src/a.d.ts"]; + const expectedFileTraces = [ + ...getLibs(), + "/src/a.ts", + ]; + verifyBuild(fs => modifyFsBTsToNonRelativeImport(fs, "node"), + allExpectedOutputs, + expectedFileTraces, + [Diagnostics.Cannot_find_module_0, "a"], + ); + }); + }); +}