diff --git a/src/errorHandling.ts b/src/errorHandling.ts index 9560c9dbb..e4de61cd6 100644 --- a/src/errorHandling.ts +++ b/src/errorHandling.ts @@ -8,6 +8,7 @@ import { SfError } from '@salesforce/core'; import { OclifError } from '@oclif/core/lib/interfaces/errors.js'; import { SfCommandError } from './types.js'; +import { removeEmpty } from './util.js'; /** * @@ -65,6 +66,28 @@ export const errorIsTypeError = (error: Error | SfError): boolean => Boolean(error.stack?.includes('TypeError')) || ('cause' in error && error.cause instanceof Error && errorIsTypeError(error.cause)); +export const errorToSfCommandError = ( + codeFromError: number, + error: Error | SfError | SfCommandError, + commandName: string +): SfCommandError => ({ + ...removeEmpty({ + code: codeFromError, + actions: 'actions' in error ? error.actions : null, + context: ('context' in error ? error.context : commandName) ?? commandName, + commandName: ('commandName' in error ? error.commandName : commandName) ?? commandName, + data: 'data' in error ? error.data : null, + result: 'result' in error ? error.result : null, + }), + ...{ + message: error.message, + name: error.name ?? 'Error', + status: codeFromError, + stack: error.stack, + exitCode: codeFromError, + }, +}); + /** custom typeGuard for handling the fact the SfCommand doesn't know about oclif error structure */ const isOclifError = (e: T): e is T & OclifError => 'oclif' in e ? true : false; diff --git a/src/sfCommand.ts b/src/sfCommand.ts index 7b24a8048..40db1d33f 100644 --- a/src/sfCommand.ts +++ b/src/sfCommand.ts @@ -24,8 +24,7 @@ import { SfCommandError } from './types.js'; import { formatActions, formatError } from './errorFormatting.js'; import { StandardColors } from './ux/standardColors.js'; import { confirm, secretPrompt, PromptInputs } from './ux/prompts.js'; -import { removeEmpty } from './util.js'; -import { computeErrorCode } from './errorHandling.js'; +import { computeErrorCode, errorToSfCommandError } from './errorHandling.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/sf-plugins-core', 'messages'); @@ -34,7 +33,7 @@ export type SfCommandInterface = { configurationVariablesSection?: HelpSection; envVariablesSection?: HelpSection; errorCodes?: HelpSection; -} & Command.Class +} & Command.Class; /** * A base command that provided common functionality for all sf commands. @@ -391,26 +390,7 @@ export abstract class SfCommand extends Command { const codeFromError = computeErrorCode(error); process.exitCode = codeFromError; - const sfErrorProperties = removeEmpty({ - code: codeFromError, - actions: 'actions' in error ? error.actions : null, - context: 'context' in error ? error.context : this.statics.name, - commandName: 'commandName' in error ? error.commandName : this.statics.name, - data: 'data' in error ? error.data : null, - result: 'result' in error ? error.result : null, - }); - - // Create printable error object - const sfCommandError: SfCommand.Error = { - ...sfErrorProperties, - ...{ - message: error.message, - name: error.name ?? 'Error', - status: process.exitCode, - stack: error.stack, - exitCode: process.exitCode, - }, - }; + const sfCommandError = errorToSfCommandError(codeFromError, error, this.statics.name); if (this.jsonEnabled()) { this.logJson(this.toErrorJson(sfCommandError)); @@ -469,6 +449,6 @@ export namespace SfCommand { status: number; result: T; warnings?: Warning[]; - } + }; export type Error = SfCommandError; } diff --git a/test/unit/errorHandling.test.ts b/test/unit/errorHandling.test.ts index 0546fc7fa..b59cd7938 100644 --- a/test/unit/errorHandling.test.ts +++ b/test/unit/errorHandling.test.ts @@ -6,7 +6,7 @@ */ import { expect } from 'chai'; import { SfError } from '@salesforce/core'; -import { computeErrorCode, errorIsGack, errorIsTypeError } from '../../src/errorHandling.js'; +import { computeErrorCode, errorIsGack, errorIsTypeError, errorToSfCommandError } from '../../src/errorHandling.js'; describe('typeErrors', () => { let typeError: Error; @@ -122,3 +122,50 @@ describe('precedence', () => { expect(computeErrorCode(e)).to.equal(10); }); }); + +describe('errorToSfCommandError', () => { + it('basic', () => { + const result = errorToSfCommandError(1, new Error('foo'), 'the:cmd'); + expect(result).to.deep.include({ + code: 1, + status: 1, + exitCode: 1, + commandName: 'the:cmd', + context: 'the:cmd', + message: 'foo', + name: 'Error', // this is the default + }); + expect(result.stack).to.be.a('string').and.include('Error: foo'); + }); + describe('context', () => { + it('sfError with context', () => { + const sfError = SfError.create({ name: 'myError', message: 'foo', actions: ['bar'], context: 'myContext' }); + const result = errorToSfCommandError(8, sfError, 'the:cmd'); + expect(result).to.deep.include({ + code: 8, + status: 8, + exitCode: 8, + commandName: 'the:cmd', + context: 'myContext', + message: 'foo', + name: 'myError', + }); + expect(result.stack).to.be.a('string').and.include('myError: foo'); + }); + it('sfError with undefined context', () => { + const sfError = SfError.create({ name: 'myError', message: 'foo', actions: ['bar'], context: undefined }); + const result = errorToSfCommandError(8, sfError, 'the:cmd'); + expect(result).to.deep.include({ + code: 8, + status: 8, + exitCode: 8, + commandName: 'the:cmd', + // defaults to the command name + context: 'the:cmd', + message: 'foo', + name: 'myError', + }); + expect(result.stack).to.be.a('string').and.include('myError: foo'); + }); + }); +});