1 | const Mustache = require('mustache');
|
2 | const path = require('path');
|
3 | const stackTrace = require('stack-trace');
|
4 | const fs = require('fs-extra');
|
5 | const cookie = require('cookie');
|
6 | const startingSlashRegex = /\\|\//;
|
7 |
|
8 | const T_error = fs.readFileSync(path.join(__dirname, 'resources/error.mustache'), 'utf-8');
|
9 |
|
10 | class HTMLifiedError {
|
11 | constructor(error, request) {
|
12 | this.codeRange = 5;
|
13 | this.skipHeaders = ['cookie', 'connection', 'dnt'];
|
14 | this.error = error;
|
15 | this.request = request;
|
16 | }
|
17 |
|
18 | async provideSnippetForFrame(frame) {
|
19 | const path = frame.getFileName().replace(/dist\/webpack:\//g, '');
|
20 |
|
21 | const content = await fs.readFile(path, 'utf-8');
|
22 | const lines = content.split(/\r?\n/);
|
23 | const lineNumber = frame.getLineNumber();
|
24 |
|
25 | return {
|
26 | pre: lines.slice(Math.max(0, lineNumber - (this.codeRange + 1)), lineNumber - 1),
|
27 | line: lines[lineNumber - 1],
|
28 | post: lines.slice(lineNumber, lineNumber + this.codeRange)
|
29 | };
|
30 | }
|
31 |
|
32 | async parse() {
|
33 | const stack = stackTrace.parse(this.error);
|
34 |
|
35 | return await Promise.all(stack.map(async frame => {
|
36 | if (this.isNode(frame)) {
|
37 | return frame;
|
38 | }
|
39 |
|
40 | const context = await this.provideSnippetForFrame(frame);
|
41 | frame.context = context;
|
42 |
|
43 | return frame;
|
44 | }))
|
45 | }
|
46 |
|
47 | getContext(frame) {
|
48 | if (!frame.context) return {}
|
49 |
|
50 | const { pre, line, post } = frame.context;
|
51 | return {
|
52 | start: frame.getLineNumber() - (pre || []).length,
|
53 | pre: pre.join('\n'),
|
54 | line,
|
55 | post: post.join('\n')
|
56 | }
|
57 | }
|
58 |
|
59 | constructCSSClasses(frame, index) {
|
60 | const classes = [];
|
61 |
|
62 | if (index === 0) classes.push('active');
|
63 | if (!frame.isApp) classes.push('native-frame');
|
64 |
|
65 | return classes.join(' ');
|
66 | }
|
67 |
|
68 | convertFrameToObject(frame) {
|
69 | const relativeFileName = frame.getFileName().indexOf(process.cwd()) > -1
|
70 | ? frame.getFileName().replace(process.cwd(), '').replace(startingSlashRegex, '')
|
71 | : frame.getFileName()
|
72 |
|
73 | return {
|
74 | file: relativeFileName,
|
75 | filePath: frame.getFileName(),
|
76 | method: frame.getFunctionName(),
|
77 | line: frame.getLineNumber(),
|
78 | column: frame.getColumnNumber(),
|
79 | context: this.getContext(frame),
|
80 | isModule: this.isNodeModule(frame),
|
81 | isNative: this.isNode(frame),
|
82 | isApp: this.isApp(frame)
|
83 | }
|
84 | }
|
85 |
|
86 | isNode(frame) {
|
87 | if (frame.isNative()) return true
|
88 |
|
89 | const filename = frame.getFileName() || ''
|
90 | return !path.isAbsolute(filename) && filename[0] !== '.'
|
91 | }
|
92 |
|
93 | isApp(frame) {
|
94 | return !this.isNode(frame) && !this.isNodeModule(frame)
|
95 | }
|
96 |
|
97 | isNodeModule(frame) {
|
98 | return (frame.getFileName() || '').indexOf('node_modules' + path.sep) > -1
|
99 | }
|
100 |
|
101 | convertFramesToObjects(stack, callback) {
|
102 | callback = callback || this._serializeFrame.bind(this)
|
103 |
|
104 | const { message, name, status } = this.error;
|
105 | return {
|
106 | message,
|
107 | name,
|
108 | status,
|
109 | frames: stack instanceof Array === true ? stack.filter((frame) => frame.getFileName()).map(callback) : []
|
110 | }
|
111 | }
|
112 |
|
113 | convertRequestToObject() {
|
114 | const headers = [];
|
115 |
|
116 | for (let header in this.request.headers) {
|
117 | if (this.skipHeaders.includes(header)) continue;
|
118 |
|
119 | headers.push({
|
120 | key: header.toUpperCase(),
|
121 | value: this.request.headers[header]
|
122 | });
|
123 | }
|
124 |
|
125 | const parsedCookies = cookie.parse(this.request.headers.cookie || '');
|
126 | const cookies = Object.keys(parsedCookies).map(key => ({ key, value: parsedCookies[key] }));
|
127 | const { url, method } = this.request;
|
128 |
|
129 | return { url, method, headers, cookies };
|
130 | }
|
131 |
|
132 | async generate() {
|
133 | const stack = await this.parse();
|
134 |
|
135 | const data = this.convertFramesToObjects(stack, (frame, index) => {
|
136 | const serializedFrame = this.convertFrameToObject(frame);
|
137 | serializedFrame.classes = this.constructCSSClasses(serializedFrame, index);
|
138 | return serializedFrame;
|
139 | });
|
140 |
|
141 | data.request = this.convertRequestToObject();
|
142 |
|
143 | return Mustache.render(T_error, data);
|
144 | }
|
145 | }
|
146 |
|
147 | module.exports = HTMLifiedError;
|