Skip to content

Commit 311204a

Browse files
AgentEnderFrozenPandaz
authored andcommitted
fix(core): show better project graph errors (#29525)
## Current Behavior Sub-errors are hidden when any project graph error is encountered. This is detrimental, as things like "missing comma in JSON" get hidden and make people think that Nx is broken, when in fact their config files are invalid. ## Expected Behavior Sub errors are shown regardless of verbose logging (but including their stack trace if verbose logging is enabled) ### Without Verbose ![image](https://github.com/user-attachments/assets/3a96d07e-3f0a-4eb7-8629-0c02c6912746) ### With Verbose ![image](https://github.com/user-attachments/assets/41b83e19-e6b1-471c-80ca-004b8f56d8f2) ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes # (cherry picked from commit c060b3a)
1 parent bd35bfe commit 311204a

File tree

5 files changed

+95
-54
lines changed

5 files changed

+95
-54
lines changed

packages/nx/src/project-graph/error-types.ts

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,11 @@ import { ProjectGraph } from '../config/project-graph';
77
import { CreateNodesFunctionV2 } from './plugins/public-api';
88

99
export class ProjectGraphError extends Error {
10-
readonly #errors: Array<
11-
| AggregateCreateNodesError
12-
| MergeNodesError
13-
| CreateMetadataError
14-
| ProjectsWithNoNameError
15-
| MultipleProjectsWithSameNameError
16-
| ProcessDependenciesError
17-
| WorkspaceValidityError
18-
>;
1910
readonly #partialProjectGraph: ProjectGraph;
2011
readonly #partialSourceMaps: ConfigurationSourceMaps;
2112

2213
constructor(
23-
errors: Array<
14+
private readonly errors: Array<
2415
| AggregateCreateNodesError
2516
| MergeNodesError
2617
| ProjectsWithNoNameError
@@ -32,16 +23,46 @@ export class ProjectGraphError extends Error {
3223
partialProjectGraph: ProjectGraph,
3324
partialSourceMaps: ConfigurationSourceMaps
3425
) {
35-
super(
36-
`Failed to process project graph. Run "nx reset" to fix this. Please report the issue if you keep seeing it.`
37-
);
26+
const messageFragments = ['Failed to process project graph.'];
27+
const mergeNodesErrors = [];
28+
const unknownErrors = [];
29+
for (const e of errors) {
30+
if (
31+
// Known errors that are self-explanatory
32+
isAggregateCreateNodesError(e) ||
33+
isCreateMetadataError(e) ||
34+
isProcessDependenciesError(e) ||
35+
isProjectsWithNoNameError(e) ||
36+
isMultipleProjectsWithSameNameError(e) ||
37+
isWorkspaceValidityError(e)
38+
) {
39+
} else if (
40+
// Known error type, but unlikely to be caused by the user
41+
isMergeNodesError(e)
42+
) {
43+
mergeNodesErrors.push(e);
44+
} else {
45+
unknownErrors.push(e);
46+
}
47+
}
48+
if (mergeNodesErrors.length > 0) {
49+
messageFragments.push(
50+
`This type of error most likely points to an issue within Nx. Please report it.`
51+
);
52+
}
53+
if (unknownErrors.length > 0) {
54+
messageFragments.push(
55+
`If the error cause is not obvious from the below error messages, running "nx reset" may fix it. Please report the issue if you keep seeing it.`
56+
);
57+
}
58+
super(messageFragments.join(' '));
3859
this.name = this.constructor.name;
39-
this.#errors = errors;
60+
this.errors = errors;
4061
this.#partialProjectGraph = partialProjectGraph;
4162
this.#partialSourceMaps = partialSourceMaps;
42-
this.stack = `${this.message}\n ${errors
63+
this.stack = errors
4364
.map((error) => indentString(formatErrorStackAndCause(error), 2))
44-
.join('\n')}`;
65+
.join('\n');
4566
}
4667

4768
/**
@@ -67,7 +88,7 @@ export class ProjectGraphError extends Error {
6788
}
6889

6990
getErrors() {
70-
return this.#errors;
91+
return this.errors;
7192
}
7293
}
7394

@@ -242,6 +263,36 @@ export class AggregateCreateNodesError extends Error {
242263
}
243264
}
244265

266+
export function formatAggregateCreateNodesError(
267+
error: AggregateCreateNodesError,
268+
pluginName: string
269+
) {
270+
const errorBodyLines = [
271+
`${
272+
error.errors.length > 1 ? `${error.errors.length} errors` : 'An error'
273+
} occurred while processing files for the ${pluginName} plugin.`,
274+
];
275+
const errorStackLines = [];
276+
277+
const innerErrors = error.errors;
278+
for (const [file, e] of innerErrors) {
279+
if (file) {
280+
errorBodyLines.push(` - ${file}: ${e.message}`);
281+
errorStackLines.push(` - ${file}: ${e.stack}`);
282+
} else {
283+
errorBodyLines.push(` - ${e.message}`);
284+
errorStackLines.push(` - ${e.stack}`);
285+
}
286+
if (e.stack && process.env.NX_VERBOSE_LOGGING === 'true') {
287+
const innerStackTrace = ' ' + e.stack.split('\n')?.join('\n ');
288+
errorStackLines.push(innerStackTrace);
289+
}
290+
}
291+
292+
error.stack = errorStackLines.join('\n');
293+
error.message = errorBodyLines.join('\n');
294+
}
295+
245296
export class MergeNodesError extends Error {
246297
file: string;
247298
pluginName: string;
@@ -292,6 +343,16 @@ export class ProcessDependenciesError extends Error {
292343
this.stack = `${this.message}\n ${cause.stack.split('\n').join('\n ')}`;
293344
}
294345
}
346+
347+
function isProcessDependenciesError(e: unknown): e is ProcessDependenciesError {
348+
return (
349+
e instanceof ProcessDependenciesError ||
350+
(typeof e === 'object' &&
351+
'name' in e &&
352+
e?.name === ProcessDependenciesError.name)
353+
);
354+
}
355+
295356
export class WorkspaceValidityError extends Error {
296357
constructor(public message: string) {
297358
message = `Configuration Error\n${message}`;

packages/nx/src/project-graph/project-graph.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,6 @@ export function handleProjectGraphError(opts: { exitOnError: boolean }, e) {
178178
const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true';
179179
if (e instanceof ProjectGraphError) {
180180
let title = e.message;
181-
if (isVerbose) {
182-
title += ' See errors below.';
183-
}
184181

185182
const bodyLines = isVerbose
186183
? [e.stack]

packages/nx/src/project-graph/utils/project-configuration-utils.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
isProjectWithNoNameError,
2929
isAggregateCreateNodesError,
3030
AggregateCreateNodesError,
31+
formatAggregateCreateNodesError,
3132
} from '../error-types';
3233
import { CreateNodesResult } from '../plugins/public-api';
3334
import { isGlobPattern } from '../../utils/globs';
@@ -392,31 +393,12 @@ export async function createProjectConfigurations(
392393
workspaceRoot: root,
393394
})
394395
.catch((e: Error) => {
395-
const errorBodyLines = [
396-
`An error occurred while processing files for the ${pluginName} plugin.`,
397-
];
398396
const error: AggregateCreateNodesError = isAggregateCreateNodesError(e)
399397
? // This is an expected error if something goes wrong while processing files.
400398
e
401399
: // This represents a single plugin erroring out with a hard error.
402400
new AggregateCreateNodesError([[null, e]], []);
403-
404-
const innerErrors = error.errors;
405-
for (const [file, e] of innerErrors) {
406-
if (file) {
407-
errorBodyLines.push(` - ${file}: ${e.message}`);
408-
} else {
409-
errorBodyLines.push(` - ${e.message}`);
410-
}
411-
if (e.stack) {
412-
const innerStackTrace =
413-
' ' + e.stack.split('\n')?.join('\n ');
414-
errorBodyLines.push(innerStackTrace);
415-
}
416-
}
417-
418-
error.stack = errorBodyLines.join('\n');
419-
401+
formatAggregateCreateNodesError(error, pluginName);
420402
// This represents a single plugin erroring out with a hard error.
421403
errors.push(error);
422404
// The plugin didn't return partial results, so we return an empty array.

packages/nx/src/utils/handle-errors.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ describe('handleErrors', () => {
2525
const body = bodyLines.join('\n');
2626
expect(body).toContain('cause message');
2727
expect(body).toContain('test-plugin');
28+
// --verbose is active, so we should see the stack trace
29+
expect(body).toMatch(/\s+at.*handle-errors.spec.ts/);
2830
});
2931

30-
it('should only display wrapper error if not verbose', async () => {
32+
it('should not display stack trace if not verbose', async () => {
3133
const spy = jest.spyOn(output, 'error').mockImplementation(() => {});
3234
await handleErrors(false, async () => {
3335
const cause = new Error('cause message');
@@ -41,7 +43,8 @@ describe('handleErrors', () => {
4143

4244
const { bodyLines, title } = spy.mock.calls[0][0];
4345
const body = bodyLines.join('\n');
44-
expect(body).not.toContain('cause message');
46+
expect(body).toContain('cause message');
47+
expect(body).not.toMatch(/\s+at.*handle-errors.spec.ts/);
4548
});
4649

4750
it('should display misc errors that do not have a cause', async () => {

packages/nx/src/utils/handle-errors.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,18 @@ export async function handleErrors(
2626
) {
2727
title += ' ' + projectGraphError.cause.message + '.';
2828
}
29-
if (isVerbose) {
30-
title += ' See errors below.';
31-
}
32-
33-
const bodyLines = isVerbose
34-
? formatErrorStackAndCause(projectGraphError)
35-
: ['Pass --verbose to see the stacktraces.'];
3629

3730
output.error({
3831
title,
39-
bodyLines: bodyLines,
32+
bodyLines: isVerbose
33+
? formatErrorStackAndCause(projectGraphError, isVerbose)
34+
: projectGraphError.getErrors().map((e) => e.message),
4035
});
4136
} else {
4237
const lines = (err.message ? err.message : err.toString()).split('\n');
4338
const bodyLines: string[] = lines.slice(1);
4439
if (isVerbose) {
45-
bodyLines.push(...formatErrorStackAndCause(err));
40+
bodyLines.push(...formatErrorStackAndCause(err, isVerbose));
4641
} else if (err.stack) {
4742
bodyLines.push('Pass --verbose to see the stacktrace.');
4843
}
@@ -59,13 +54,16 @@ export async function handleErrors(
5954
}
6055
}
6156

62-
function formatErrorStackAndCause<T extends Error>(error: T): string[] {
57+
function formatErrorStackAndCause<T extends Error>(
58+
error: T,
59+
verbose: boolean
60+
): string[] {
6361
return [
64-
error.stack || error.message,
62+
verbose ? error.stack || error.message : error.message,
6563
...(error.cause && typeof error.cause === 'object'
6664
? [
6765
'Caused by:',
68-
'stack' in error.cause
66+
verbose && 'stack' in error.cause
6967
? error.cause.stack.toString()
7068
: error.cause.toString(),
7169
]

0 commit comments

Comments
 (0)