mirror of
https://github.com/JonasunderscoreJones/api.jonasjones.dev.git
synced 2025-10-23 03:59:19 +02:00
339 lines
11 KiB
JavaScript
339 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
const O = Object,
|
|
isBrowser = (typeof window !== 'undefined') && (window.window === window) && window.navigator,
|
|
nodeRequire = isBrowser ? null : module.require, // to prevent bundlers from expanding the require call
|
|
lastOf = x => x[x.length - 1],
|
|
getSource = require ('get-source'),
|
|
partition = require ('./impl/partition'),
|
|
asTable = require ('as-table'),
|
|
nixSlashes = x => x.replace (/\\/g, '/'),
|
|
pathRoot = isBrowser ? window.location.href : (nixSlashes (process.cwd ()) + '/')
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
class StackTracey {
|
|
|
|
constructor (input, offset) {
|
|
|
|
const originalInput = input
|
|
, isParseableSyntaxError = input && (input instanceof SyntaxError && !isBrowser)
|
|
|
|
/* new StackTracey () */
|
|
|
|
if (!input) {
|
|
input = new Error ()
|
|
offset = (offset === undefined) ? 1 : offset
|
|
}
|
|
|
|
/* new StackTracey (Error) */
|
|
|
|
if (input instanceof Error) {
|
|
input = input.stack || ''
|
|
}
|
|
|
|
/* new StackTracey (string) */
|
|
|
|
if (typeof input === 'string') {
|
|
input = this.rawParse (input).slice (offset).map (x => this.extractEntryMetadata (x))
|
|
}
|
|
|
|
/* new StackTracey (array) */
|
|
|
|
if (Array.isArray (input)) {
|
|
|
|
if (isParseableSyntaxError) {
|
|
|
|
const rawLines = nodeRequire ('util').inspect (originalInput).split ('\n')
|
|
, fileLine = rawLines[0].split (':')
|
|
, line = fileLine.pop ()
|
|
, file = fileLine.join (':')
|
|
|
|
if (file) {
|
|
input.unshift ({
|
|
file: nixSlashes (file),
|
|
line: line,
|
|
column: (rawLines[2] || '').indexOf ('^') + 1,
|
|
sourceLine: rawLines[1],
|
|
callee: '(syntax error)',
|
|
syntaxError: true
|
|
})
|
|
}
|
|
}
|
|
|
|
this.items = input
|
|
|
|
} else {
|
|
this.items = []
|
|
}
|
|
}
|
|
|
|
extractEntryMetadata (e) {
|
|
|
|
const decomposedPath = this.decomposePath (e.file || '')
|
|
const fileRelative = decomposedPath[0]
|
|
const externalDomain = decomposedPath[1]
|
|
|
|
return O.assign (e, {
|
|
|
|
calleeShort: e.calleeShort || lastOf ((e.callee || '').split ('.')),
|
|
fileRelative: fileRelative,
|
|
fileShort: this.shortenPath (fileRelative),
|
|
fileName: lastOf ((e.file || '').split ('/')),
|
|
thirdParty: this.isThirdParty (fileRelative, externalDomain) && !e.index,
|
|
externalDomain: externalDomain
|
|
})
|
|
}
|
|
|
|
shortenPath (relativePath) {
|
|
return relativePath.replace (/^node_modules\//, '')
|
|
.replace (/^webpack\/bootstrap\//, '')
|
|
.replace (/^__parcel_source_root\//, '')
|
|
}
|
|
|
|
decomposePath (fullPath) {
|
|
let result = fullPath
|
|
|
|
if (isBrowser) result = result.replace (pathRoot, '')
|
|
|
|
const externalDomainMatch = result.match (/^(http|https)\:\/\/?([^\/]+)\/(.*)/)
|
|
const externalDomain = externalDomainMatch ? externalDomainMatch[2] : undefined
|
|
result = externalDomainMatch ? externalDomainMatch[3] : result
|
|
|
|
if (!isBrowser) result = nodeRequire ('path').relative (pathRoot, result)
|
|
|
|
return [
|
|
nixSlashes(result).replace (/^.*\:\/\/?\/?/, ''), // cut webpack:/// and webpack:/ things
|
|
externalDomain
|
|
]
|
|
}
|
|
|
|
isThirdParty (relativePath, externalDomain) {
|
|
return externalDomain ||
|
|
(relativePath[0] === '~') || // webpack-specific heuristic
|
|
(relativePath[0] === '/') || // external source
|
|
(relativePath.indexOf ('node_modules') === 0) ||
|
|
(relativePath.indexOf ('webpack/bootstrap') === 0)
|
|
}
|
|
|
|
rawParse (str) {
|
|
|
|
const lines = (str || '').split ('\n')
|
|
|
|
const entries = lines.map (line => {
|
|
|
|
line = line.trim ()
|
|
|
|
let callee, fileLineColumn = [], native, planA, planB
|
|
|
|
if ((planA = line.match (/at (.+) \(eval at .+ \((.+)\), .+\)/)) || // eval calls
|
|
(planA = line.match (/at (.+) \((.+)\)/)) ||
|
|
((line.slice (0, 3) !== 'at ') && (planA = line.match (/(.*)@(.*)/)))) {
|
|
|
|
callee = planA[1]
|
|
native = (planA[2] === 'native')
|
|
fileLineColumn = (planA[2].match (/(.*):(\d+):(\d+)/) ||
|
|
planA[2].match (/(.*):(\d+)/) || []).slice (1)
|
|
|
|
} else if ((planB = line.match (/^(at\s+)*(.+):(\d+):(\d+)/) )) {
|
|
fileLineColumn = (planB).slice (2)
|
|
|
|
} else {
|
|
return undefined
|
|
}
|
|
|
|
/* Detect things like Array.reduce
|
|
TODO: detect more built-in types */
|
|
|
|
if (callee && !fileLineColumn[0]) {
|
|
const type = callee.split ('.')[0]
|
|
if (type === 'Array') {
|
|
native = true
|
|
}
|
|
}
|
|
|
|
return {
|
|
beforeParse: line,
|
|
callee: callee || '',
|
|
index: isBrowser && (fileLineColumn[0] === window.location.href),
|
|
native: native || false,
|
|
file: nixSlashes (fileLineColumn[0] || ''),
|
|
line: parseInt (fileLineColumn[1] || '', 10) || undefined,
|
|
column: parseInt (fileLineColumn[2] || '', 10) || undefined
|
|
}
|
|
})
|
|
|
|
return entries.filter (x => (x !== undefined))
|
|
}
|
|
|
|
withSourceAt (i) {
|
|
return this.items[i] && this.withSource (this.items[i])
|
|
}
|
|
|
|
withSourceAsyncAt (i) {
|
|
return this.items[i] && this.withSourceAsync (this.items[i])
|
|
}
|
|
|
|
withSource (loc) {
|
|
|
|
if (this.shouldSkipResolving (loc)) {
|
|
return loc
|
|
|
|
} else {
|
|
|
|
let resolved = getSource (loc.file || '').resolve (loc)
|
|
|
|
if (!resolved.sourceFile) {
|
|
return loc
|
|
}
|
|
|
|
return this.withSourceResolved (loc, resolved)
|
|
}
|
|
}
|
|
|
|
withSourceAsync (loc) {
|
|
|
|
if (this.shouldSkipResolving (loc)) {
|
|
return Promise.resolve (loc)
|
|
|
|
} else {
|
|
return getSource.async (loc.file || '')
|
|
.then (x => x.resolve (loc))
|
|
.then (resolved => this.withSourceResolved (loc, resolved))
|
|
.catch (e => this.withSourceResolved (loc, { error: e, sourceLine: '' }))
|
|
}
|
|
}
|
|
|
|
shouldSkipResolving (loc) {
|
|
return loc.sourceFile || loc.error || (loc.file && loc.file.indexOf ('<') >= 0) // skip things like <anonymous> and stuff that was already fetched
|
|
}
|
|
|
|
withSourceResolved (loc, resolved) {
|
|
|
|
if (resolved.sourceFile && !resolved.sourceFile.error) {
|
|
resolved.file = nixSlashes (resolved.sourceFile.path)
|
|
resolved = this.extractEntryMetadata (resolved)
|
|
}
|
|
|
|
if (resolved.sourceLine.includes ('// @hide')) {
|
|
resolved.sourceLine = resolved.sourceLine.replace ('// @hide', '')
|
|
resolved.hide = true
|
|
}
|
|
|
|
if (resolved.sourceLine.includes ('__webpack_require__') || // webpack-specific heuristics
|
|
resolved.sourceLine.includes ('/******/ ({')) {
|
|
resolved.thirdParty = true
|
|
}
|
|
|
|
return O.assign ({ sourceLine: '' }, loc, resolved)
|
|
}
|
|
|
|
withSources () {
|
|
return this.map (x => this.withSource (x))
|
|
}
|
|
|
|
withSourcesAsync () {
|
|
return Promise.all (this.items.map (x => this.withSourceAsync (x)))
|
|
.then (items => new StackTracey (items))
|
|
}
|
|
|
|
mergeRepeatedLines () {
|
|
return new StackTracey (
|
|
partition (this.items, e => e.file + e.line).map (
|
|
group => {
|
|
return group.items.slice (1).reduce ((memo, entry) => {
|
|
memo.callee = (memo.callee || '<anonymous>') + ' → ' + (entry.callee || '<anonymous>')
|
|
memo.calleeShort = (memo.calleeShort || '<anonymous>') + ' → ' + (entry.calleeShort || '<anonymous>')
|
|
return memo
|
|
}, O.assign ({}, group.items[0]))
|
|
}
|
|
)
|
|
)
|
|
}
|
|
|
|
clean () {
|
|
const s = this.withSources ().mergeRepeatedLines ()
|
|
return s.filter (s.isClean.bind (s))
|
|
}
|
|
|
|
cleanAsync () {
|
|
return this.withSourcesAsync ().then (s => {
|
|
s = s.mergeRepeatedLines ()
|
|
return s.filter (s.isClean.bind (s))
|
|
})
|
|
}
|
|
|
|
isClean (entry, index) {
|
|
return (index === 0) || !(entry.thirdParty || entry.hide || entry.native)
|
|
}
|
|
|
|
at (i) {
|
|
return O.assign ({
|
|
|
|
beforeParse: '',
|
|
callee: '<???>',
|
|
index: false,
|
|
native: false,
|
|
file: '<???>',
|
|
line: 0,
|
|
column: 0
|
|
|
|
}, this.items[i])
|
|
}
|
|
|
|
asTable (opts) {
|
|
|
|
const maxColumnWidths = (opts && opts.maxColumnWidths) || this.maxColumnWidths ()
|
|
|
|
const trimEnd = (s, n) => s && ((s.length > n) ? (s.slice (0, n-1) + '…') : s)
|
|
const trimStart = (s, n) => s && ((s.length > n) ? ('…' + s.slice (-(n-1))) : s)
|
|
|
|
const trimmed = this.map (
|
|
e => [
|
|
('at ' + trimEnd (e.calleeShort, maxColumnWidths.callee)),
|
|
trimStart ((e.fileShort && (e.fileShort + ':' + e.line)) || '', maxColumnWidths.file),
|
|
trimEnd (((e.sourceLine || '').trim () || ''), maxColumnWidths.sourceLine)
|
|
]
|
|
)
|
|
|
|
return asTable (trimmed.items)
|
|
}
|
|
|
|
maxColumnWidths () {
|
|
return {
|
|
callee: 30,
|
|
file: 60,
|
|
sourceLine: 80
|
|
}
|
|
}
|
|
|
|
static resetCache () {
|
|
|
|
getSource.resetCache ()
|
|
getSource.async.resetCache ()
|
|
}
|
|
|
|
static locationsEqual (a, b) {
|
|
|
|
return (a.file === b.file) &&
|
|
(a.line === b.line) &&
|
|
(a.column === b.column)
|
|
}
|
|
}
|
|
|
|
/* Array methods
|
|
------------------------------------------------------------------------ */
|
|
|
|
;['map', 'filter', 'slice', 'concat'].forEach (method => {
|
|
|
|
StackTracey.prototype[method] = function (/*...args */) { // no support for ...args in Node v4 :(
|
|
return new StackTracey (this.items[method].apply (this.items, arguments))
|
|
}
|
|
})
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
module.exports = StackTracey
|
|
|