const { AssertionError } = require('assert')
const fs = require('fs')
const url = require('url')
const os = require('os')
const StackParser = require('error-stack-parser')
const { INDENT, IS_BARE } = require('./constants')
const parseStack = StackParser.parse.bind(StackParser)
const IGNORE = typeof __filename === 'string' ? [__filename, __filename.replace(/lib(\/|\\)errors\.js$/, 'index.js')] : []
exports.stringify = stringify
exports.explain = explain
function explain (ok, message, assert, stackStartFunction, actual, expected, top, extra) {
if (ok) return null
const cwd = IS_BARE ? os.cwd() : process.cwd()
const err = new AssertionError({ stackStartFunction, message, operator: assert, actual, expected })
stackScrub(err)
if (top) {
const line = top.getLineNumber()
const column = top.getColumnNumber()
let file = top.getFileName()?.replace(/\?cacheBust=\d+/g, '')
try {
try {
if (url) file = url.fileURLToPath(new URL(file, 'file:'))
} catch {}
if (file.startsWith(cwd)) {
file = file.replace(cwd, '.')
}
if (fs) {
const code = fs.readFileSync(file, { encoding: 'utf-8' })
const split = code.split(/\r?\n/g)
const point = Array.from({ length: column - 1 }).map(() => '-').join('') + '^'
const source = [...split.slice(line - 2, line), point, ...split.slice(line, line + 2)]
err.source = source.join('\n')
} else {
err.source = ''
}
} /* c8 ignore next */ catch {}
}
const { code, generatedMessage, ...info } = err
err.code = code
err.generatedMessage = generatedMessage
Object.defineProperty(info, 'err', { value: err })
if (err.stack) {
info.stack = err.stack.slice(err.toString().length + 1).split('\n').map((line) => {
let match = false
line = line.slice(7).replace(cwd, () => {
match = true
return '.'
})
if (match) line = line.replace(/file:\/?\/?\/?/, '')
return line
}).join('\n').trim()
}
if (!info.stack || code === 'ERR_TIMEOUT' || code === 'ERR_PREMATURE_END' || actual?.code === 'ERR_TIMEOUT' || actual?.code === 'ERR_PREMATURE_END') delete info.stack
if (actual === undefined && expected === undefined) {
delete info.actual
delete info.expected
}
if (info.diff) delete info.diff
return info
}
function stackScrub (err) {
if (err && err.stack) {
const errString = err.toString()
const stack = '\n' + err.stack.slice(errString.length + 1)
const scrubbed = parseStack({ stack }).filter(({ fileName }) => !IGNORE.includes(fileName))
if (scrubbed.length > 0) {
err.stack = `${errString}\n at ${scrubbed.join('\n at ').replace(/\?cacheBust=\d+/g, '')}`
}
}
return err
}
function indent (src) {
return src.split('\n').map(s => INDENT + ' ' + s).join('\n')
}
function stringify (o) {
return indent('---\n' + toYAML(o)).trimRight() + '\n' + INDENT + ' ...'
}
// for the life of me can't find a module that does this, that also works in the browser...
function toYAML (o, maxDepth = 5, indent = '', prev = null) {
if (maxDepth < 0) return indent + '...'
if (typeof o === 'string') {
if (o.indexOf('\n') === -1) return o + '\n'
return '|\n' + o.split('\n').map(s => indent + s).join('\n').trimRight() + '\n'
}
if (Array.isArray(o) || o instanceof Set) {
let s = prev ? '\n' : ''
for (const i of o) {
s += indent + '- ' + toYAML(i, maxDepth - 1, indent + ' ', 'array')
}
return s.trimRight() + '\n'
}
if (o && typeof o === 'object') {
const p = prev && prev !== 'array'
let s = p ? '\n' : ''
for (const k of Object.keys(o)) {
const v = o[k]
s += indent + k + ': ' + toYAML(v, maxDepth - 1, indent + ' ', 'object')
}
return (p ? s.trimRight() : s.trim()) + '\n'
}
return '' + o + '\n'
}