From c8d9c1c292e5fa5c72559c9fd1e7c399beb8517a Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 4 Apr 2024 10:50:51 -0500 Subject: [PATCH 1/7] feat: sfError --- src/sfError.ts | 69 ++++++++++++++++-------- test/unit/sfErrorTest.ts | 114 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 159 insertions(+), 24 deletions(-) diff --git a/src/sfError.ts b/src/sfError.ts index 390a550285..e551e56235 100644 --- a/src/sfError.ts +++ b/src/sfError.ts @@ -5,9 +5,20 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { NamedError } from '@salesforce/kit'; import { AnyJson, hasString, isString, JsonMap } from '@salesforce/ts-types'; +export type SfErrorOptions = { + message: string; + exitCode?: number; + name?: string; + data?: T; + cause?: Error; + context?: string; + actions: string[]; +}; + +type ErrorDataProperties = AnyJson; + /** * A generalized sfdx error which also contains an action. The action is used in the * CLI to help guide users past the error. @@ -24,7 +35,8 @@ import { AnyJson, hasString, isString, JsonMap } from '@salesforce/ts-types'; * throw new SfError(message.getMessage('myError'), 'MyErrorName'); * ``` */ -export class SfError extends NamedError { +export class SfError extends Error { + public readonly name: string; /** * Action messages. Hints to the users regarding what can be done to fix related issues. */ @@ -59,13 +71,15 @@ export class SfError extends NamedError { */ public constructor( message: string, - name?: string, + name = 'SfError', actions?: string[], exitCodeOrCause?: number | Error, cause?: Error ) { - cause = exitCodeOrCause instanceof Error ? exitCodeOrCause : cause; - super(name ?? 'SfError', message || name, cause); + const derivedCause = exitCodeOrCause instanceof Error ? exitCodeOrCause : cause; + super(message); + this.name = name; + this.cause = derivedCause; this.actions = actions; if (typeof exitCodeOrCause === 'number') { this.exitCode = exitCodeOrCause; @@ -74,6 +88,10 @@ export class SfError extends NamedError { } } + public get fullStack(): string | undefined { + return recursiveStack(this).join('\nCaused by: '); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any public get code(): any { return this.#code ?? this.name; @@ -83,26 +101,39 @@ export class SfError extends NamedError { this.#code = code; } + /** like the constructor, but takes an typed object and let you also set context and data properties */ + public static create(inputs: SfErrorOptions): SfError { + const error = new SfError(inputs.message, inputs.name, inputs.actions, inputs.exitCode, inputs.cause); + error.data = inputs.data; + error.context = inputs.context; + return error; + } /** * Convert an Error to an SfError. * * @param err The error to convert. */ - public static wrap(err: Error | string): SfError { + public static wrap(err: unknown): SfError { if (isString(err)) { - return new SfError(err); + return new SfError(err); } if (err instanceof SfError) { - return err; + return err as SfError; } - const sfError = new SfError(err.message, err.name, undefined, err); + const sfError = + err instanceof Error + ? // a basic error with message and name. We make it the cause to preserve any other properties + new SfError(err.message, err.name, undefined, err) + : // ok, something was throws that wasn't error or string. Convert it to an Error that preserves the information as the cause and wrap that. + SfError.wrap(new TypeError('An unexpected error occurred', { cause: err })); // If the original error has a code, use that instead of name. if (hasString(err, 'code')) { sfError.code = err.code; } + return sfError; } @@ -130,24 +161,16 @@ export class SfError extends NamedError { * Convert an {@link SfError} state to an object. Returns a plain object representing the state of this error. */ public toObject(): JsonMap { - const obj: JsonMap = { + return { name: this.name, message: this.message ?? this.name, exitCode: this.exitCode, actions: this.actions, + ...(this.context ? { context: this.context } : {}), + ...(this.data ? { data: this.data } : {}), }; - - if (this.context) { - obj.context = this.context; - } - - if (this.data) { - // DANGER: data was previously typed as `unknown` and this assertion was here on the toObject. - // TODO in next major release: put proper type constraint on SfError.data to something that can serialize - // while we're making breaking changes, provide a more definite type for toObject - obj.data = this.data as AnyJson; - } - - return obj; } } + +const recursiveStack = (err: Error): string[] => + (err.cause && err.cause instanceof Error ? [err.stack, ...recursiveStack(err.cause)] : [err.stack]).filter(isString); diff --git a/test/unit/sfErrorTest.ts b/test/unit/sfErrorTest.ts index 87969bd2f7..2e9c692f68 100644 --- a/test/unit/sfErrorTest.ts +++ b/test/unit/sfErrorTest.ts @@ -4,7 +4,7 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { expect } from 'chai'; +import { expect, assert } from 'chai'; import { Messages } from '../../src/messages'; import { SfError } from '../../src/sfError'; @@ -31,8 +31,71 @@ describe('SfError', () => { const err = new SfError(msg, 'myErrorName'); expect(err.name).to.equal('myErrorName'); }); + + it('sets actions', () => { + const msg = 'this is a test message'; + const actions = ['Do this action', 'Do that action']; + const err = new SfError(msg, 'myErrorName', actions); + expect(err.actions).to.equal(actions); + }); + + it('cause as 4th property', () => { + const msg = 'this is a test message'; + const cause = new Error('cause'); + const err = new SfError(msg, 'myErrorName', undefined, cause); + expect(err.cause).to.equal(cause); + }); + + it('cause as 5th property + exitCode', () => { + const msg = 'this is a test message'; + const cause = new Error('cause'); + const err = new SfError(msg, 'myErrorName', undefined, 2, cause); + expect(err.cause).to.equal(cause); + expect(err.exitCode).to.equal(2); + }); + + it('exitCode is 1 when undefined is provided', () => { + const msg = 'this is a test message'; + const cause = new Error('cause'); + const err = new SfError(msg, 'myErrorName', undefined, undefined, cause); + expect(err.cause).to.equal(cause); + expect(err.exitCode).to.equal(1); + }); + + it('exitCode is 1 when no arg is provided', () => { + const msg = 'this is a test message'; + const err = new SfError(msg, 'myErrorName'); + expect(err.cause).to.equal(undefined); + expect(err.exitCode).to.equal(1); + }); }); + describe('fullStack', () => { + it('returned `name:message` when no cause', () => { + const err = new SfError('test'); + expect(err.fullStack).to.include('SfError: test'); + expect(err.fullStack).to.include('sfErrorTest.ts'); + expect(err.fullStack).to.not.include('Caused by:'); + }); + it('1 cause', () => { + const nestedError = new Error('nested'); + const err = new SfError('test', undefined, undefined, nestedError); + expect(err.fullStack).to.include('SfError: test'); + expect(err.fullStack).to.include('sfErrorTest.ts'); + expect(err.fullStack).to.include('nested'); + expect(err.fullStack?.match(/Caused by:/g)).to.have.lengthOf(1); + }); + it('recurse through stacked causes', () => { + const nestedError = new Error('nested'); + const nestedError2 = new Error('nested2', { cause: nestedError }); + const err = new SfError('test', undefined, undefined, nestedError2); + expect(err.fullStack).to.include('SfError: test'); + expect(err.fullStack).to.include('sfErrorTest.ts'); + expect(err.fullStack).to.include('nested'); + expect(err.fullStack).to.include('nested2'); + expect(err.fullStack?.match(/Caused by:/g)).to.have.lengthOf(2); + }); + }); describe('wrap', () => { it('should return a wrapped error', () => { const myErrorMsg = 'yikes! What did you do?'; @@ -70,6 +133,55 @@ describe('SfError', () => { expect(mySfError).to.be.an.instanceOf(SfError); expect(mySfError).to.equal(existingSfError); }); + + describe('handling "other" stuff that is not Error', () => { + it('undefined', () => { + const wrapMe = undefined; + const mySfError = SfError.wrap(wrapMe); + expect(mySfError).to.be.an.instanceOf(SfError); + expect(mySfError.message === 'An unexpected error occurred'); + expect(mySfError.name === 'TypeError'); + assert(mySfError.cause instanceof TypeError); + expect(mySfError.cause.message === 'An unexpected error occurred'); + expect(mySfError.cause.cause).to.equal(wrapMe); + }); + it('a number', () => { + const wrapMe = 2; + const mySfError = SfError.wrap(wrapMe); + expect(mySfError).to.be.an.instanceOf(SfError); + assert(mySfError.cause instanceof TypeError); + expect(mySfError.cause.cause).to.equal(wrapMe); + }); + it('an object', () => { + const wrapMe = { a: 2 }; + const mySfError = SfError.wrap(wrapMe); + expect(mySfError).to.be.an.instanceOf(SfError); + assert(mySfError.cause instanceof TypeError); + expect(mySfError.cause.cause).to.equal(wrapMe); + }); + it('an object that has a code', () => { + const wrapMe = { a: 2, code: 'foo' }; + const mySfError = SfError.wrap(wrapMe); + expect(mySfError).to.be.an.instanceOf(SfError); + assert(mySfError.cause instanceof TypeError); + expect(mySfError.cause.cause).to.equal(wrapMe); + expect(mySfError.code).to.equal('foo'); + }); + it('an array', () => { + const wrapMe = [1, 5, 6]; + const mySfError = SfError.wrap(wrapMe); + expect(mySfError).to.be.an.instanceOf(SfError); + assert(mySfError.cause instanceof TypeError); + expect(mySfError.cause.cause).to.equal(wrapMe); + }); + it('a class', () => { + const wrapMe = new (class Test {})(); + const mySfError = SfError.wrap(wrapMe); + expect(mySfError).to.be.an.instanceOf(SfError); + assert(mySfError.cause instanceof TypeError); + expect(mySfError.cause.cause).to.equal(wrapMe); + }); + }); }); describe('generic for data', () => { From 91cc5c23279ec4cd1242a31381c0ab26d628616e Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 4 Apr 2024 11:02:35 -0500 Subject: [PATCH 2/7] test: create --- src/sfError.ts | 2 +- test/unit/sfErrorTest.ts | 83 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/sfError.ts b/src/sfError.ts index e551e56235..8e32d2951b 100644 --- a/src/sfError.ts +++ b/src/sfError.ts @@ -14,7 +14,7 @@ export type SfErrorOptions data?: T; cause?: Error; context?: string; - actions: string[]; + actions?: string[]; }; type ErrorDataProperties = AnyJson; diff --git a/test/unit/sfErrorTest.ts b/test/unit/sfErrorTest.ts index 2e9c692f68..d5f896e106 100644 --- a/test/unit/sfErrorTest.ts +++ b/test/unit/sfErrorTest.ts @@ -243,4 +243,87 @@ describe('SfError', () => { }); }); }); + + describe('create', () => { + it('message only sets the default error name', () => { + const message = 'its a trap!'; + const error = SfError.create({ message }); + expect(error.message).to.equal(message); + expect(error.name).to.equal('SfError'); + }); + it('sets name', () => { + const message = 'its a trap!'; + const name = 'BadError'; + const error = SfError.create({ message, name }); + expect(error.message).to.equal(message); + expect(error.name).to.equal(name); + }); + it('sets cause', () => { + const cause = new Error('cause'); + const error = SfError.create({ message: 'its a trap!', cause }); + expect(error.cause).to.equal(cause); + }); + it('sets exit code', () => { + const message = 'its a trap!'; + const exitCode = 100; + const error = SfError.create({ message, exitCode }); + expect(error.message).to.equal(message); + expect(error.exitCode).to.equal(exitCode); + }); + it('sets actions', () => { + const message = 'its a trap!'; + const actions = ['do the opposite']; + const error = SfError.create({ message, actions }); + expect(error.message).to.equal(message); + expect(error.actions).to.equal(actions); + }); + it('sets data', () => { + const message = 'its a trap!'; + const data = { foo: 'pity the foo' }; + const error = SfError.create({ message, data }); + expect(error.message).to.equal(message); + expect(error.data).to.equal(data); + }); + it('sets data (typed)', () => { + const message = 'its a trap!'; + const data = { foo: 'pity the foo' }; + const error = SfError.create<{ foo: string }>({ message, data }); + expect(error.message).to.equal(message); + expect(error.data).to.equal(data); + }); + it('sets context', () => { + const message = 'its a trap!'; + const context = 'TestContext1'; + const error = SfError.create({ message, context }); + expect(error.message).to.equal(message); + expect(error.context).to.equal(context); + }); + it('all the things', () => { + const message = 'its a trap!'; + const name = 'BadError'; + const actions = ['do the opposite']; + const cause = new Error('cause'); + const exitCode = 100; + const context = 'TestContext1'; + const data = { foo: 'pity the foo' }; + + const error = SfError.create({ + message, + name, + actions, + cause, + exitCode, + context, + data, + }); + + expect(error.message).to.equal(message); + expect(error.name).to.equal(name); + expect(error.actions).to.equal(actions); + expect(error.cause).to.equal(cause); + expect(error.exitCode).to.equal(exitCode); + expect(error.context).to.equal(context); + expect(error.data).to.equal(data); + }); + }); }); From 39bd401cd9c5756ed071c7935360428eaa8a6434 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 4 Apr 2024 11:05:38 -0500 Subject: [PATCH 3/7] chore: tsconfig changes to get es2022 error.cause (node 18+) --- test/tsconfig.json | 6 +++++- tsconfig.json | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/test/tsconfig.json b/test/tsconfig.json index d8c4f367f4..d1a7d129dd 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -5,6 +5,10 @@ "noEmit": true, "skipLibCheck": true, "resolveJsonModule": true, - "esModuleInterop": true + "esModuleInterop": true, + "lib": ["ES2022"], + "module": "Node16", + "moduleResolution": "Node16", + "target": "ES2022" } } diff --git a/tsconfig.json b/tsconfig.json index 7f7d88a843..6bd25876fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,10 @@ "compilerOptions": { "outDir": "./lib", "resolveJsonModule": true, + "lib": ["ES2022"], + "module": "Node16", + "moduleResolution": "Node16", + "target": "ES2022", "rootDir": "./src", "plugins": [{ "transform": "./src/messageTransformer.ts" }], "esModuleInterop": true From aed3f3c5323c1369e44fca9158516783be9395db Mon Sep 17 00:00:00 2001 From: mshanemc Date: Fri, 5 Apr 2024 09:17:38 -0500 Subject: [PATCH 4/7] feat: let cause be unknown for constructor, create (like wrap) --- src/messages.ts | 6 ++-- src/sfError.ts | 25 ++++++++--------- test/unit/org/scratchOrgInfoApiTest.ts | 4 +-- test/unit/sfErrorTest.ts | 38 ++++++++++++++++---------- 4 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/messages.ts b/src/messages.ts index 280eebde7b..be5c21da7d 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -363,14 +363,14 @@ export class Messages { } if (!packageName) { - const errMessage = `Invalid or missing package.json file at '${moduleMessagesDirPath}'. If not using a package.json, pass in a packageName.`; + const message = `Invalid or missing package.json file at '${moduleMessagesDirPath}'. If not using a package.json, pass in a packageName.`; try { packageName = asString(ensureJsonMap(Messages.readFile(path.join(moduleMessagesDirPath, 'package.json'))).name); if (!packageName) { - throw new NamedError('MissingPackageName', errMessage); + throw SfError.create({ message, name: 'MissingPackageName' }); } } catch (err) { - throw new NamedError('MissingPackageName', errMessage, err as Error); + throw SfError.create({ message, name: 'MissingPackageName', cause: err }); } } diff --git a/src/sfError.ts b/src/sfError.ts index 8e32d2951b..2c8fa524ef 100644 --- a/src/sfError.ts +++ b/src/sfError.ts @@ -12,7 +12,8 @@ export type SfErrorOptions exitCode?: number; name?: string; data?: T; - cause?: Error; + /** pass an Error. For convenience in catch blocks, code will check that it is, in fact, an Error */ + cause?: unknown; context?: string; actions?: string[]; }; @@ -74,12 +75,14 @@ export class SfError extend name = 'SfError', actions?: string[], exitCodeOrCause?: number | Error, - cause?: Error + cause?: unknown ) { - const derivedCause = exitCodeOrCause instanceof Error ? exitCodeOrCause : cause; + if (typeof cause !== 'undefined' && !(cause instanceof Error)) { + throw new TypeError(`The cause, if provided, must be an instance of Error. Received: ${typeof cause}`); + } super(message); this.name = name; - this.cause = derivedCause; + this.cause = exitCodeOrCause instanceof Error ? exitCodeOrCause : cause; this.actions = actions; if (typeof exitCodeOrCause === 'number') { this.exitCode = exitCodeOrCause; @@ -88,12 +91,7 @@ export class SfError extend } } - public get fullStack(): string | undefined { - return recursiveStack(this).join('\nCaused by: '); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public get code(): any { + public get code(): string { return this.#code ?? this.name; } @@ -127,7 +125,9 @@ export class SfError extend ? // a basic error with message and name. We make it the cause to preserve any other properties new SfError(err.message, err.name, undefined, err) : // ok, something was throws that wasn't error or string. Convert it to an Error that preserves the information as the cause and wrap that. - SfError.wrap(new TypeError('An unexpected error occurred', { cause: err })); + SfError.wrap( + new TypeError(`SfError.wrap received type ${typeof err} but expects type Error or string`, { cause: err }) + ); // If the original error has a code, use that instead of name. if (hasString(err, 'code')) { @@ -171,6 +171,3 @@ export class SfError extend }; } } - -const recursiveStack = (err: Error): string[] => - (err.cause && err.cause instanceof Error ? [err.stack, ...recursiveStack(err.cause)] : [err.stack]).filter(isString); diff --git a/test/unit/org/scratchOrgInfoApiTest.ts b/test/unit/org/scratchOrgInfoApiTest.ts index 06c03d38dc..7b15c0fa28 100644 --- a/test/unit/org/scratchOrgInfoApiTest.ts +++ b/test/unit/org/scratchOrgInfoApiTest.ts @@ -192,7 +192,7 @@ describe('requestScratchOrgCreation', () => { expect.fail('should have thrown SfError'); } expect(error).to.exist; - expect(error).to.have.keys(['cause', 'name', 'actions', 'exitCode']); + expect(error).to.include.keys(['cause', 'name', 'actions', 'exitCode']); expect((error as Error).toString()).to.include('SignupDuplicateSettingsSpecifiedError'); } }); @@ -216,7 +216,7 @@ describe('requestScratchOrgCreation', () => { expect.fail('should have thrown SfError'); } expect(error).to.exist; - expect(error).to.have.keys(['cause', 'name', 'actions', 'exitCode']); + expect(error).to.include.keys(['cause', 'name', 'actions', 'exitCode']); expect((error as Error).toString()).to.include(messages.getMessage('DeprecatedPrefFormat')); } }); diff --git a/test/unit/sfErrorTest.ts b/test/unit/sfErrorTest.ts index d5f896e106..ef7835117c 100644 --- a/test/unit/sfErrorTest.ts +++ b/test/unit/sfErrorTest.ts @@ -4,12 +4,15 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import { inspect } from 'node:util'; import { expect, assert } from 'chai'; import { Messages } from '../../src/messages'; import { SfError } from '../../src/sfError'; Messages.importMessageFile('pname', 'testMessages.json'); +const causeDelimiter = 'cause:'; + describe('SfError', () => { describe('constructor', () => { it('should return a mutable SfError', () => { @@ -70,32 +73,38 @@ describe('SfError', () => { }); }); - describe('fullStack', () => { + describe('nested errors', () => { + const causeRegex = new RegExp(causeDelimiter, 'g'); + const nestedCauseRegex = new RegExp(/\[cause:\]/, 'g'); it('returned `name:message` when no cause', () => { const err = new SfError('test'); - expect(err.fullStack).to.include('SfError: test'); - expect(err.fullStack).to.include('sfErrorTest.ts'); - expect(err.fullStack).to.not.include('Caused by:'); + expect(inspect(err)).to.include('SfError: test'); + expect(inspect(err)).to.include('sfErrorTest.ts'); + // there's always 1 cause from the `cause:` property, even if undefined + expect(inspect(err)?.match(causeRegex)).to.have.lengthOf(1); }); it('1 cause', () => { const nestedError = new Error('nested'); const err = new SfError('test', undefined, undefined, nestedError); - expect(err.fullStack).to.include('SfError: test'); - expect(err.fullStack).to.include('sfErrorTest.ts'); - expect(err.fullStack).to.include('nested'); - expect(err.fullStack?.match(/Caused by:/g)).to.have.lengthOf(1); + expect(inspect(err)).to.include('SfError: test'); + expect(inspect(err)).to.include('sfErrorTest.ts'); + expect(inspect(err)).to.include('nested'); + expect(inspect(err)?.match(causeRegex)).to.have.lengthOf(1); + expect(inspect(err)?.match(nestedCauseRegex)).to.be.null; }); it('recurse through stacked causes', () => { const nestedError = new Error('nested'); const nestedError2 = new Error('nested2', { cause: nestedError }); const err = new SfError('test', undefined, undefined, nestedError2); - expect(err.fullStack).to.include('SfError: test'); - expect(err.fullStack).to.include('sfErrorTest.ts'); - expect(err.fullStack).to.include('nested'); - expect(err.fullStack).to.include('nested2'); - expect(err.fullStack?.match(/Caused by:/g)).to.have.lengthOf(2); + expect(inspect(err)).to.include('SfError: test'); + expect(inspect(err)).to.include('sfErrorTest.ts'); + expect(inspect(err)).to.include('nested'); + expect(inspect(err)).to.include('nested2'); + expect(inspect(err)?.match(causeRegex)).to.have.lengthOf(1); + expect(inspect(err)?.match(causeRegex)).to.have.lengthOf(1); }); }); + describe('wrap', () => { it('should return a wrapped error', () => { const myErrorMsg = 'yikes! What did you do?'; @@ -106,7 +115,8 @@ describe('SfError', () => { expect(mySfError).to.be.an.instanceOf(SfError); expect(mySfError.message).to.equal(myErrorMsg); expect(mySfError.name).to.equal(myErrorName); - expect(mySfError.fullStack).to.contain('Caused by:').and.contain(myError.stack); + expect(mySfError.cause).to.equal(myError); + expect(inspect(mySfError)).to.contain(causeDelimiter).and.contain(myErrorMsg); }); it('should return a wrapped error with a code', () => { From cdfa9286b8951b251acebbcbcc986d01e08c417c Mon Sep 17 00:00:00 2001 From: mshanemc Date: Fri, 5 Apr 2024 12:34:44 -0500 Subject: [PATCH 5/7] chore: no TypeError --- src/sfError.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/sfError.ts b/src/sfError.ts index 2c8fa524ef..a59c23dd30 100644 --- a/src/sfError.ts +++ b/src/sfError.ts @@ -123,12 +123,16 @@ export class SfError extend const sfError = err instanceof Error ? // a basic error with message and name. We make it the cause to preserve any other properties - new SfError(err.message, err.name, undefined, err) + SfError.create({ + message: err.message, + name: err.name, + cause: err, + }) : // ok, something was throws that wasn't error or string. Convert it to an Error that preserves the information as the cause and wrap that. - SfError.wrap( - new TypeError(`SfError.wrap received type ${typeof err} but expects type Error or string`, { cause: err }) - ); - + SfError.create({ + message: `SfError.wrap received type ${typeof err} but expects type Error or string`, + cause: err, + }); // If the original error has a code, use that instead of name. if (hasString(err, 'code')) { sfError.code = err.code; From 08b0c74eff40c13e68c44f5fe149add26122f1d3 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Fri, 5 Apr 2024 12:48:59 -0500 Subject: [PATCH 6/7] chore: wrap an Error (so cause can be the original error) --- src/sfError.ts | 8 ++-- test/unit/sfErrorTest.ts | 84 ++++++++++++++++++++-------------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/sfError.ts b/src/sfError.ts index a59c23dd30..ef9469f98c 100644 --- a/src/sfError.ts +++ b/src/sfError.ts @@ -129,10 +129,10 @@ export class SfError extend cause: err, }) : // ok, something was throws that wasn't error or string. Convert it to an Error that preserves the information as the cause and wrap that. - SfError.create({ - message: `SfError.wrap received type ${typeof err} but expects type Error or string`, - cause: err, - }); + SfError.wrap( + new Error(`SfError.wrap received type ${typeof err} but expects type Error or string`, { cause: err }) + ); + // If the original error has a code, use that instead of name. if (hasString(err, 'code')) { sfError.code = err.code; diff --git a/test/unit/sfErrorTest.ts b/test/unit/sfErrorTest.ts index ef7835117c..514e5b6aca 100644 --- a/test/unit/sfErrorTest.ts +++ b/test/unit/sfErrorTest.ts @@ -106,42 +106,44 @@ describe('SfError', () => { }); describe('wrap', () => { - it('should return a wrapped error', () => { - const myErrorMsg = 'yikes! What did you do?'; - const myErrorName = 'OhMyError'; - const myError = new Error(myErrorMsg); - myError.name = myErrorName; - const mySfError = SfError.wrap(myError); - expect(mySfError).to.be.an.instanceOf(SfError); - expect(mySfError.message).to.equal(myErrorMsg); - expect(mySfError.name).to.equal(myErrorName); - expect(mySfError.cause).to.equal(myError); - expect(inspect(mySfError)).to.contain(causeDelimiter).and.contain(myErrorMsg); - }); + describe('happy path', () => { + it('should return a wrapped error', () => { + const myErrorMsg = 'yikes! What did you do?'; + const myErrorName = 'OhMyError'; + const myError = new Error(myErrorMsg); + myError.name = myErrorName; + const mySfError = SfError.wrap(myError); + expect(mySfError).to.be.an.instanceOf(SfError); + expect(mySfError.message).to.equal(myErrorMsg); + expect(mySfError.name).to.equal(myErrorName); + expect(mySfError.cause).to.equal(myError); + expect(inspect(mySfError)).to.contain(causeDelimiter).and.contain(myErrorMsg); + }); - it('should return a wrapped error with a code', () => { - class CodeError extends Error { - public code?: string; - } - const myErrorCode = 'OhMyError'; - const myError = new CodeError('test'); - myError.code = myErrorCode; - const mySfError = SfError.wrap(myError); - expect(mySfError).to.be.an.instanceOf(SfError); - expect(mySfError.code).to.equal(myErrorCode); - }); + it('should return a wrapped error with a code', () => { + class CodeError extends Error { + public code?: string; + } + const myErrorCode = 'OhMyError'; + const myError = new CodeError('test'); + myError.code = myErrorCode; + const mySfError = SfError.wrap(myError); + expect(mySfError).to.be.an.instanceOf(SfError); + expect(mySfError.code).to.equal(myErrorCode); + }); - it('should return a new error with just a string', () => { - const mySfError = SfError.wrap('test'); - expect(mySfError).to.be.an.instanceOf(SfError); - expect(mySfError.message).to.equal('test'); - }); + it('should return a new error with just a string', () => { + const mySfError = SfError.wrap('test'); + expect(mySfError).to.be.an.instanceOf(SfError); + expect(mySfError.message).to.equal('test'); + }); - it('should return the error if already a SfError', () => { - const existingSfError = new SfError('test'); - const mySfError = SfError.wrap(existingSfError); - expect(mySfError).to.be.an.instanceOf(SfError); - expect(mySfError).to.equal(existingSfError); + it('should return the error if already a SfError', () => { + const existingSfError = new SfError('test'); + const mySfError = SfError.wrap(existingSfError); + expect(mySfError).to.be.an.instanceOf(SfError); + expect(mySfError).to.equal(existingSfError); + }); }); describe('handling "other" stuff that is not Error', () => { @@ -149,31 +151,29 @@ describe('SfError', () => { const wrapMe = undefined; const mySfError = SfError.wrap(wrapMe); expect(mySfError).to.be.an.instanceOf(SfError); - expect(mySfError.message === 'An unexpected error occurred'); - expect(mySfError.name === 'TypeError'); - assert(mySfError.cause instanceof TypeError); - expect(mySfError.cause.message === 'An unexpected error occurred'); + expect(mySfError.message).to.include('SfError.wrap received type '); + assert(mySfError.cause instanceof Error); expect(mySfError.cause.cause).to.equal(wrapMe); }); it('a number', () => { const wrapMe = 2; const mySfError = SfError.wrap(wrapMe); expect(mySfError).to.be.an.instanceOf(SfError); - assert(mySfError.cause instanceof TypeError); + assert(mySfError.cause instanceof Error); expect(mySfError.cause.cause).to.equal(wrapMe); }); it('an object', () => { const wrapMe = { a: 2 }; const mySfError = SfError.wrap(wrapMe); expect(mySfError).to.be.an.instanceOf(SfError); - assert(mySfError.cause instanceof TypeError); + assert(mySfError.cause instanceof Error); expect(mySfError.cause.cause).to.equal(wrapMe); }); it('an object that has a code', () => { const wrapMe = { a: 2, code: 'foo' }; const mySfError = SfError.wrap(wrapMe); expect(mySfError).to.be.an.instanceOf(SfError); - assert(mySfError.cause instanceof TypeError); + assert(mySfError.cause instanceof Error); expect(mySfError.cause.cause).to.equal(wrapMe); expect(mySfError.code).to.equal('foo'); }); @@ -181,14 +181,14 @@ describe('SfError', () => { const wrapMe = [1, 5, 6]; const mySfError = SfError.wrap(wrapMe); expect(mySfError).to.be.an.instanceOf(SfError); - assert(mySfError.cause instanceof TypeError); + assert(mySfError.cause instanceof Error); expect(mySfError.cause.cause).to.equal(wrapMe); }); it('a class', () => { const wrapMe = new (class Test {})(); const mySfError = SfError.wrap(wrapMe); expect(mySfError).to.be.an.instanceOf(SfError); - assert(mySfError.cause instanceof TypeError); + assert(mySfError.cause instanceof Error); expect(mySfError.cause.cause).to.equal(wrapMe); }); }); From bcdaf7ff12010dc333fb6d7c04a46055c971b358 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Fri, 5 Apr 2024 12:58:24 -0500 Subject: [PATCH 7/7] chore: tsconfig for postcompile (doc) --- typedocExamples/tsconfig.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/typedocExamples/tsconfig.json b/typedocExamples/tsconfig.json index a45fc563b6..c7fee8df61 100644 --- a/typedocExamples/tsconfig.json +++ b/typedocExamples/tsconfig.json @@ -5,6 +5,10 @@ "noEmit": true, "skipLibCheck": true, "resolveJsonModule": true, - "esModuleInterop": true + "esModuleInterop": true, + "lib": ["ES2022"], + "module": "Node16", + "moduleResolution": "Node16", + "target": "ES2022" } }