Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Typescript source map resolution #30

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
},
"dependencies": {
"mustache": "^3.0.1",
"source-map": "^0.7.3",
"stack-trace": "^0.0.10"
},
"peerDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions src/__tests__/error-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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();
Expand Down
12 changes: 10 additions & 2 deletions src/__tests__/flub-error-handler-e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand All @@ -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 () => {
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/flub-error-handler-e2e-spec.ts.map

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

27 changes: 16 additions & 11 deletions src/error-handler.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/flub-error-handler.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/flub.options.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface FlubOptions {
theme?: string;
quote?: boolean;
sourcemap?: boolean;
}
32 changes: 24 additions & 8 deletions src/parser/error-parser.ts
Original file line number Diff line number Diff line change
@@ -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;
}

/**
Expand All @@ -22,18 +24,29 @@ export class ErrorParser {
*
* @return {Object}
*/
public serialize(stack: object, callback?): object {
public async serialize(stack: object, callback?): Promise<object> {
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?
};
}

Expand All @@ -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;
});
}),
)
Expand Down
44 changes: 39 additions & 5 deletions src/parser/frame-parser.ts
Original file line number Diff line number Diff line change
@@ -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<StackTraceInterface> {
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.
Expand All @@ -15,8 +46,8 @@ export class FrameParser {
*/
public static async readCodeFrame(
frame: StackTraceInterface,
): Promise<object> {
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(
Expand Down Expand Up @@ -47,7 +78,9 @@ export class FrameParser {
*
* @return {Object}
*/
public static serializeCodeFrame(frame: StackTraceInterface): FrameInterface {
public static async serializeCodeFrame(
frame: StackTraceInterface,
): Promise<FrameInterface> {
let relativeFileName = frame.getFileName().indexOf(process.cwd());
if (relativeFileName > -1) {
relativeFileName = frame
Expand All @@ -67,6 +100,7 @@ export class FrameParser {
method: frame.getFunctionName(),
};
}

/**
* Serializes frame to a usable as an error object.
*
Expand Down
41 changes: 41 additions & 0 deletions src/parser/synthetic-stack-trace.ts
Original file line number Diff line number Diff line change
@@ -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) : '<unknown>';
}
getLineNumber() {
return this.originalSourceData.line;
}
getColumnNumber() {
return this.originalSourceData.column;
}
isNative() {
return this.frame.isNative();
}
}