mirror of
https://github.com/JonasunderscoreJones/api-worker.git
synced 2025-10-23 10:29:19 +02:00
382 lines
8.7 KiB
JavaScript
382 lines
8.7 KiB
JavaScript
'use strict'
|
|
|
|
/*
|
|
* youch
|
|
*
|
|
* (c) Harminder Virk <virk@adonisjs.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
const cookie = require('cookie')
|
|
const Mustache = require('mustache')
|
|
const { fileURLToPath } = require('url')
|
|
const StackTracey = require('stacktracey')
|
|
const { serialize } = require('v8')
|
|
const VIEW_PATH = './error.compiled.mustache'
|
|
|
|
const viewTemplate = fs.readFileSync(path.join(__dirname, VIEW_PATH), 'utf-8')
|
|
|
|
class Youch {
|
|
constructor (error, request, options = {}) {
|
|
this.options = options
|
|
this.options.postLines = options.postLines || 5
|
|
this.options.preLines = options.preLines || 5
|
|
|
|
this._filterHeaders = ['cookie', 'connection']
|
|
this.error = error
|
|
this.request = request
|
|
this.links = []
|
|
this.showAllFrames = false
|
|
}
|
|
|
|
/**
|
|
* Returns the source code for a given file. It unable to
|
|
* read file it resolves the promise with a null.
|
|
*
|
|
* @param {Object} frame
|
|
* @return {Promise}
|
|
*/
|
|
_getFrameSource (frame) {
|
|
let path = frame
|
|
.file
|
|
.replace(/dist\/webpack:\//g, '') // unix
|
|
.replace(/dist\\webpack:\\/g, '') // windows
|
|
|
|
/**
|
|
* We ignore the error when "fileURLToPath" is unable to parse
|
|
* the path, since returning the frame source is an optional
|
|
* thing
|
|
*/
|
|
try {
|
|
path = path.startsWith('file:') ? fileURLToPath(path) : path
|
|
} catch {
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
fs.readFile(path, 'utf-8', (error, contents) => {
|
|
if (error) {
|
|
resolve(null)
|
|
return
|
|
}
|
|
|
|
const lines = contents.split(/\r?\n/)
|
|
const lineNumber = frame.line
|
|
|
|
resolve({
|
|
pre: lines.slice(
|
|
Math.max(0, lineNumber - (this.options.preLines + 1)),
|
|
lineNumber - 1
|
|
),
|
|
line: lines[lineNumber - 1],
|
|
post: lines.slice(lineNumber, lineNumber + this.options.postLines)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Parses the error stack and returns serialized
|
|
* frames out of it.
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
_parseError () {
|
|
return new Promise((resolve, reject) => {
|
|
const stack = new StackTracey(this.error)
|
|
Promise.all(
|
|
stack.items.map(async (frame) => {
|
|
if (this._isNode(frame)) {
|
|
return Promise.resolve(frame)
|
|
}
|
|
return this._getFrameSource(frame).then((context) => {
|
|
frame.context = context
|
|
return frame
|
|
})
|
|
})
|
|
)
|
|
.then(resolve)
|
|
.catch(reject)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Returns the context with code for a given
|
|
* frame.
|
|
*
|
|
* @param {Object}
|
|
* @return {Object}
|
|
*/
|
|
_getContext (frame) {
|
|
if (!frame.context) {
|
|
return {}
|
|
}
|
|
|
|
return {
|
|
start: frame.line - (frame.context.pre || []).length,
|
|
pre: frame.context.pre.join('\n'),
|
|
line: frame.context.line,
|
|
post: frame.context.post.join('\n')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns classes to be used inside HTML when
|
|
* displaying the frames list.
|
|
*
|
|
* @param {Object}
|
|
* @param {Number}
|
|
*
|
|
* @return {String}
|
|
*/
|
|
_getDisplayClasses (frame) {
|
|
const classes = []
|
|
if (!frame.isApp) {
|
|
classes.push('native-frame')
|
|
}
|
|
|
|
return classes
|
|
}
|
|
|
|
/**
|
|
* Compiles the view using HTML
|
|
*
|
|
* @param {String}
|
|
* @param {Object}
|
|
*
|
|
* @return {String}
|
|
*/
|
|
_compileView (view, data) {
|
|
return Mustache.render(view, data)
|
|
}
|
|
|
|
/**
|
|
* Serializes frame to a usable error object.
|
|
*
|
|
* @param {Object}
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
_serializeFrame (frame) {
|
|
return {
|
|
file: frame.fileRelative,
|
|
filePath: frame.file.startsWith('file:')
|
|
? fileURLToPath(frame.file).replaceAll('\\', '/')
|
|
: frame.file,
|
|
line: frame.line,
|
|
callee: frame.callee,
|
|
calleeShort: frame.calleeShort,
|
|
column: frame.column,
|
|
context: this._getContext(frame),
|
|
isModule: frame.thirdParty,
|
|
isNative: frame.native,
|
|
isApp: this._isApp(frame)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether frame belongs to nodejs
|
|
* or not.
|
|
*
|
|
* @return {Boolean} [description]
|
|
*/
|
|
_isNode (frame) {
|
|
if (frame.native) {
|
|
return true
|
|
}
|
|
|
|
const filename = frame.file || ''
|
|
if (filename.startsWith('node:')) {
|
|
return true
|
|
}
|
|
return false
|
|
|
|
// return !path.isAbsolute(filename) && filename[0] !== '.'
|
|
}
|
|
|
|
/**
|
|
* Returns whether code belongs to the app
|
|
* or not.
|
|
*
|
|
* @return {Boolean} [description]
|
|
*/
|
|
_isApp (frame) {
|
|
return !this._isNode(frame) && !this._isNodeModule(frame)
|
|
}
|
|
|
|
/**
|
|
* Returns whether frame belongs to a node_module or
|
|
* not
|
|
*
|
|
* @method _isNodeModule
|
|
*
|
|
* @param {Object} frame
|
|
*
|
|
* @return {Boolean}
|
|
*
|
|
* @private
|
|
*/
|
|
_isNodeModule (frame) {
|
|
return (frame.file || '').indexOf('node_modules/') > -1
|
|
}
|
|
|
|
/**
|
|
* Serializes stack to Mustache friendly object to
|
|
* be used within the view. Optionally can pass
|
|
* a callback to customize the frames output.
|
|
*
|
|
* @param {Object}
|
|
* @param {Function} [callback]
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
_serializeData (stack, callback) {
|
|
callback = callback || this._serializeFrame.bind(this)
|
|
return {
|
|
message: this.error.message,
|
|
help: this.error.help,
|
|
cause: this.error.cause,
|
|
name: this.error.name,
|
|
status: this.error.status,
|
|
frames:
|
|
stack instanceof Array === true
|
|
? stack.filter((frame) => frame.file).map(callback)
|
|
: []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a serialized object with important
|
|
* information.
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
_serializeRequest () {
|
|
const headers = []
|
|
const cookies = []
|
|
|
|
if (this.request.headers) {
|
|
Object.keys(this.request.headers).forEach((key) => {
|
|
if (this._filterHeaders.indexOf(key) > -1) {
|
|
return
|
|
}
|
|
headers.push({
|
|
key: key.toUpperCase(),
|
|
value: this.request.headers[key]
|
|
})
|
|
})
|
|
|
|
if (this.request.headers.cookie) {
|
|
const parsedCookies = cookie.parse(this.request.headers.cookie || '')
|
|
Object.keys(parsedCookies).forEach((key) => {
|
|
cookies.push({ key, value: parsedCookies[key] })
|
|
})
|
|
}
|
|
}
|
|
|
|
return {
|
|
url: this.request.url,
|
|
httpVersion: this.request.httpVersion,
|
|
method: this.request.method,
|
|
connection: this.request.headers ? this.request.headers.connection : null,
|
|
headers,
|
|
cookies
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stores the link `callback` which
|
|
* will be processed when rendering
|
|
* the HTML view.
|
|
*
|
|
* @param {Function} callback
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
addLink (callback) {
|
|
if (typeof callback === 'function') {
|
|
this.links.push(callback)
|
|
return this
|
|
}
|
|
|
|
throw new Error('Pass a callback function to "addLink"')
|
|
}
|
|
|
|
/**
|
|
* Toggle the state of showing all frames by default
|
|
*/
|
|
toggleShowAllFrames () {
|
|
this.showAllFrames = !this.showAllFrames
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Returns error stack as JSON.
|
|
*
|
|
* @return {Promise}
|
|
*/
|
|
toJSON () {
|
|
return new Promise((resolve, reject) => {
|
|
this._parseError()
|
|
.then((stack) => {
|
|
resolve({
|
|
error: this._serializeData(stack)
|
|
})
|
|
})
|
|
.catch(reject)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Returns HTML representation of the error stack
|
|
* by parsing the stack into frames and getting
|
|
* important info out of it.
|
|
*
|
|
* @return {Promise}
|
|
*/
|
|
toHTML (templateState) {
|
|
return new Promise((resolve, reject) => {
|
|
this._parseError()
|
|
.then((stack) => {
|
|
let foundActiveFrame = false
|
|
|
|
const data = this._serializeData(stack, (frame, index) => {
|
|
const serializedFrame = this._serializeFrame(frame)
|
|
const classes = this._getDisplayClasses(serializedFrame)
|
|
|
|
/**
|
|
* Adding active class to first app framework
|
|
*/
|
|
if (!foundActiveFrame && (serializedFrame.isApp || index + 1 === stack.length)) {
|
|
classes.push('active')
|
|
foundActiveFrame = true
|
|
}
|
|
|
|
serializedFrame.classes = classes.join(' ')
|
|
|
|
return serializedFrame
|
|
})
|
|
|
|
if (templateState) {
|
|
Object.assign(data, templateState)
|
|
}
|
|
|
|
if (this.request) {
|
|
data.request = this._serializeRequest()
|
|
}
|
|
|
|
data.links = this.links.map((renderLink) => renderLink(data))
|
|
data.loadFA = !!data.links.find((link) => link.includes('fa-'))
|
|
data.showAllFrames = this.showAllFrames
|
|
|
|
return resolve(this._compileView(viewTemplate, data))
|
|
})
|
|
.catch(reject)
|
|
})
|
|
}
|
|
}
|
|
|
|
module.exports = Youch
|