Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
20 changes: 20 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/compiler/tsbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export enum UpToDateStatusType {
OutOfDateOptions,
OutOfDateRoots,
UpstreamOutOfDate,
UpstreamBlocked,
ComputingUpstream,
TsVersionOutputOfDate,
UpToDateWithInputFileText,
Expand All @@ -47,6 +48,7 @@ export type UpToDateStatus =
| Status.OutOfDateBuildInfo
| Status.OutOfDateRoots
| Status.UpstreamOutOfDate
| Status.UpstreamBlocked
| Status.ComputingUpstream
| Status.TsVersionOutOfDate
| Status.ContainerOnly
Expand Down Expand Up @@ -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
*/
Expand Down
49 changes: 49 additions & 0 deletions src/compiler/tsbuildPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export interface BuildOptions {
dry?: boolean;
force?: boolean;
verbose?: boolean;
stopBuildOnErrors?: boolean;

/** @internal */ clean?: boolean;
/** @internal */ watch?: boolean;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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 });
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 13 additions & 9 deletions src/testRunner/unittests/helpers/sampleProjectReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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),
Expand All @@ -81,19 +85,19 @@ 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,
},
);
}

export function getSysForSampleProjectReferences(withNodeNext?: boolean) {
export function getSysForSampleProjectReferences(withNodeNext?: boolean, skipReferenceCoreFromTest?: boolean) {
return createWatchedSystem(
getFsContentsForSampleProjectReferences(withNodeNext),
getFsContentsForSampleProjectReferences(withNodeNext, skipReferenceCoreFromTest),
{
currentDirectory: "/user/username/projects/sample1",
},
Expand Down
17 changes: 17 additions & 0 deletions src/testRunner/unittests/tsbuild/sample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
22 changes: 22 additions & 0 deletions src/testRunner/unittests/tsbuildWatch/programUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9802,6 +9802,7 @@ declare namespace ts {
dry?: boolean;
force?: boolean;
verbose?: boolean;
stopBuildOnErrors?: boolean;
incremental?: boolean;
assumeChangesOnlyAffectDirectDependencies?: boolean;
declaration?: boolean;
Expand Down
Loading