Invalidation + separated downstream builds

This commit is contained in:
Ryan Cavanaugh 2018-05-31 14:38:26 -07:00
parent cd7a844a48
commit d21c03ab9d
3 changed files with 227 additions and 67 deletions

View file

@ -3685,6 +3685,10 @@
"category": "Error", "category": "Error",
"code": 6369 "code": 6369
}, },
"Skipping clean because not all projects could be located": {
"category": "Error",
"code": 6340
},
"Variable '{0}' implicitly has an '{1}' type.": { "Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error", "category": "Error",

View file

@ -38,6 +38,10 @@ namespace ts {
* Issue a verbose diagnostic message. No-ops when options.verbose is false. * Issue a verbose diagnostic message. No-ops when options.verbose is false.
*/ */
verbose(diag: DiagnosticMessage, ...args: any[]): void; verbose(diag: DiagnosticMessage, ...args: any[]): void;
invalidatedProjects: FileMap<true>;
queuedProjects: FileMap<true>;
missingRoots: Map<true>;
} }
type Mapper = ReturnType<typeof createDependencyMapper>; type Mapper = ReturnType<typeof createDependencyMapper>;
@ -73,7 +77,7 @@ namespace ts {
AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors
} }
enum UpToDateStatusType { export enum UpToDateStatusType {
Unbuildable, Unbuildable,
UpToDate, UpToDate,
/** /**
@ -89,7 +93,7 @@ namespace ts {
UpstreamBlocked UpstreamBlocked
} }
type UpToDateStatus = export type UpToDateStatus =
| Status.Unbuildable | Status.Unbuildable
| Status.UpToDate | Status.UpToDate
| Status.OutputMissing | Status.OutputMissing
@ -98,7 +102,7 @@ namespace ts {
| Status.UpstreamOutOfDate | Status.UpstreamOutOfDate
| Status.UpstreamBlocked; | Status.UpstreamBlocked;
namespace Status { export namespace Status {
/** /**
* The project can't be built at all in its current state. For example, * The project can't be built at all in its current state. For example,
* its config file cannot be parsed, or it has a syntax error or missing file * its config file cannot be parsed, or it has a syntax error or missing file
@ -170,6 +174,9 @@ namespace ts {
setValue(fileName: string, value: T): void; setValue(fileName: string, value: T): void;
getValue(fileName: string): T | never; getValue(fileName: string): T | never;
getValueOrUndefined(fileName: string): T | undefined; getValueOrUndefined(fileName: string): T | undefined;
hasKey(fileName: string): boolean;
removeKey(fileName: string): void;
getKeys(): string[];
} }
/** /**
@ -183,8 +190,23 @@ namespace ts {
setValue, setValue,
getValue, getValue,
getValueOrUndefined, getValueOrUndefined,
removeKey,
getKeys,
hasKey
}; };
function getKeys(): string[] {
return Object.keys(lookup);
}
function hasKey(fileName: string) {
return normalizePath(fileName) in lookup;
}
function removeKey(fileName: string) {
delete lookup[fileName];
}
function setValue(fileName: string, value: T) { function setValue(fileName: string, value: T) {
lookup[normalizePath(fileName)] = value; lookup[normalizePath(fileName)] = value;
} }
@ -211,30 +233,30 @@ namespace ts {
} }
export function createDependencyMapper() { export function createDependencyMapper() {
const childToParents: { [key: string]: string[] } = {}; const childToParents: { [key: string]: ResolvedConfigFileName[] } = {};
const parentToChildren: { [key: string]: string[] } = {}; const parentToChildren: { [key: string]: ResolvedConfigFileName[] } = {};
const allKeys: string[] = []; const allKeys: ResolvedConfigFileName[] = [];
function addReference(childConfigFileName: string, parentConfigFileName: string): void { function addReference(childConfigFileName: ResolvedConfigFileName, parentConfigFileName: ResolvedConfigFileName): void {
addEntry(childToParents, childConfigFileName, parentConfigFileName); addEntry(childToParents, childConfigFileName, parentConfigFileName);
addEntry(parentToChildren, parentConfigFileName, childConfigFileName); addEntry(parentToChildren, parentConfigFileName, childConfigFileName);
} }
function getReferencesTo(parentConfigFileName: string): string[] { function getReferencesTo(parentConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] {
return parentToChildren[normalizePath(parentConfigFileName)] || []; return parentToChildren[normalizePath(parentConfigFileName)] || [];
} }
function getReferencesOf(childConfigFileName: string): string[] { function getReferencesOf(childConfigFileName: ResolvedConfigFileName): ResolvedConfigFileName[] {
return childToParents[normalizePath(childConfigFileName)] || []; return childToParents[normalizePath(childConfigFileName)] || [];
} }
function getKeys(): ReadonlyArray<string> { function getKeys(): ReadonlyArray<ResolvedConfigFileName> {
return allKeys; return allKeys;
} }
function addEntry(mapToAddTo: typeof childToParents | typeof parentToChildren, key: string, element: string) { function addEntry(mapToAddTo: typeof childToParents | typeof parentToChildren, key: ResolvedConfigFileName, element: ResolvedConfigFileName) {
key = normalizePath(key); key = normalizePath(key) as ResolvedConfigFileName;
element = normalizePath(element); element = normalizePath(element) as ResolvedConfigFileName;
const arr = (mapToAddTo[key] = mapToAddTo[key] || []); const arr = (mapToAddTo[key] = mapToAddTo[key] || []);
if (arr.indexOf(element) < 0) { if (arr.indexOf(element) < 0) {
arr.push(element); arr.push(element);
@ -316,8 +338,13 @@ namespace ts {
return parsed; return parsed;
} }
function removeKey(configFilePath: ResolvedConfigFileName) {
cache.removeKey(configFilePath);
}
return { return {
parseConfigFile parseConfigFile,
removeKey
}; };
} }
@ -331,11 +358,19 @@ namespace ts {
export function createBuildContext(options: BuildOptions, reportDiagnostic: DiagnosticReporter): BuildContext { export function createBuildContext(options: BuildOptions, reportDiagnostic: DiagnosticReporter): BuildContext {
const verboseDiag = options.verbose && reportDiagnostic; const verboseDiag = options.verbose && reportDiagnostic;
const invalidatedProjects = createFileMap<true>();
const queuedProjects = createFileMap<true>();
const missingRoots = createMap<true>();
return { return {
options, options,
projectStatus: createFileMap(), projectStatus: createFileMap(),
unchangedOutputs: createFileMap(), unchangedOutputs: createFileMap(),
verbose: verboseDiag ? (diag, ...args) => verboseDiag(createCompilerDiagnostic(diag, ...args)) : () => undefined verbose: verboseDiag ? (diag, ...args) => verboseDiag(createCompilerDiagnostic(diag, ...args)) : () => undefined,
invalidatedProjects,
missingRoots,
queuedProjects
}; };
} }
@ -437,12 +472,12 @@ namespace ts {
addProject("."); addProject(".");
} }
const builder = createSolutionBuilder(host, reportDiagnostic, { verbose, dry, force }); const builder = createSolutionBuilder(host, projects, reportDiagnostic, { verbose, dry, force });
if (clean) { if (clean) {
builder.cleanProjects(projects); builder.cleanAllProjects();
} }
else { else {
builder.buildProjects(projects); builder.buildAllProjects();
} }
function addProject(projectSpecification: string) { function addProject(projectSpecification: string) {
@ -461,7 +496,11 @@ namespace ts {
} }
} }
export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions) { /**
* A SolutionBuilder has an immutable set of rootNames that are the "entry point" projects, but
* can dynamically add/remove other projects based on changes on the rootNames' references
*/
export function createSolutionBuilder(host: CompilerHost, rootNames: ReadonlyArray<string>, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions) {
if (!host.getModifiedTime || !host.setModifiedTime) { if (!host.getModifiedTime || !host.setModifiedTime) {
throw new Error("Host must support timestamp APIs"); throw new Error("Host must support timestamp APIs");
} }
@ -470,12 +509,18 @@ namespace ts {
let context = createBuildContext(defaultOptions, reportDiagnostic); let context = createBuildContext(defaultOptions, reportDiagnostic);
return { return {
buildAllProjects,
getUpToDateStatus, getUpToDateStatus,
getUpToDateStatusOfFile, getUpToDateStatusOfFile,
buildProjects, cleanAllProjects,
cleanProjects,
resetBuildContext, resetBuildContext,
getBuildGraph getBuildGraph,
invalidateProject,
buildInvalidatedProjects,
buildDependentInvalidatedProjects,
resolveProjectName
}; };
function resetBuildContext(opts = defaultOptions) { function resetBuildContext(opts = defaultOptions) {
@ -486,13 +531,17 @@ namespace ts {
return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); return getUpToDateStatus(configFileCache.parseConfigFile(configFileName));
} }
function getBuildGraph(configFileNames: string[]) { function getBuildGraph(configFileNames: ReadonlyArray<string>) {
const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames);
if (resolvedNames === undefined) return; if (resolvedNames === undefined) return undefined;
return createDependencyGraph(resolvedNames); return createDependencyGraph(resolvedNames);
} }
function getGlobalDependencyGraph() {
return getBuildGraph(rootNames);
}
function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus {
if (project === undefined) { if (project === undefined) {
return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" };
@ -507,6 +556,73 @@ namespace ts {
return actual; return actual;
} }
function invalidateProject(configFileName: string) {
const resolved = resolveProjectName(configFileName);
if (resolved === undefined) {
// If this was a rootName, we need to track it as missing.
// Otherwise we can just ignore it and have it possibly surface as an error in any downstream projects,
// if they exist
// TODO: do those things
return;
}
configFileCache.removeKey(resolved);
context.invalidatedProjects.setValue(resolved, true);
context.projectStatus.removeKey(resolved);
const graph = getGlobalDependencyGraph()!;
if (graph) {
queueBuildForDownstreamReferences(resolved);
}
// Mark all downstream projects of this one needing to be built "later"
function queueBuildForDownstreamReferences(root: ResolvedConfigFileName) {
debugger;
const deps = graph.dependencyMap.getReferencesTo(root);
for (const ref of deps) {
// Can skip circular references
if (!context.queuedProjects.hasKey(ref)) {
context.queuedProjects.setValue(ref, true);
queueBuildForDownstreamReferences(ref);
}
}
}
}
function buildInvalidatedProjects() {
buildSomeProjects(p => context.invalidatedProjects.hasKey(p));
}
function buildDependentInvalidatedProjects() {
buildSomeProjects(p => context.queuedProjects.hasKey(p));
}
function buildSomeProjects(predicate: (projName: ResolvedConfigFileName) => boolean) {
const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(rootNames);
if (resolvedNames === undefined) return;
const graph = createDependencyGraph(resolvedNames)!;
for (const next of graph.buildQueue) {
if (!predicate(next)) continue;
const resolved = resolveProjectName(next);
if (!resolved) continue; // ??
const proj = configFileCache.parseConfigFile(resolved);
if (!proj) continue; // ?
const status = getUpToDateStatus(proj);
reportProjectStatus(next, status);
if (status.type === UpToDateStatusType.UpstreamBlocked) {
context.verbose(Diagnostics.Skipping_build_of_project_0_because_its_upstream_project_1_has_errors, resolved, status.upstreamProjectName);
continue;
}
buildSingleProject(next);
}
}
function getAllProjectOutputs(project: ParsedCommandLine): ReadonlyArray<string> { function getAllProjectOutputs(project: ParsedCommandLine): ReadonlyArray<string> {
if (project.options.outFile) { if (project.options.outFile) {
return getOutFileOutputs(project); return getOutFileOutputs(project);
@ -824,7 +940,7 @@ namespace ts {
context.projectStatus.setValue(proj.options.configFilePath!, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); context.projectStatus.setValue(proj.options.configFilePath!, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus);
} }
function getFilesToClean(configFileNames: ResolvedConfigFileName[]): string[] | undefined { function getFilesToClean(configFileNames: ReadonlyArray<ResolvedConfigFileName>): string[] | undefined {
const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames);
if (resolvedNames === undefined) return undefined; if (resolvedNames === undefined) return undefined;
@ -849,12 +965,24 @@ namespace ts {
return filesToDelete; return filesToDelete;
} }
function cleanProjects(configFileNames: string[]) { function getAllProjectsInScope(): ReadonlyArray<ResolvedConfigFileName> | undefined {
const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); const resolvedNames = resolveProjectNames(rootNames);
if (resolvedNames === undefined) return; if (resolvedNames === undefined) return undefined;
const graph = createDependencyGraph(resolvedNames);
if (graph === undefined) return undefined;
return graph.buildQueue;
}
function cleanAllProjects() {
const resolvedNames: ReadonlyArray<ResolvedConfigFileName> | undefined = getAllProjectsInScope();
if (resolvedNames === undefined) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located));
return;
}
const filesToDelete = getFilesToClean(resolvedNames); const filesToDelete = getFilesToClean(resolvedNames);
if (filesToDelete === undefined) { if (filesToDelete === undefined) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located));
return; return;
} }
@ -885,7 +1013,7 @@ namespace ts {
return undefined; return undefined;
} }
function resolveProjectNames(configFileNames: string[]): ResolvedConfigFileName[] | undefined { function resolveProjectNames(configFileNames: ReadonlyArray<string>): ResolvedConfigFileName[] | undefined {
const resolvedNames: ResolvedConfigFileName[] = []; const resolvedNames: ResolvedConfigFileName[] = [];
for (const name of configFileNames) { for (const name of configFileNames) {
const resolved = resolveProjectName(name); const resolved = resolveProjectName(name);
@ -897,12 +1025,8 @@ namespace ts {
return resolvedNames; return resolvedNames;
} }
function buildProjects(configFileNames: string[]) { function buildAllProjects() {
const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames); const graph = getGlobalDependencyGraph();
if (resolvedNames === undefined) return;
// Establish what needs to be built
const graph = createDependencyGraph(resolvedNames);
if (graph === undefined) return; if (graph === undefined) return;
const queue = graph.buildQueue; const queue = graph.buildQueue;

View file

@ -19,11 +19,10 @@ namespace ts {
it("can build the sample project 'sample1' without error", () => { it("can build the sample project 'sample1' without error", () => {
const fs = bfs.shadow(); const fs = bfs.shadow();
const host = new fakes.CompilerHost(fs); const host = new fakes.CompilerHost(fs);
const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false });
clearDiagnostics(); clearDiagnostics();
fs.chdir("/src/tests"); builder.buildAllProjects();
builder.buildProjects(["."]);
assertDiagnosticMessages(/*empty*/); assertDiagnosticMessages(/*empty*/);
// Check for outputs. Not an exhaustive list // Check for outputs. Not an exhaustive list
@ -38,9 +37,8 @@ namespace ts {
clearDiagnostics(); clearDiagnostics();
const fs = bfs.shadow(); const fs = bfs.shadow();
const host = new fakes.CompilerHost(fs); const host = new fakes.CompilerHost(fs);
const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false });
fs.chdir("/src/tests"); builder.buildAllProjects();
builder.buildProjects(["."]);
assertDiagnosticMessages(Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0, Diagnostics.Would_build_project_0); 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 // Check for outputs to not be written. Not an exhaustive list
@ -54,14 +52,13 @@ namespace ts {
const fs = bfs.shadow(); const fs = bfs.shadow();
const host = new fakes.CompilerHost(fs); const host = new fakes.CompilerHost(fs);
let builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); let builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false });
fs.chdir("/src/tests"); builder.buildAllProjects();
builder.buildProjects(["."]);
tick(); tick();
clearDiagnostics(); clearDiagnostics();
builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: true, force: false, verbose: false });
builder.buildProjects(["."]); builder.buildAllProjects();
assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date);
}); });
}); });
@ -72,20 +69,19 @@ namespace ts {
const fs = bfs.shadow(); const fs = bfs.shadow();
const host = new fakes.CompilerHost(fs); const host = new fakes.CompilerHost(fs);
const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: false }); const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false });
fs.chdir("/src/tests"); builder.buildAllProjects();
builder.buildProjects(["."]);
// Verify they exist // Verify they exist
for (const output of allExpectedOutputs) { for (const output of allExpectedOutputs) {
assert(fs.existsSync(output), `Expect file ${output} to exist`); assert(fs.existsSync(output), `Expect file ${output} to exist`);
} }
builder.cleanProjects(["."]); builder.cleanAllProjects();
// Verify they are gone // Verify they are gone
for (const output of allExpectedOutputs) { for (const output of allExpectedOutputs) {
assert(!fs.existsSync(output), `Expect file ${output} to not exist`); assert(!fs.existsSync(output), `Expect file ${output} to not exist`);
} }
// Subsequent clean shouldn't throw / etc // Subsequent clean shouldn't throw / etc
builder.cleanProjects(["."]); builder.cleanAllProjects();
}); });
}); });
@ -94,16 +90,15 @@ namespace ts {
const fs = bfs.shadow(); const fs = bfs.shadow();
const host = new fakes.CompilerHost(fs); const host = new fakes.CompilerHost(fs);
const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: true, verbose: false }); const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: true, verbose: false });
fs.chdir("/src/tests"); builder.buildAllProjects();
builder.buildProjects(["."]);
let currentTime = time(); let currentTime = time();
checkOutputTimestamps(currentTime); checkOutputTimestamps(currentTime);
tick(); tick();
Debug.assert(time() !== currentTime, "Time moves on"); Debug.assert(time() !== currentTime, "Time moves on");
currentTime = time(); currentTime = time();
builder.buildProjects(["."]); builder.buildAllProjects();
checkOutputTimestamps(currentTime); checkOutputTimestamps(currentTime);
function checkOutputTimestamps(expected: number) { function checkOutputTimestamps(expected: number) {
@ -119,14 +114,12 @@ namespace ts {
describe("tsbuild - can detect when and what to rebuild", () => { describe("tsbuild - can detect when and what to rebuild", () => {
const fs = bfs.shadow(); const fs = bfs.shadow();
const host = new fakes.CompilerHost(fs); const host = new fakes.CompilerHost(fs);
const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true });
fs.chdir("/src/tests");
it("Builds the project", () => { it("Builds the project", () => {
clearDiagnostics(); clearDiagnostics();
builder.resetBuildContext(); builder.resetBuildContext();
builder.buildProjects(["."]); builder.buildAllProjects();
assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0,
Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist,
Diagnostics.Building_project_0, Diagnostics.Building_project_0,
@ -141,7 +134,7 @@ namespace ts {
it("Detects that all projects are up to date", () => { it("Detects that all projects are up to date", () => {
clearDiagnostics(); clearDiagnostics();
builder.resetBuildContext(); builder.resetBuildContext();
builder.buildProjects(["."]); builder.buildAllProjects();
assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0,
Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2,
Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2,
@ -154,7 +147,7 @@ namespace ts {
clearDiagnostics(); clearDiagnostics();
fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); fs.writeFileSync("/src/tests/index.ts", "const m = 10;");
builder.resetBuildContext(); builder.resetBuildContext();
builder.buildProjects(["."]); builder.buildAllProjects();
assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0,
Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2,
@ -169,7 +162,7 @@ namespace ts {
clearDiagnostics(); clearDiagnostics();
replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET");
builder.resetBuildContext(); builder.resetBuildContext();
builder.buildProjects(["."]); builder.buildAllProjects();
assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0, assertDiagnosticMessages(Diagnostics.Sorted_list_of_input_projects_Colon_0,
Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2,
@ -185,14 +178,13 @@ namespace ts {
it("won't build downstream projects if upstream projects have errors", () => { it("won't build downstream projects if upstream projects have errors", () => {
const fs = bfs.shadow(); const fs = bfs.shadow();
const host = new fakes.CompilerHost(fs); const host = new fakes.CompilerHost(fs);
const builder = createSolutionBuilder(host, reportDiagnostic, { dry: false, force: false, verbose: true }); const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: true });
clearDiagnostics(); clearDiagnostics();
// Induce an error in the middle project // Induce an error in the middle project
replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`);
fs.chdir("/src/tests"); builder.buildAllProjects();
builder.buildProjects(["."]);
assertDiagnosticMessages( assertDiagnosticMessages(
Diagnostics.Sorted_list_of_input_projects_Colon_0, Diagnostics.Sorted_list_of_input_projects_Colon_0,
Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist,
@ -221,8 +213,6 @@ namespace ts {
writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps);
const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false });
it("orders the graph correctly - specify two roots", () => { it("orders the graph correctly - specify two roots", () => {
checkGraphOrdering(["A", "G"], ["A", "B", "C", "D", "E", "G"]); checkGraphOrdering(["A", "G"], ["A", "B", "C", "D", "E", "G"]);
}); });
@ -240,6 +230,8 @@ namespace ts {
}); });
function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) {
const builder = createSolutionBuilder(host, rootNames, reportDiagnostic, { dry: true, force: false, verbose: false });
const projFileNames = rootNames.map(getProjectFileName); const projFileNames = rootNames.map(getProjectFileName);
const graph = builder.getBuildGraph(projFileNames); const graph = builder.getBuildGraph(projFileNames);
if (graph === undefined) throw new Error("Graph shouldn't be undefined"); if (graph === undefined) throw new Error("Graph shouldn't be undefined");
@ -280,6 +272,39 @@ namespace ts {
} }
}); });
describe("tsbuild - project invalidation", () => {
it ("invalidates projects correctly", () => {
const fs = bfs.shadow();
const host = new fakes.CompilerHost(fs);
const builder = createSolutionBuilder(host, ["/src/tests"], reportDiagnostic, { dry: false, force: false, verbose: false });
clearDiagnostics();
builder.buildAllProjects();
assertDiagnosticMessages(/*empty*/);
// Update a timestamp in the middle project
tick();
touch(fs, "/src/logic/index.ts");
// 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");
// Rebuild this project
tick();
builder.invalidateProject("/src/logic");
builder.buildInvalidatedProjects();
// The file should be updated
assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt");
assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt");
// Build downstream projects should update 'tests', but not 'core'
tick();
builder.buildDependentInvalidatedProjects();
assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt");
assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt");
});
});
function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) {
if (!fs.statSync(path).isFile()) { if (!fs.statSync(path).isFile()) {
throw new Error(`File ${path} does not exist`); throw new Error(`File ${path} does not exist`);
@ -324,6 +349,13 @@ namespace ts {
return currentTime; 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 loadFsMirror(vfs: vfs.FileSystem, localRoot: string, virtualRoot: string) { function loadFsMirror(vfs: vfs.FileSystem, localRoot: string, virtualRoot: string) {
vfs.mkdirpSync(virtualRoot); vfs.mkdirpSync(virtualRoot);
for (const path of Harness.IO.readDirectory(localRoot)) { for (const path of Harness.IO.readDirectory(localRoot)) {