diff --git a/README.md b/README.md index cdf6f58..02c93da 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ export class CatsController { ```typescript theme: string; // for themes ['dark', 'light', 'default'] quote: boolean; // for displaying very good quotes +souremap: boolean; // for resolving sourcemap positions ``` example diff --git a/package.json b/package.json index aa3486b..9468ab3 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ }, "dependencies": { "mustache": "^3.0.1", + "source-map": "^0.7.3", "stack-trace": "^0.0.10" }, "peerDependencies": { diff --git a/src/__tests__/error-handler.spec.ts b/src/__tests__/error-handler.spec.ts index 6f74fec..55622ba 100644 --- a/src/__tests__/error-handler.spec.ts +++ b/src/__tests__/error-handler.spec.ts @@ -5,6 +5,7 @@ describe('ErrorHandler', () => { const errorHandler = new ErrorHandler(new Error('hello I am an error'), { theme: 'dark', quote: false, + sourcemap: true, }); expect(errorHandler).toBeInstanceOf(ErrorHandler); @@ -20,6 +21,7 @@ describe('ErrorHandler', () => { const errorHandler = new ErrorHandler(new Error('hello, another error'), { theme: 'dark', quote: false, + sourcemap: false, }); const result: any = await errorHandler.toJSON(); @@ -32,6 +34,7 @@ describe('ErrorHandler', () => { const errorHandler = new ErrorHandler(new Error('hello, error here'), { theme: 'dark', quote: false, + sourcemap: true, }); const html = await errorHandler.toHTML(); diff --git a/src/__tests__/flub-error-handler-e2e-spec.ts b/src/__tests__/flub-error-handler-e2e-spec.ts index 9c74d6c..28ba07e 100644 --- a/src/__tests__/flub-error-handler-e2e-spec.ts +++ b/src/__tests__/flub-error-handler-e2e-spec.ts @@ -2,16 +2,16 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Controller, Get, UseFilters, INestApplication } from '@nestjs/common'; import { FlubErrorHandler } from './../flub-error-handler'; import * as request from 'supertest'; +const fs = require('fs'); let flubModule: TestingModule; let app: INestApplication; @Controller('test') -@UseFilters(new FlubErrorHandler()) +@UseFilters(new FlubErrorHandler({ sourcemap: true })) class TestController { @Get('') testMe() { - return 'test'; throw new Error('standard error'); } @@ -38,6 +38,14 @@ describe('FlubErrorHandler', () => { .expect(200, { success: true }) .expect('Content-Type', /json/); }); + + it('Errors out', async () => { + return await request(app.getHttpServer()) + .get('/test') + .set('Accept', 'application/json') + .expect(500) + .expect('Content-Type', /text\/html/); + }); }); afterAll(async () => { diff --git a/src/__tests__/flub-error-handler-e2e-spec.ts.map b/src/__tests__/flub-error-handler-e2e-spec.ts.map new file mode 100644 index 0000000..86b048b --- /dev/null +++ b/src/__tests__/flub-error-handler-e2e-spec.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"flub-error-handler-e2e-spec.js","sourceRoot":"","sources":["../../src/__tests__/flub-error-handler-e2e-spec.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,6CAAsD;AACtD,2CAA+E;AAC/E,gEAA2D;AAC3D,qCAAqC;AAErC,IAAI,UAAyB,CAAC;AAC9B,IAAI,GAAqB,CAAC;AAI1B,IAAM,cAAc,GAApB,MAAM,cAAc;IAElB,MAAM;QACJ,OAAO,MAAM,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACpC,CAAC;IAGD,OAAO;QACL,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;CACF,CAAA;AATC;IADC,YAAG,CAAC,EAAE,CAAC;;;;4CAIP;AAGD;IADC,YAAG,CAAC,IAAI,CAAC;;;;6CAGT;AAVG,cAAc;IAFnB,mBAAU,CAAC,MAAM,CAAC;IAClB,mBAAU,CAAC,IAAI,qCAAgB,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;GAChD,cAAc,CAWnB;AAED,SAAS,CAAC,GAAS,EAAE;IACnB,UAAU,GAAG,MAAM,cAAI,CAAC,mBAAmB,CAAC;QAC1C,WAAW,EAAE,CAAC,cAAc,CAAC;KAC9B,CAAC,CAAC,OAAO,EAAE,CAAC;IAEb,GAAG,GAAG,UAAU,CAAC,qBAAqB,EAAE,CAAC;IACzC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;AACnB,CAAC,CAAA,CAAC,CAAC;AAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,UAAU,EAAE,GAAS,EAAE;QACxB,OAAO,MAAM,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;aACtC,GAAG,CAAC,UAAU,CAAC;aACf,GAAG,CAAC,QAAQ,EAAE,kBAAkB,CAAC;aACjC,MAAM,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;aAC9B,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC,CAAA,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,GAAS,EAAE;IAClB,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;AACpB,CAAC,CAAA,CAAC,CAAC"} \ No newline at end of file diff --git a/src/error-handler.ts b/src/error-handler.ts index 78b723c..097ab06 100644 --- a/src/error-handler.ts +++ b/src/error-handler.ts @@ -1,9 +1,9 @@ -import { ErrorParser, FrameParser } from './parser'; -import { DefaultFlubOptions } from './default-flub-options'; -import { FlubOptions } from './interfaces'; import * as fs from 'fs'; import * as Mustache from 'mustache'; import * as path from 'path'; +import { DefaultFlubOptions } from './default-flub-options'; +import { FlubOptions } from './interfaces'; +import { ErrorParser, FrameParser } from './parser'; export class ErrorHandler { private error: Error; @@ -25,9 +25,9 @@ export class ErrorHandler { return new Promise((resolve, reject) => { this.errorParser .parse() - .then(stack => { + .then(async stack => { resolve({ - error: this.errorParser.serialize(stack), + error: await this.errorParser.serialize(stack), }); }) .catch(reject); @@ -45,12 +45,17 @@ export class ErrorHandler { return new Promise((resolve, reject) => { this.errorParser .parse() - .then(stack => { - const data = this.errorParser.serialize(stack, (frame, index) => { - const serializedFrame = FrameParser.serializeCodeFrame(frame); - serializedFrame.classes = this.getDisplayClasses(frame, index); - return serializedFrame; - }); + .then(async stack => { + const data = await this.errorParser.serialize( + stack, + async (frame, index) => { + const serializedFrame = await FrameParser.serializeCodeFrame( + frame, + ); + serializedFrame.classes = this.getDisplayClasses(frame, index); + return serializedFrame; + }, + ); const viewTemplate = fs.readFileSync( path.join( __dirname, diff --git a/src/flub-error-handler.ts b/src/flub-error-handler.ts index 7244299..b3c167a 100644 --- a/src/flub-error-handler.ts +++ b/src/flub-error-handler.ts @@ -1,7 +1,7 @@ -import { Catch, ExceptionFilter, ArgumentsHost } from '@nestjs/common'; +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; +import { Logger } from '@nestjs/common'; import { ErrorHandler } from './error-handler'; import { FlubOptions } from './interfaces'; -import { Logger } from '@nestjs/common'; @Catch(Error) export class FlubErrorHandler implements ExceptionFilter { diff --git a/src/interfaces/flub.options.interface.ts b/src/interfaces/flub.options.interface.ts index 9eabd22..9415726 100644 --- a/src/interfaces/flub.options.interface.ts +++ b/src/interfaces/flub.options.interface.ts @@ -1,4 +1,5 @@ export interface FlubOptions { theme?: string; quote?: boolean; + sourcemap?: boolean; } diff --git a/src/parser/error-parser.ts b/src/parser/error-parser.ts index 3a7ef06..83b6a13 100644 --- a/src/parser/error-parser.ts +++ b/src/parser/error-parser.ts @@ -1,15 +1,17 @@ -import { FlubOptions } from '../interfaces'; import * as stackTrace from 'stack-trace'; +import { FlubOptions } from '../interfaces'; import quotes from './../quotes'; import { FrameParser } from './frame-parser'; export class ErrorParser { public viewQuote: boolean = true; + public resolveSourceMap: boolean = false; private readonly error: Error; constructor(error: Error, options: FlubOptions) { this.error = error; this.viewQuote = options.quote; + this.resolveSourceMap = options.sourcemap; } /** @@ -22,18 +24,29 @@ export class ErrorParser { * * @return {Object} */ - public serialize(stack: object, callback?): object { + public async serialize(stack: object, callback?): Promise { callback = callback || FrameParser.serializeCodeFrame.bind(this); let frames = []; if (stack instanceof Array) { - frames = stack.filter(frame => frame.getFileName()).map(callback); + if (this.resolveSourceMap) { + const resolvedStack = await Promise.all( + stack.map(async frame => await FrameParser.resolveSourceMap(frame)), + ); + frames = await Promise.all( + resolvedStack.filter(frame => frame.getFileName()).map(callback), + ); + } else { + frames = await Promise.all( + stack.filter(frame => frame.getFileName()).map(callback), + ); + } } return { frames, message: this.error.message, name: this.error.name, quote: this.viewQuote ? this.randomQuote() : undefined, - //status: this.error.status, //TODO what's status for? + // status: this.error.status, //TODO what's status for? }; } @@ -47,13 +60,16 @@ export class ErrorParser { return new Promise((resolve, reject) => { const stack = stackTrace.parse(this.error); Promise.all( - stack.map(frame => { + stack.map(async frame => { if (FrameParser.isNode(frame)) { return Promise.resolve(frame); } - return FrameParser.readCodeFrame(frame).then(context => { - frame.context = context; - return frame; + const resolvedFrame = this.resolveSourceMap + ? await FrameParser.resolveSourceMap(frame) + : frame; + return FrameParser.readCodeFrame(resolvedFrame).then(context => { + resolvedFrame.context = context; + return resolvedFrame; }); }), ) diff --git a/src/parser/frame-parser.ts b/src/parser/frame-parser.ts index 2491966..d1b9b65 100644 --- a/src/parser/frame-parser.ts +++ b/src/parser/frame-parser.ts @@ -1,11 +1,42 @@ +import { Logger } from '@nestjs/common'; import * as fs from 'fs'; import * as path from 'path'; -import { StackTraceInterface, FrameInterface } from './../interfaces'; -import { Logger } from '@nestjs/common'; +import { SourceMapConsumer } from 'source-map'; +import { FrameInterface, StackTraceInterface } from './../interfaces'; +import { SyntheticStackTrace } from './synthetic-stack-trace'; export class FrameParser { public static codeContext: number = 7; + /** + * Returns the `StackTrace` + * + */ + public static resolveSourceMap( + frame: StackTraceInterface, + ): Promise { + return new Promise((resolve, reject) => { + fs.readFile( + `${frame.getFileName()}.map`, + 'utf-8', + async (error, contents) => { + if (error) { + return resolve(frame); + } + const consumer = await new SourceMapConsumer(contents); + const originalSourceData = consumer.originalPositionFor({ + column: frame.getColumnNumber(), + line: frame.getLineNumber(), + }); + const stackTrace = new SyntheticStackTrace(frame, originalSourceData); + stackTrace.context = await this.readCodeFrame(stackTrace); + + return resolve(stackTrace); + }, + ); + }); + } + /** * Returns the source code for a given file. If unable to * read file it log a warn and resolves the promise with a null. @@ -15,8 +46,8 @@ export class FrameParser { */ public static async readCodeFrame( frame: StackTraceInterface, - ): Promise { - return new Promise((resolve, reject) => { + ): Promise<{ pre: any; post: any; line: any }> { + return new Promise(async (resolve, reject) => { fs.readFile(frame.getFileName(), 'utf-8', (error, contents) => { if (error) { Logger.warn( @@ -47,7 +78,9 @@ export class FrameParser { * * @return {Object} */ - public static serializeCodeFrame(frame: StackTraceInterface): FrameInterface { + public static async serializeCodeFrame( + frame: StackTraceInterface, + ): Promise { let relativeFileName = frame.getFileName().indexOf(process.cwd()); if (relativeFileName > -1) { relativeFileName = frame @@ -67,6 +100,7 @@ export class FrameParser { method: frame.getFunctionName(), }; } + /** * Serializes frame to a usable as an error object. * diff --git a/src/parser/synthetic-stack-trace.ts b/src/parser/synthetic-stack-trace.ts new file mode 100644 index 0000000..2d0c7e5 --- /dev/null +++ b/src/parser/synthetic-stack-trace.ts @@ -0,0 +1,41 @@ +import { Context, StackTraceInterface } from './../interfaces'; + +export class SyntheticStackTrace implements StackTraceInterface { + context: Context; + frame: StackTraceInterface; + originalSourceData: any; + + constructor(frame, originalSourceData) { + this.frame = frame; + this.originalSourceData = originalSourceData; + } + + get(belowFn?: any) { + return this.frame.get(belowFn); + } + parse(err) { + return this.frame.parse(err); + } + getTypeName() { + return this.frame.getTypeName(); + } + getFunctionName() { + return this.frame.getFunctionName(); + } + getMethodName() { + return this.frame.getMethodName(); + } + getFileName() { + const source = this.originalSourceData.source; + return source ? source.substring(1) : ''; + } + getLineNumber() { + return this.originalSourceData.line; + } + getColumnNumber() { + return this.originalSourceData.column; + } + isNative() { + return this.frame.isNative(); + } +}