@stacksjs/error-handling
Version:
Type safe error handling.
3 lines • 5.33 kB
JavaScript
import{HTTP_ERRORS as HTTP_ERROR_MAP,isFrameworkFrame,renderHttpErrorHints}from"./error-page";import{highlightSnippet,languageForFile}from"./error-page-highlighter";import{buildErrorMarkdown,escapeHtml,readCodeSnippet}from"./error-page-template";function parseStackTrace(stack,basePaths,options={}){if(!stack)return[];const lines=stack.split(`
`).slice(1),frames=[],includeAll=options.includeFrameworkFrames===!0;for(const line of lines){const match=line.match(/^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/);if(match){let file=match[2];if(file===void 0)continue;const original=file,isFramework=isFrameworkFrame(original);if(basePaths){for(const basePath of basePaths)if(file.startsWith(basePath)){file=file.slice(basePath.length+1);break}}if(!includeAll&&isFramework)continue;frames.push({function:match[1]||"<anonymous>",file,absoluteFile:original,isFramework,line:parseInt(match[3]??"0",10),column:parseInt(match[4]??"0",10)})}}if(!includeAll&&frames.length===0)return parseStackTrace(stack,basePaths,{includeFrameworkFrames:!0});return frames}function groupFrames(frames){const groups=[];let vendorBatch=[];const flushVendor=()=>{if(vendorBatch.length>0){groups.push(vendorBatch.length===1?vendorBatch[0]:[...vendorBatch]);vendorBatch=[]}};for(const frame of frames)if(frame.isFramework)vendorBatch.push(frame);else{flushVendor();groups.push(frame)}flushVendor();return groups}async function buildFrameView(frame,index,snippetLines){const snippet=readCodeSnippet(frame.absoluteFile,frame.line,snippetLines);let codeHtml="";if(snippet){const code=snippet.lines.map((l)=>l.content).join(`
`),startingLine=snippet.lines[0]?.number??frame.line;codeHtml=(await highlightSnippet(code,frame.file,frame.line,startingLine)).html}return{function:frame.function||"<anonymous>",file:frame.file,line:frame.line,expanded:index===0,hasCode:codeHtml.length>0,codeHtml,isVendor:frame.isFramework}}export async function buildErrorPageViewModel(opts){const{error,status,config}=opts,statusTitle=HTTP_ERROR_MAP[status]?.title??"Error",frames=parseStackTrace(error.stack,config.basePaths,{includeFrameworkFrames:config.showFrameworkFrames===!0}),topFrame=frames[0],runtimeVersion=typeof process<"u"?process.version.replace(/^v/,""):"",runtimeLabel=typeof process<"u"&&process.versions.bun?"BUN":"NODE",errorCode=error.code,traceGroups=[];let frameIndex=0;for(const group of groupFrames(frames))if(Array.isArray(group)){const vendorFrames=await Promise.all(group.map((f,i)=>buildFrameView(f,frameIndex+i,config.snippetLines??8)));traceGroups.push({type:"vendor",frames:vendorFrames,vendorCount:vendorFrames.length});frameIndex+=vendorFrames.length}else{traceGroups.push({type:"frame",frames:[await buildFrameView(group,frameIndex,config.snippetLines??8)],vendorCount:0});frameIndex+=1}const headers=opts.request?Object.entries(opts.request.headers).map(([key,value])=>({key,value})):[],routing=[];if(opts.routing?.controller)routing.push({key:"controller",value:opts.routing.controller});if(opts.routing?.routeName)routing.push({key:"route name",value:opts.routing.routeName});if(opts.routing?.middleware?.length)routing.push({key:"middleware",value:opts.routing.middleware.join(", ")});const userRows=[];if(opts.user?.id!==void 0)userRows.push({key:"id",value:String(opts.user.id)});if(opts.user?.email)userRows.push({key:"email",value:opts.user.email});if(opts.user?.name)userRows.push({key:"name",value:opts.user.name});const environmentRows=config.showEnvironment&&typeof process<"u"?[{key:"runtime",value:process.version},{key:"platform",value:process.platform},{key:"arch",value:process.arch}]:[],queries=(opts.queries??[]).slice(0,100).map((q)=>({connection:q.connection??"default",sql:q.query,sqlHtml:escapeHtml(q.query),time:q.time!==void 0?q.time.toFixed(2):"0.00"})),bodyContent=opts.request?.body?JSON.stringify(opts.request.body,null,2):"// No request body",routeParamsJson=opts.request?.queryParams&&Object.keys(opts.request.queryParams).length>0?JSON.stringify(opts.request.queryParams,null,2):"",markdown=buildErrorMarkdown({statusTitle,errorName:error.name||"Error",errorMessage:error.message,status,file:topFrame?.file,line:topFrame?.line,request:opts.request,framework:opts.framework,frames}),{getSharedHighlighterCss}=await import("./error-page-highlighter");return{pageTitle:statusTitle,statusTitle,markdownJson:JSON.stringify(markdown),exceptionClass:error.name||"Error",message:error.message,fileLine:topFrame?`${topFrame.file}:${topFrame.line}`:"",statusCode:status,errorCode:errorCode!==void 0?String(errorCode):"0",frameworkLabel:opts.framework?.name?.toUpperCase()??"STACKS",frameworkVersion:opts.framework?.version??"",runtimeLabel,runtimeVersion,requestMethod:opts.request?.method??"GET",requestUrl:opts.request?.url??"",hasRequest:!!opts.request,hintsHtml:renderHttpErrorHints(status),traceGroups,queries,queryCount:queries.length,headers,bodyContent,routing,routeParamsJson,userRows,environmentRows,hasUser:userRows.length>0,hasEnvironment:environmentRows.length>0,hasQueries:queries.length>0&&config.showQueries!==!1,hasHeaders:headers.length>0&&config.showRequest!==!1,hasBody:config.showRequest!==!1,hasRouting:routing.length>0,hasRouteParams:routeParamsJson.length>0,highlightCss:getSharedHighlighterCss(),enableCopyMarkdown:config.enableCopyMarkdown!==!1}}export{parseStackTrace,groupFrames,languageForFile};