import codeExcerpt, { type CodeExcerpt } from 'code-excerpt'; import { readFileSync } from 'fs'; import React from 'react'; import StackUtils from 'stack-utils'; import Box from './Box.js'; import Text from './Text.js'; /* eslint-disable custom-rules/no-process-cwd -- stack trace file:// paths are relative to the real OS cwd, not the virtual cwd */ // Error's source file is reported as file:///home/user/file.js // This function removes the file://[cwd] part const cleanupPath = (path: string | undefined): string | undefined => { return path?.replace(`file://${process.cwd()}/`, ''); }; let stackUtils: StackUtils | undefined; function getStackUtils(): StackUtils { return (stackUtils ??= new StackUtils({ cwd: process.cwd(), internals: StackUtils.nodeInternals(), })); } /* eslint-enable custom-rules/no-process-cwd */ type ErrorLike = { readonly message: string; readonly stack?: string; }; type Props = { readonly error: ErrorLike; }; export default function ErrorOverview({ error }: Props) { const stack = error.stack ? error.stack.split('\n').slice(1) : undefined; const origin = stack ? getStackUtils().parseLine(stack[0]!) : undefined; const filePath = cleanupPath(origin?.file); let excerpt: CodeExcerpt[] | undefined; let lineWidth = 0; if (filePath && origin?.line) { try { // eslint-disable-next-line custom-rules/no-sync-fs -- sync render path; error overlay can't go async without suspense restructuring const sourceCode = readFileSync(filePath, 'utf8'); excerpt = codeExcerpt(sourceCode, origin.line); if (excerpt) { for (const { line } of excerpt) { lineWidth = Math.max(lineWidth, String(line).length); } } } catch { // file not readable — skip source context } } return ( {' '} ERROR{' '} {error.message} {origin && filePath && ( {filePath}:{origin.line}:{origin.column} )} {origin && excerpt && ( {excerpt.map(({ line, value }) => ( {String(line).padStart(lineWidth, ' ')}: {' ' + value} ))} )} {error.stack && ( {error.stack .split('\n') .slice(1) .map(line => { const parsedLine = getStackUtils().parseLine(line); // If the line from the stack cannot be parsed, we print out the unparsed line. if (!parsedLine) { return ( - {line} ); } return ( - {parsedLine.function} {' '} ({cleanupPath(parsedLine.file) ?? ''}:{parsedLine.line}:{parsedLine.column}) ); })} )} ); }