diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 8f1da78f1e9..bfd261802f4 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3634,6 +3634,10 @@ "category": "Message", "code": 6359 }, + "Project '{0}' is up to date": { + "category": "Message", + "code": 6360 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 10ed5280f71..ec9e18899aa 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -608,10 +608,11 @@ namespace ts { } } - // TODO Accept parsedCommandLine + // TODO Accept parsedCommandLine instead? function buildSingleProject(proj: string) { if (context.options.dry) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_build_project_0, proj)); + return; } context.verbose(Diagnostics.Building_project_0, proj); @@ -714,34 +715,47 @@ namespace ts { context.projectStatus.setValue(proj.options.configFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); } - function cleanProjects(configFileNames: string[]) { - // Get the same graph for cleaning we'd use for building - const graph = createDependencyGraph(configFileNames); + function getFilesToClean(configFileNames: string[]): string[] | undefined { + const resolvedNames: string[] | undefined = resolveProjectNames(configFileNames); + if (resolvedNames === undefined) return; - const fileReport: string[] = []; + // Get the same graph for cleaning we'd use for building + const graph = createDependencyGraph(resolvedNames); + + const filesToDelete: string[] = []; for (const level of graph.buildQueue) { for (const proj of level) { const parsed = configFileCache.parseConfigFile(proj); const outputs = getAllProjectOutputs(parsed); for (const output of outputs) { if (host.fileExists(output)) { - if (context.options.dry) { - fileReport.push(output); - } - else { - host.deleteFile(output); - } + filesToDelete.push(output); } } } } + return filesToDelete; + } + + function cleanProjects(configFileNames: string[]) { + const filesToDelete = getFilesToClean(configFileNames); if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, fileReport.map(f => `\r\n * ${f}`).join(""))); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); + } + else { + if (!host.deleteFile) { + throw new Error("Host does not support deleting files"); + } + + for (const output of filesToDelete) { + host.deleteFile(output); + } } } - function buildProjects(configFileNames: string[]) { + // TODO add branding to resolved filenames + function resolveProjectNames(configFileNames: string[]): string[] | undefined { const resolvedNames: string[] = []; for (const name of configFileNames) { let fullPath = resolvePath(host.getCurrentDirectory(), name); @@ -755,8 +769,14 @@ namespace ts { continue; } reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath)); - return; + return undefined; } + return resolvedNames; + } + + function buildProjects(configFileNames: string[]) { + const resolvedNames: string[] | undefined = resolveProjectNames(configFileNames); + if (resolvedNames === undefined) return; // Establish what needs to be built const graph = createDependencyGraph(resolvedNames); @@ -772,6 +792,10 @@ namespace ts { if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { // Up to date, skip + if (options.dry) { + // In a dry build, inform the user of this fact + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date)); + } continue; } diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index f5a2861d385..958629a0988 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -51,6 +51,10 @@ namespace fakes { this.vfs.writeFileSync(path, writeByteOrderMark ? utils.addUTF8ByteOrderMark(data) : data); } + public deleteFile(path: string) { + this.vfs.unlinkSync(path); + } + public fileExists(path: string) { const stats = this._getStats(path); return stats ? stats.isFile() : false; @@ -248,6 +252,10 @@ namespace fakes { return this.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); } + public deleteFile(fileName: string) { + this.sys.deleteFile(fileName); + } + public fileExists(fileName: string): boolean { return this.sys.fileExists(fileName); } diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index e84fa403475..a761e154898 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -11,6 +11,9 @@ namespace ts { bfs.meta.set("defaultLibLocation", "/lib"); bfs.makeReadonly(); tick(); + const allExpectedOutputs = ["/src/tests/index.js", + "/src/core/index.js", "/src/core/index.d.ts", + "/src/logic/index.js", "/src/logic/index.d.ts"]; describe("tsbuild - sanity check of clean build of 'sample1' project", () => { it("can build the sample project 'sample1' without error", () => { @@ -23,13 +26,95 @@ namespace ts { assertDiagnosticMessages(/*empty*/); // Check for outputs. Not an exhaustive list - const expectedOutputs = ["/src/tests/index.js", "/src/core/index.js", "/src/core/index.d.ts"]; - for (const output of expectedOutputs) { + for (const output of allExpectedOutputs) { assert(fs.existsSync(output), `Expect file ${output} to exist`); } }); }); + describe("tsbuild - dry builds", () => { + it("doesn't write any files in a dry build", () => { + clearDiagnostics(); + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + assertDiagnosticMessages(Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0); + + // 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", () => { + clearDiagnostics(); + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + + let builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + tick(); + + clearDiagnostics(); + builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildProjects(["."]); + assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); + }); + }); + + describe("tsbuild - clean builds", () => { + it("removes all files it built", () => { + clearDiagnostics(); + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + // Verify they exist + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + builder.cleanProjects(["."]); + // 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.cleanProjects(["."]); + }); + }); + + describe("tsbuild - force builds", () => { + it("always builds under --force", () => { + const fs = bfs.shadow(); + const host = new fakes.CompilerHost(fs); + + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: true, verbose: false }); + fs.chdir("/src/tests"); + builder.buildProjects(["."]); + let currentTime = time(); + checkOutputTimestamps(currentTime); + + tick(); + Debug.assert(time() !== currentTime, "Time moves on"); + currentTime = time(); + builder.buildProjects(["."]); + 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("tsbuild - can detect when and what to rebuild", () => { const fs = bfs.shadow(); const host = new fakes.CompilerHost(fs); @@ -38,6 +123,7 @@ namespace ts { fs.chdir("/src/tests"); it("Builds the project", () => { + clearDiagnostics(); builder.resetBuildContext(); builder.buildProjects(["."]); assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0,