Skip to content

Commit

Permalink
feat: add metadata support
Browse files Browse the repository at this point in the history
  • Loading branch information
alvis committed Dec 19, 2020
1 parent ab3a7d3 commit 8147763
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 44 deletions.
52 changes: 23 additions & 29 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
},
"dependencies": {
"chalk": "4.x",
"highlight-es": "1.x"
"highlight-es": "1.x",
"yamlify-object": "0.5.x"
}
}
8 changes: 7 additions & 1 deletion source/prototype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,27 @@ export class Xception extends Error {
/** upstream error */
public cause?: unknown;

/** running context */
public meta: Record<string, unknown>;

/**
* @param message error message
* @param options additional options for the error
* @param options.cause upstream error
* @param options.meta context where the error occur
*/
constructor(
message: string,
options?: {
cause?: unknown;
meta?: Record<string, unknown>;
},
) {
const { cause } = { ...options };
const { cause, meta = {} } = { ...options };

super(message);
this.cause = cause;
this.meta = meta;

// fix the name of the error class being 'Error'
this.name = this.constructor.name;
Expand Down
71 changes: 58 additions & 13 deletions source/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
import chalk from 'chalk';
import { existsSync, readFileSync } from 'fs';
import highlight from 'highlight-es';
import yamlify from 'yamlify-object';

import { Xception } from '#prototype';
import { disassembleStack } from './stack';

import type { StackDescriptionBlock, StackLocationBlock } from './stack';
Expand All @@ -34,15 +36,32 @@ const DEFAULT_CODE_THEME = {
invalid: chalk.inverse,
};

const DEFAULT_YAML_THEME = {
error: chalk.red,
symbol: chalk.magenta,
string: chalk.green,
date: chalk.cyan,
number: chalk.magenta,
boolean: chalk.yellow,
null: chalk.yellow.bold,
undefined: chalk.yellow.bold,
};

/**
* render a description line
* @param block a stack block about an error description
* @param error error the related error
* @returns a rendered string to print
*/
function renderDescription(block: StackDescriptionBlock): string {
function renderDescription(
block: StackDescriptionBlock,
error?: unknown,
): string {
const { name, message } = block;
const description = chalk.red(`[${chalk.bold(name)}] ${message}`);
const meta = renderMeta(error);

return chalk.red(`[${chalk.bold(name)}] ${message}`);
return [description, meta].filter((block) => !!block).join('\n');
}

/**
Expand All @@ -69,6 +88,22 @@ function renderLocation(
);
}

/**
* render metadata in an error
* @param error the related error
* @returns a rendered string to print
*/
function renderMeta(error: unknown): string | null {
return error instanceof Xception && Object.keys(error.meta).length
? yamlify(error.meta, {
indent: ' ',
prefix: '\n',
postfix: '\n',
colors: DEFAULT_YAML_THEME,
})
: null;
}

/**
* render a source frame
* @param block a location block
Expand Down Expand Up @@ -129,15 +164,25 @@ export function renderStack(

const blocks = disassembleStack(error.stack!);

return blocks
.map((block, index) =>
block.type === 'location'
? renderLocation(block, {
showSource:
// NOTE a location block must follow a description block
showSource && blocks[index - 1].type === 'description',
})
: renderDescription(block),
)
.join('\n');
let currentError: unknown = error;
const renderedBlocks: string[] = [];

for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];

if (block.type === 'location') {
renderedBlocks.push(
renderLocation(block, {
showSource:
// NOTE a location block must follow a description block
showSource && blocks[i - 1].type === 'description',
}),
);
} else {
renderedBlocks.push(renderDescription(block, currentError));
currentError = error['cause'];
}
}

return renderedBlocks.join('\n');
}
5 changes: 5 additions & 0 deletions spec/prototype.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function getExtendedError(): Xception {
} catch (cause) {
return new Xception('extended', {
cause,
meta: { name: 'xception' },
});
}
}
Expand All @@ -52,6 +53,10 @@ describe('cl:Xception', () => {
expect(xception.cause).toEqual(cause);
});

it('embeds the metadata when supplied', () => {
expect(extendedError.meta).toEqual({ name: 'xception' });
});

it('keeps its own stack if the attached error has no stack', () => {
const error = new Xception('message', {
cause: { name: 'GenericError', message: 'error' },
Expand Down
19 changes: 19 additions & 0 deletions spec/render.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

import { renderStack } from '#render';

import { Xception } from '#prototype';

jest.mock('fs', () => ({
__esModule: true,
existsSync(path: string) {
Expand Down Expand Up @@ -133,4 +135,21 @@ describe('fn:renderStack', () => {
' at entry2 (absent:2:0)',
);
});

it('renders metadata', () => {
const rendered = renderStack(
new Xception('message', {
cause: new Xception('message', {
meta: { name: 'xception' },
}),
}),
);

const plain = rendered.replace(ansiExpression, '');

expect(plain).toContain('[Xception] message\n' + ' at');
expect(plain).toContain(
'[Xception] message\n' + '\n' + ' name: xception\n' + '\n' + ' at',
);
});
});
Loading

0 comments on commit 8147763

Please sign in to comment.