Add option --stopBuildOnErrors to tsbuild to get previous behavior of not building downstream projects if upstream has errors (#59433)

This commit is contained in:
Sheetal Nandi
2024-08-16 19:02:42 -07:00
committed by GitHub
parent f850298f70
commit d06fb82d7c
13 changed files with 3702 additions and 9 deletions
+7
View File
@@ -1696,6 +1696,13 @@ export const optionsForBuild: CommandLineOption[] = [
type: "boolean",
defaultValueDescription: false,
},
{
name: "stopBuildOnErrors",
category: Diagnostics.Command_line_Options,
description: Diagnostics.Skip_building_downstream_projects_on_error_in_upstream_project,
type: "boolean",
defaultValueDescription: false,
},
];
/** @internal */
+20
View File
@@ -5716,6 +5716,14 @@
"category": "Message",
"code": 6361
},
"Skipping build of project '{0}' because its dependency '{1}' has errors": {
"category": "Message",
"code": 6362
},
"Project '{0}' can't be built because its dependency '{1}' has errors": {
"category": "Message",
"code": 6363
},
"Build one or more projects and their dependencies, if out of date": {
"category": "Message",
"code": 6364
@@ -5760,6 +5768,14 @@
"category": "Message",
"code": 6381
},
"Skipping build of project '{0}' because its dependency '{1}' was not built": {
"category": "Message",
"code": 6382
},
"Project '{0}' can't be built because its dependency '{1}' was not built": {
"category": "Message",
"code": 6383
},
"Have recompiles in '--incremental' and '--watch' assume that changes within a file will only affect files directly depending on it.": {
"category": "Message",
"code": 6384
@@ -6095,6 +6111,10 @@
"category": "Message",
"code": 6639
},
"Skip building downstream projects on error in upstream project.": {
"category": "Message",
"code": 6640
},
"Specify a list of glob patterns that match files to be included in compilation.": {
"category": "Message",
"code": 6641
+11
View File
@@ -25,6 +25,7 @@ export enum UpToDateStatusType {
OutOfDateOptions,
OutOfDateRoots,
UpstreamOutOfDate,
UpstreamBlocked,
ComputingUpstream,
TsVersionOutputOfDate,
UpToDateWithInputFileText,
@@ -47,6 +48,7 @@ export type UpToDateStatus =
| Status.OutOfDateBuildInfo
| Status.OutOfDateRoots
| Status.UpstreamOutOfDate
| Status.UpstreamBlocked
| Status.ComputingUpstream
| Status.TsVersionOutOfDate
| Status.ContainerOnly
@@ -135,6 +137,15 @@ export namespace Status {
upstreamProjectName: string;
}
/**
* This project depends an upstream project with build errors
*/
export interface UpstreamBlocked {
type: UpToDateStatusType.UpstreamBlocked;
upstreamProjectName: string;
upstreamProjectBlocked: boolean;
}
/**
* Computing status of upstream projects referenced
*/
+49
View File
@@ -135,6 +135,7 @@ export interface BuildOptions {
dry?: boolean;
force?: boolean;
verbose?: boolean;
stopBuildOnErrors?: boolean;
/** @internal */ clean?: boolean;
/** @internal */ watch?: boolean;
@@ -1254,6 +1255,23 @@ function getNextInvalidatedProjectCreateInfo<T extends BuilderProgram>(
}
}
if (status.type === UpToDateStatusType.UpstreamBlocked) {
verboseReportProjectStatus(state, project, status);
reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config));
projectPendingBuild.delete(projectPath);
if (options.verbose) {
reportStatus(
state,
status.upstreamProjectBlocked ?
Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_was_not_built :
Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors,
project,
status.upstreamProjectName,
);
}
continue;
}
if (status.type === UpToDateStatusType.ContainerOnly) {
verboseReportProjectStatus(state, project, status);
reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config));
@@ -1455,6 +1473,20 @@ function getUpToDateStatusWorker<T extends BuilderProgram>(state: SolutionBuilde
continue;
}
// An upstream project is blocked
if (
state.options.stopBuildOnErrors && (
refStatus.type === UpToDateStatusType.Unbuildable ||
refStatus.type === UpToDateStatusType.UpstreamBlocked
)
) {
return {
type: UpToDateStatusType.UpstreamBlocked,
upstreamProjectName: ref.path,
upstreamProjectBlocked: refStatus.type === UpToDateStatusType.UpstreamBlocked,
};
}
if (!force) (referenceStatuses ||= []).push({ ref, refStatus, resolvedRefPath, resolvedConfig });
}
}
@@ -1848,6 +1880,8 @@ function queueReferencingProjects<T extends BuilderProgram>(
buildOrder: readonly ResolvedConfigFileName[],
buildResult: BuildResultFlags,
) {
// Queue only if there are no errors
if (state.options.stopBuildOnErrors && (buildResult & BuildResultFlags.AnyErrors)) return;
// Only composite projects can be referenced by other projects
if (!config.options.composite) return;
// Always use build order to queue projects
@@ -1883,6 +1917,12 @@ function queueReferencingProjects<T extends BuilderProgram>(
});
}
break;
case UpToDateStatusType.UpstreamBlocked:
if (toResolvedConfigFilePath(state, resolveProjectName(state, status.upstreamProjectName)) === projectPath) {
clearProjectStatus(state, nextProjectPath);
}
break;
}
}
addProjToQueue(state, nextProjectPath, ProgramUpdateLevel.Update);
@@ -2386,6 +2426,15 @@ function reportUpToDateStatus<T extends BuilderProgram>(state: SolutionBuilderSt
relName(state, configFileName),
relName(state, status.upstreamProjectName),
);
case UpToDateStatusType.UpstreamBlocked:
return reportStatus(
state,
status.upstreamProjectBlocked ?
Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_was_not_built :
Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors,
relName(state, configFileName),
relName(state, status.upstreamProjectName),
);
case UpToDateStatusType.Unbuildable:
return reportStatus(
state,
@@ -26,7 +26,7 @@ export function getFsContentsForSampleProjectReferencesLogicConfig(withNodeNext?
],
});
}
export function getFsContentsForSampleProjectReferences(withNodeNext?: boolean): FsContents {
export function getFsContentsForSampleProjectReferences(withNodeNext?: boolean, skipReferenceCoreFromTest?: boolean): FsContents {
return {
[libFile.path]: libFile.content,
"/user/username/projects/sample1/core/tsconfig.json": jsonToReadableText({
@@ -55,10 +55,14 @@ export function getFsContentsForSampleProjectReferences(withNodeNext?: boolean):
export const m = mod;
`,
"/user/username/projects/sample1/tests/tsconfig.json": jsonToReadableText({
references: [
{ path: "../core" },
{ path: "../logic" },
],
references: !skipReferenceCoreFromTest ?
[
{ path: "../core" },
{ path: "../logic" },
] :
[
{ path: "../logic" },
],
files: ["index.ts"],
compilerOptions: {
...getProjectConfigWithNodeNext(withNodeNext),
@@ -81,9 +85,9 @@ export function getFsContentsForSampleProjectReferences(withNodeNext?: boolean):
};
}
export function getFsForSampleProjectReferences() {
export function getFsForSampleProjectReferences(withNodeNext?: boolean, skipReferenceCoreFromTest?: boolean) {
return loadProjectFromFiles(
getFsContentsForSampleProjectReferences(),
getFsContentsForSampleProjectReferences(withNodeNext, skipReferenceCoreFromTest),
{
cwd: "/user/username/projects/sample1",
executingFilePath: libFile.path,
@@ -91,9 +95,9 @@ export function getFsForSampleProjectReferences() {
);
}
export function getSysForSampleProjectReferences(withNodeNext?: boolean) {
export function getSysForSampleProjectReferences(withNodeNext?: boolean, skipReferenceCoreFromTest?: boolean) {
return createWatchedSystem(
getFsContentsForSampleProjectReferences(withNodeNext),
getFsContentsForSampleProjectReferences(withNodeNext, skipReferenceCoreFromTest),
{
currentDirectory: "/user/username/projects/sample1",
},
@@ -359,6 +359,23 @@ describe("unittests:: tsbuild:: on 'sample1' project", () => {
modifyFs: fs => replaceText(fs, "logic/index.ts", "c.multiply(10, 15)", `c.muitply()`),
edits: noChangeOnlyRuns,
});
[false, true].forEach(skipReferenceCoreFromTest =>
verifyTsc({
scenario: "sample1",
subScenario: `skips builds downstream projects if upstream projects have errors with stopBuildOnErrors${skipReferenceCoreFromTest ? " when test does not reference core" : ""}`,
fs: () => getFsForSampleProjectReferences(/*withNodeNext*/ undefined, skipReferenceCoreFromTest),
commandLineArgs: ["--b", "tests", "--verbose", "--stopBuildOnErrors"],
modifyFs: fs => appendText(fs, "core/index.ts", `multiply();`),
edits: [
noChangeRun,
{
caption: "fix error",
edit: fs => replaceText(fs, "core/index.ts", "multiply();", ""),
},
],
})
);
});
describe("project invalidation", () => {
@@ -316,6 +316,28 @@ createSomeObject().message;`,
}
verifyIncrementalErrors("when preserveWatchOutput is not used", ts.emptyArray);
verifyIncrementalErrors("when preserveWatchOutput is passed on command line", ["--preserveWatchOutput"]);
verifyIncrementalErrors("when stopBuildOnErrors is passed on command line", ["--stopBuildOnErrors"]);
[false, true].forEach(skipReferenceCoreFromTest =>
verifyTscWatch({
scenario: "programUpdates",
subScenario: `skips builds downstream projects if upstream projects have errors with stopBuildOnErrors${skipReferenceCoreFromTest ? " when test does not reference core" : ""}`,
sys: () => {
const sys = getSysForSampleProjectReferences(/*withNodeNext*/ undefined, skipReferenceCoreFromTest);
sys.appendFile("core/index.ts", `multiply();`);
return sys;
},
commandLineArgs: ["--b", "-w", "tests", "--verbose", "--stopBuildOnErrors"],
edits: [{
caption: "fix error",
edit: sys => sys.replaceFileText("core/index.ts", "multiply();", ""),
timeouts: sys => {
sys.runQueuedTimeoutCallbacks();
sys.runQueuedTimeoutCallbacks();
},
}],
})
);
describe("when declaration emit errors are present", () => {
const solution = "solution";