diff --git a/packages/nx/src/command-line/add/add.ts b/packages/nx/src/command-line/add/add.ts index 82e829f291fa0..9cf31c4d35ab1 100644 --- a/packages/nx/src/command-line/add/add.ts +++ b/packages/nx/src/command-line/add/add.ts @@ -9,7 +9,7 @@ import { writeJsonFile } from '../../utils/fileutils'; import { logger } from '../../utils/logger'; import { output } from '../../utils/output'; import { getPackageManagerCommand } from '../../utils/package-manager'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import { getPluginCapabilities } from '../../utils/plugins'; import { nxVersion } from '../../utils/versions'; import { workspaceRoot } from '../../utils/workspace-root'; diff --git a/packages/nx/src/command-line/affected/command-object.ts b/packages/nx/src/command-line/affected/command-object.ts index 98133dba3dedf..389186cab7f57 100644 --- a/packages/nx/src/command-line/affected/command-object.ts +++ b/packages/nx/src/command-line/affected/command-object.ts @@ -9,7 +9,7 @@ import { withRunOptions, withTargetAndConfigurationOption, } from '../yargs-utils/shared-options'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; export const yargsAffectedCommand: CommandModule = { command: 'affected', diff --git a/packages/nx/src/command-line/deprecated/command-objects.ts b/packages/nx/src/command-line/deprecated/command-objects.ts index 03fa3d317aa11..922b63734c19d 100644 --- a/packages/nx/src/command-line/deprecated/command-objects.ts +++ b/packages/nx/src/command-line/deprecated/command-objects.ts @@ -1,5 +1,5 @@ import { CommandModule } from 'yargs'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import { withAffectedOptions, withTargetAndConfigurationOption, diff --git a/packages/nx/src/command-line/generate/generate.ts b/packages/nx/src/command-line/generate/generate.ts index 65d4c3eeb533f..77d1777327e74 100644 --- a/packages/nx/src/command-line/generate/generate.ts +++ b/packages/nx/src/command-line/generate/generate.ts @@ -12,14 +12,13 @@ import { import { logger, NX_PREFIX } from '../../utils/logger'; import { combineOptionsForGenerator, - handleErrors, Options, Schema, } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import { getLocalWorkspacePlugins } from '../../utils/plugins/local-plugins'; import { printHelp } from '../../utils/print-help'; import { workspaceRoot } from '../../utils/workspace-root'; -import { NxJsonConfiguration } from '../../config/nx-json'; import { calculateDefaultProjectName } from '../../config/calculate-default-project-name'; import { findInstalledPlugins } from '../../utils/plugins/installed-plugins'; import { getGeneratorInformation } from './generator-utils'; diff --git a/packages/nx/src/command-line/import/command-object.ts b/packages/nx/src/command-line/import/command-object.ts index 10e3395fab347..c639f6354da30 100644 --- a/packages/nx/src/command-line/import/command-object.ts +++ b/packages/nx/src/command-line/import/command-object.ts @@ -1,7 +1,7 @@ import { CommandModule } from 'yargs'; import { linkToNxDevAndExamples } from '../yargs-utils/documentation'; import { withVerbose } from '../yargs-utils/shared-options'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; export const yargsImportCommand: CommandModule = { command: 'import [sourceRepository] [destinationDirectory]', diff --git a/packages/nx/src/command-line/login/login.ts b/packages/nx/src/command-line/login/login.ts index 9404f9a86769b..6b8c74ef51ec1 100644 --- a/packages/nx/src/command-line/login/login.ts +++ b/packages/nx/src/command-line/login/login.ts @@ -1,6 +1,6 @@ import { verifyOrUpdateNxCloudClient } from '../../nx-cloud/update-manager'; import { getCloudOptions } from '../../nx-cloud/utilities/get-cloud-options'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; export interface LoginArgs { nxCloudUrl?: string; diff --git a/packages/nx/src/command-line/logout/logout.ts b/packages/nx/src/command-line/logout/logout.ts index 685fb9b57e89c..1e5b49035dcd9 100644 --- a/packages/nx/src/command-line/logout/logout.ts +++ b/packages/nx/src/command-line/logout/logout.ts @@ -1,6 +1,6 @@ import { verifyOrUpdateNxCloudClient } from '../../nx-cloud/update-manager'; import { getCloudOptions } from '../../nx-cloud/utilities/get-cloud-options'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; export interface LogoutArgs { verbose?: boolean; diff --git a/packages/nx/src/command-line/migrate/migrate.ts b/packages/nx/src/command-line/migrate/migrate.ts index c7f3568b42ebe..c365c1d9f75b5 100644 --- a/packages/nx/src/command-line/migrate/migrate.ts +++ b/packages/nx/src/command-line/migrate/migrate.ts @@ -49,7 +49,7 @@ import { packageRegistryView, resolvePackageVersionUsingRegistry, } from '../../utils/package-manager'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import { connectToNxCloudWithPrompt, onlyDefaultRunnerIsUsed, diff --git a/packages/nx/src/command-line/new/new.ts b/packages/nx/src/command-line/new/new.ts index e4251ba1a4bb9..d926a0a377073 100644 --- a/packages/nx/src/command-line/new/new.ts +++ b/packages/nx/src/command-line/new/new.ts @@ -1,5 +1,6 @@ import { flushChanges, FsTree } from '../../generators/tree'; -import { combineOptionsForGenerator, handleErrors } from '../../utils/params'; +import { combineOptionsForGenerator } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import { getGeneratorInformation } from '../generate/generator-utils'; function removeSpecialFlags(generatorOptions: { [p: string]: any }): void { diff --git a/packages/nx/src/command-line/release/changelog.ts b/packages/nx/src/command-line/release/changelog.ts index e735c957c3bee..0fb524f6e0e38 100644 --- a/packages/nx/src/command-line/release/changelog.ts +++ b/packages/nx/src/command-line/release/changelog.ts @@ -25,7 +25,7 @@ import { createProjectGraphAsync } from '../../project-graph/project-graph'; import { interpolate } from '../../tasks-runner/utils'; import { isCI } from '../../utils/is-ci'; import { output } from '../../utils/output'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import { joinPathFragments } from '../../utils/path'; import { workspaceRoot } from '../../utils/workspace-root'; import { ChangelogOptions } from './command-object'; diff --git a/packages/nx/src/command-line/release/plan-check.ts b/packages/nx/src/command-line/release/plan-check.ts index ab9b2b869558e..ac112adacab8b 100644 --- a/packages/nx/src/command-line/release/plan-check.ts +++ b/packages/nx/src/command-line/release/plan-check.ts @@ -7,7 +7,7 @@ import { splitArgsIntoNxArgsAndOverrides, } from '../../utils/command-line-utils'; import { output } from '../../utils/output'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import { PlanCheckOptions, PlanOptions } from './command-object'; import { createNxReleaseConfig, diff --git a/packages/nx/src/command-line/release/plan.ts b/packages/nx/src/command-line/release/plan.ts index a6cec478741ca..35432497f9a58 100644 --- a/packages/nx/src/command-line/release/plan.ts +++ b/packages/nx/src/command-line/release/plan.ts @@ -12,7 +12,7 @@ import { splitArgsIntoNxArgsAndOverrides, } from '../../utils/command-line-utils'; import { output } from '../../utils/output'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import { PlanOptions } from './command-object'; import { createNxReleaseConfig, diff --git a/packages/nx/src/command-line/release/publish.ts b/packages/nx/src/command-line/release/publish.ts index d8f6890730705..bf2ccc6f38427 100644 --- a/packages/nx/src/command-line/release/publish.ts +++ b/packages/nx/src/command-line/release/publish.ts @@ -15,7 +15,7 @@ import { readGraphFileFromGraphArg, } from '../../utils/command-line-utils'; import { output } from '../../utils/output'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import { projectHasTarget } from '../../utils/project-graph-utils'; import { generateGraph } from '../graph/graph'; import { PublishOptions } from './command-object'; diff --git a/packages/nx/src/command-line/release/release.ts b/packages/nx/src/command-line/release/release.ts index 8c45c29a1eaa6..b7428340d451f 100644 --- a/packages/nx/src/command-line/release/release.ts +++ b/packages/nx/src/command-line/release/release.ts @@ -4,7 +4,7 @@ import { NxReleaseConfiguration, readNxJson } from '../../config/nx-json'; import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils'; import { createProjectGraphAsync } from '../../project-graph/project-graph'; import { output } from '../../utils/output'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import { createAPI as createReleaseChangelogAPI, shouldCreateGitHubRelease, diff --git a/packages/nx/src/command-line/release/version.ts b/packages/nx/src/command-line/release/version.ts index 70b5984636632..e990727057c12 100644 --- a/packages/nx/src/command-line/release/version.ts +++ b/packages/nx/src/command-line/release/version.ts @@ -19,7 +19,7 @@ import { readProjectsConfigurationFromProjectGraph, } from '../../project-graph/project-graph'; import { output } from '../../utils/output'; -import { combineOptionsForGenerator, handleErrors } from '../../utils/params'; +import { combineOptionsForGenerator } from '../../utils/params'; import { joinPathFragments } from '../../utils/path'; import { workspaceRoot } from '../../utils/workspace-root'; import { parseGeneratorString } from '../generate/generate'; @@ -52,6 +52,7 @@ import { createGitTagValues, handleDuplicateGitTags, } from './utils/shared'; +import { handleErrors } from '../../utils/handle-errors'; const LARGE_BUFFER = 1024 * 1000000; diff --git a/packages/nx/src/command-line/repair/repair.ts b/packages/nx/src/command-line/repair/repair.ts index cae4540769491..10a645a7d544f 100644 --- a/packages/nx/src/command-line/repair/repair.ts +++ b/packages/nx/src/command-line/repair/repair.ts @@ -1,4 +1,4 @@ -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import * as migrationsJson from '../../../migrations.json'; import { executeMigrations } from '../migrate/migrate'; import { output } from '../../utils/output'; diff --git a/packages/nx/src/command-line/run-many/command-object.ts b/packages/nx/src/command-line/run-many/command-object.ts index ae3d409adfc12..3916349673218 100644 --- a/packages/nx/src/command-line/run-many/command-object.ts +++ b/packages/nx/src/command-line/run-many/command-object.ts @@ -7,7 +7,7 @@ import { withOverrides, withBatch, } from '../yargs-utils/shared-options'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; export const yargsRunManyCommand: CommandModule = { command: 'run-many', diff --git a/packages/nx/src/command-line/run/command-object.ts b/packages/nx/src/command-line/run/command-object.ts index 1a2a9ee8d8e6c..1dd1e70748a21 100644 --- a/packages/nx/src/command-line/run/command-object.ts +++ b/packages/nx/src/command-line/run/command-object.ts @@ -4,7 +4,7 @@ import { withOverrides, withRunOneOptions, } from '../yargs-utils/shared-options'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; export const yargsRunCommand: CommandModule = { command: 'run [project][:target][:configuration] [_..]', diff --git a/packages/nx/src/command-line/run/run.ts b/packages/nx/src/command-line/run/run.ts index f33b863b53135..d7679ebe63cde 100644 --- a/packages/nx/src/command-line/run/run.ts +++ b/packages/nx/src/command-line/run/run.ts @@ -1,9 +1,6 @@ import { env as appendLocalEnv } from 'npm-run-path'; -import { - combineOptionsForExecutor, - handleErrors, - Schema, -} from '../../utils/params'; +import { combineOptionsForExecutor, Schema } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import { printHelp } from '../../utils/print-help'; import { NxJsonConfiguration } from '../../config/nx-json'; import { relative } from 'path'; diff --git a/packages/nx/src/command-line/show/command-object.ts b/packages/nx/src/command-line/show/command-object.ts index 77ab9a81fc502..4dea34461c92e 100644 --- a/packages/nx/src/command-line/show/command-object.ts +++ b/packages/nx/src/command-line/show/command-object.ts @@ -5,7 +5,7 @@ import { withAffectedOptions, withVerbose, } from '../yargs-utils/shared-options'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; export interface NxShowArgs { json?: boolean; diff --git a/packages/nx/src/command-line/sync/sync.ts b/packages/nx/src/command-line/sync/sync.ts index 8d7461b60a561..ec8eb59bd2c43 100644 --- a/packages/nx/src/command-line/sync/sync.ts +++ b/packages/nx/src/command-line/sync/sync.ts @@ -2,7 +2,7 @@ import * as ora from 'ora'; import { readNxJson } from '../../config/nx-json'; import { createProjectGraphAsync } from '../../project-graph/project-graph'; import { output } from '../../utils/output'; -import { handleErrors } from '../../utils/params'; +import { handleErrors } from '../../utils/handle-errors'; import { collectAllRegisteredSyncGenerators, flushSyncGeneratorChanges, diff --git a/packages/nx/src/project-graph/error-types.ts b/packages/nx/src/project-graph/error-types.ts index 795e1c3d0cfbd..ac20857e7bc8d 100644 --- a/packages/nx/src/project-graph/error-types.ts +++ b/packages/nx/src/project-graph/error-types.ts @@ -42,7 +42,7 @@ export class ProjectGraphError extends Error { this.#partialProjectGraph = partialProjectGraph; this.#partialSourceMaps = partialSourceMaps; this.stack = `${this.message}\n ${errors - .map((error) => error.stack.split('\n').join('\n ')) + .map((error) => indentString(formatErrorStackAndCause(error), 2)) .join('\n')}`; } @@ -263,15 +263,21 @@ export class MergeNodesError extends Error { this.name = this.constructor.name; this.file = file; this.pluginName = pluginName; - this.stack = `${this.message}\n ${error.stack.split('\n').join('\n ')}`; + this.stack = `${this.message}\n${indentString( + formatErrorStackAndCause(error), + 2 + )}`; } } export class CreateMetadataError extends Error { constructor(public readonly error: Error, public readonly plugin: string) { - super(`The "${plugin}" plugin threw an error while creating metadata:`, { - cause: error, - }); + super( + `The "${plugin}" plugin threw an error while creating metadata: ${error.message}`, + { + cause: error, + } + ); this.name = this.constructor.name; } } @@ -279,7 +285,7 @@ export class CreateMetadataError extends Error { export class ProcessDependenciesError extends Error { constructor(public readonly pluginName: string, { cause }) { super( - `The "${pluginName}" plugin threw an error while creating dependencies:`, + `The "${pluginName}" plugin threw an error while creating dependencies: ${cause.message}`, { cause, } @@ -316,7 +322,7 @@ export class ProcessProjectGraphError extends Error { } ); this.name = this.constructor.name; - this.stack = `${this.message}\n ${cause.stack.split('\n').join('\n ')}`; + this.stack = `${this.message}\n${indentString(cause, 2)}`; } } @@ -394,3 +400,24 @@ export class LoadPluginError extends Error { this.name = this.constructor.name; } } + +function indentString(str: string, indent: number): string { + return ( + ' '.repeat(indent) + + str + .split('\n') + .map((line) => ' '.repeat(indent) + line) + .join('\n') + ); +} + +function formatErrorStackAndCause(error: Error): string { + const cause = + error.cause && error.cause instanceof Error ? error.cause : null; + return ( + error.stack + + (cause + ? `\nCaused by: \n${indentString(cause.stack ?? cause.message, 2)}` + : '') + ); +} diff --git a/packages/nx/src/tasks-runner/run-command.ts b/packages/nx/src/tasks-runner/run-command.ts index 8e323067c6abf..8352e5d88a624 100644 --- a/packages/nx/src/tasks-runner/run-command.ts +++ b/packages/nx/src/tasks-runner/run-command.ts @@ -20,7 +20,7 @@ import { isRelativePath } from '../utils/fileutils'; import { isCI } from '../utils/is-ci'; import { isNxCloudUsed } from '../utils/nx-cloud-utils'; import { output } from '../utils/output'; -import { handleErrors } from '../utils/params'; +import { handleErrors } from '../utils/handle-errors'; import { collectEnabledTaskSyncGeneratorsFromTaskGraph, flushSyncGeneratorChanges, diff --git a/packages/nx/src/utils/handle-errors.spec.ts b/packages/nx/src/utils/handle-errors.spec.ts new file mode 100644 index 0000000000000..bc9a6877fa1d1 --- /dev/null +++ b/packages/nx/src/utils/handle-errors.spec.ts @@ -0,0 +1,70 @@ +import { + CreateMetadataError, + ProjectGraphError, +} from '../project-graph/error-types'; +import { handleErrors } from './handle-errors'; +import { output } from './output'; + +describe('handleErrors', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should display project graph error cause message', async () => { + const spy = jest.spyOn(output, 'error').mockImplementation(() => {}); + await handleErrors(true, async () => { + const cause = new Error('cause message'); + const metadataError = new CreateMetadataError(cause, 'test-plugin'); + throw new ProjectGraphError( + [metadataError], + { nodes: {}, dependencies: {} }, + {} + ); + }); + const { bodyLines, title } = spy.mock.calls[0][0]; + const body = bodyLines.join('\n'); + expect(body).toContain('cause message'); + expect(body).toContain('test-plugin'); + }); + + it('should only display wrapper error if not verbose', async () => { + const spy = jest.spyOn(output, 'error').mockImplementation(() => {}); + await handleErrors(false, async () => { + const cause = new Error('cause message'); + const metadataError = new CreateMetadataError(cause, 'test-plugin'); + throw new ProjectGraphError( + [metadataError], + { nodes: {}, dependencies: {} }, + {} + ); + }); + + const { bodyLines, title } = spy.mock.calls[0][0]; + const body = bodyLines.join('\n'); + expect(body).not.toContain('cause message'); + }); + + it('should display misc errors that do not have a cause', async () => { + const spy = jest.spyOn(output, 'error').mockImplementation(() => {}); + await handleErrors(true, async () => { + throw new Error('misc error'); + }); + const { bodyLines, title } = spy.mock.calls[0][0]; + const body = bodyLines.join('\n'); + expect(body).toContain('misc error'); + expect(body).not.toMatch(/[Cc]ause/); + }); + + it('should display misc errors that have a cause', async () => { + const spy = jest.spyOn(output, 'error').mockImplementation(() => {}); + await handleErrors(true, async () => { + const cause = new Error('cause message'); + const err = new Error('misc error', { cause }); + throw err; + }); + const { bodyLines, title } = spy.mock.calls[0][0]; + const body = bodyLines.join('\n'); + expect(body).toContain('misc error'); + expect(body).toContain('cause message'); + }); +}); diff --git a/packages/nx/src/utils/handle-errors.ts b/packages/nx/src/utils/handle-errors.ts new file mode 100644 index 0000000000000..77de737eea592 --- /dev/null +++ b/packages/nx/src/utils/handle-errors.ts @@ -0,0 +1,74 @@ +import { daemonClient } from '../daemon/client/client'; +import { ProjectGraphError } from '../project-graph/error-types'; +import { logger } from './logger'; +import { output } from './output'; + +export async function handleErrors( + isVerbose: boolean, + fn: Function +): Promise { + try { + const result = await fn(); + if (typeof result === 'number') { + return result; + } + return 0; + } catch (err) { + err ||= new Error('Unknown error caught'); + if (err.constructor.name === 'UnsuccessfulWorkflowExecution') { + logger.error('The generator workflow failed. See above.'); + } else if (err.name === 'ProjectGraphError') { + const projectGraphError = err as ProjectGraphError; + let title = projectGraphError.message; + if ( + projectGraphError.cause && + typeof projectGraphError.cause === 'object' && + 'message' in projectGraphError.cause + ) { + title += ' ' + projectGraphError.cause.message + '.'; + } + if (isVerbose) { + title += ' See errors below.'; + } + + const bodyLines = isVerbose + ? formatErrorStackAndCause(projectGraphError) + : ['Pass --verbose to see the stacktraces.']; + + output.error({ + title, + bodyLines: bodyLines, + }); + } else { + const lines = (err.message ? err.message : err.toString()).split('\n'); + const bodyLines: string[] = lines.slice(1); + if (isVerbose) { + bodyLines.push(...formatErrorStackAndCause(err)); + } else if (err.stack) { + bodyLines.push('Pass --verbose to see the stacktrace.'); + } + output.error({ + title: lines[0], + bodyLines, + }); + } + if (daemonClient.enabled()) { + daemonClient.reset(); + } + return 1; + } +} + +function formatErrorStackAndCause(error: T): string[] { + return [ + error.stack || error.message, + ...(error.cause && typeof error.cause === 'object' + ? [ + 'Caused by:', + 'stack' in error.cause + ? error.cause.stack.toString() + : error.cause.toString(), + ] + : []), + ]; +} diff --git a/packages/nx/src/utils/params.ts b/packages/nx/src/utils/params.ts index c7a80fba5093a..562736183969e 100644 --- a/packages/nx/src/utils/params.ts +++ b/packages/nx/src/utils/params.ts @@ -89,56 +89,6 @@ export type Options = { [k: string]: string | number | boolean | string[] | Unmatched[] | undefined; }; -export async function handleErrors( - isVerbose: boolean, - fn: Function -): Promise { - try { - const result = await fn(); - if (typeof result === 'number') { - return result; - } - return 0; - } catch (err) { - err ||= new Error('Unknown error caught'); - if (err.constructor.name === 'UnsuccessfulWorkflowExecution') { - logger.error('The generator workflow failed. See above.'); - } else if (err.name === 'ProjectGraphError') { - const projectGraphError = err as ProjectGraphError; - let title = projectGraphError.message; - if (isVerbose) { - title += ' See errors below.'; - } - - const bodyLines = isVerbose - ? [projectGraphError.stack] - : ['Pass --verbose to see the stacktraces.']; - - output.error({ - title, - bodyLines: bodyLines, - }); - } else { - const lines = (err.message ? err.message : err.toString()).split('\n'); - const bodyLines = lines.slice(1); - if (err.stack && !isVerbose) { - bodyLines.push('Pass --verbose to see the stacktrace.'); - } - output.error({ - title: lines[0], - bodyLines, - }); - if (err.stack && isVerbose) { - logger.info(err.stack); - } - } - if (daemonClient.enabled()) { - daemonClient.reset(); - } - return 1; - } -} - function camelCase(input: string): string { if (input.indexOf('-') > 1) { return input