| | import type { OverlayState } from '../../../../next-devtools/dev-overlay/shared' |
| | import type { SupportedErrorEvent } from '../../../../next-devtools/dev-overlay/container/runtime-error/render-error' |
| | import { getErrorSource } from '../../../../shared/lib/error-source' |
| | import type { |
| | OriginalStackFramesRequest, |
| | OriginalStackFramesResponse, |
| | } from '../../../../next-devtools/server/shared' |
| |
|
| | type StackFrameForFormatting = { |
| | file: string | null |
| | methodName: string |
| | line1: number | null |
| | column1: number | null |
| | } |
| |
|
| | type StackFrameResolver = ( |
| | request: OriginalStackFramesRequest |
| | ) => Promise<OriginalStackFramesResponse> |
| |
|
| | |
| | let stackFrameResolver: StackFrameResolver | undefined |
| |
|
| | export function setStackFrameResolver(fn: StackFrameResolver) { |
| | stackFrameResolver = fn |
| | } |
| |
|
| | async function resolveStackFrames( |
| | request: OriginalStackFramesRequest |
| | ): Promise<OriginalStackFramesResponse> { |
| | if (!stackFrameResolver) { |
| | throw new Error( |
| | 'Stack frame resolver not initialized. This is a bug in Next.js.' |
| | ) |
| | } |
| | return stackFrameResolver(request) |
| | } |
| |
|
| | const formatStackFrame = (frame: StackFrameForFormatting): string => { |
| | const file = frame.file || '<unknown>' |
| | const method = frame.methodName || '<anonymous>' |
| | const { line1: line, column1: column } = frame |
| | return line && column |
| | ? ` at ${method} (${file}:${line}:${column})` |
| | : line |
| | ? ` at ${method} (${file}:${line})` |
| | : ` at ${method} (${file})` |
| | } |
| |
|
| | const formatErrorFrames = async ( |
| | frames: readonly StackFrameForFormatting[], |
| | context: { |
| | isServer: boolean |
| | isEdgeServer: boolean |
| | isAppDirectory: boolean |
| | } |
| | ): Promise<string> => { |
| | try { |
| | const resolvedFrames = await resolveStackFrames({ |
| | frames: frames.map((frame) => ({ |
| | file: frame.file || null, |
| | methodName: frame.methodName || '<anonymous>', |
| | arguments: [], |
| | line1: frame.line1 || null, |
| | column1: frame.column1 || null, |
| | })), |
| | isServer: context.isServer, |
| | isEdgeServer: context.isEdgeServer, |
| | isAppDirectory: context.isAppDirectory, |
| | }) |
| |
|
| | return ( |
| | resolvedFrames |
| | .filter( |
| | (resolvedFrame) => |
| | !( |
| | resolvedFrame.status === 'fulfilled' && |
| | resolvedFrame.value.originalStackFrame?.ignored |
| | ) |
| | ) |
| | .map((resolvedFrame, j) => |
| | resolvedFrame.status === 'fulfilled' && |
| | resolvedFrame.value.originalStackFrame |
| | ? formatStackFrame(resolvedFrame.value.originalStackFrame) |
| | : formatStackFrame(frames[j]) |
| | ) |
| | .join('\n') + '\n' |
| | ) |
| | } catch { |
| | return frames.map(formatStackFrame).join('\n') + '\n' |
| | } |
| | } |
| |
|
| | async function formatRuntimeError( |
| | errors: readonly SupportedErrorEvent[], |
| | isAppDirectory: boolean |
| | ): Promise<string> { |
| | const formatError = async ( |
| | error: SupportedErrorEvent, |
| | index: number |
| | ): Promise<string> => { |
| | const errorHeader = `\n#### Error ${index + 1} (Type: ${error.type})\n\n` |
| | const errorName = error.error?.name || 'Error' |
| | const errorMsg = error.error?.message || 'Unknown error' |
| | const errorMessage = `**${errorName}**: ${errorMsg}\n\n` |
| |
|
| | if (!error.frames?.length) { |
| | const stack = error.error?.stack || '' |
| | return ( |
| | errorHeader + errorMessage + (stack ? `\`\`\`\n${stack}\n\`\`\`\n` : '') |
| | ) |
| | } |
| |
|
| | const errorSource = getErrorSource(error.error) |
| | const frames = await formatErrorFrames(error.frames, { |
| | isServer: errorSource === 'server', |
| | isEdgeServer: errorSource === 'edge-server', |
| | isAppDirectory, |
| | }) |
| |
|
| | return errorHeader + errorMessage + `\`\`\`\n${frames}\`\`\`\n` |
| | } |
| |
|
| | const formattedErrors = await Promise.all(errors.map(formatError)) |
| | return '### Runtime Errors\n' + formattedErrors.join('\n---\n') |
| | } |
| |
|
| | export async function formatErrors( |
| | errorsByUrl: Map<string, OverlayState>, |
| | nextInstanceErrors: { nextConfig: unknown[] } = { nextConfig: [] } |
| | ): Promise<string> { |
| | let output = '' |
| |
|
| | |
| | if (nextInstanceErrors.nextConfig.length > 0) { |
| | output += `# Next.js Configuration Errors\n\n` |
| | output += `**${nextInstanceErrors.nextConfig.length} error(s) found in next.config**\n\n` |
| |
|
| | nextInstanceErrors.nextConfig.forEach((error, index) => { |
| | output += `## Error ${index + 1}\n\n` |
| | output += '```\n' |
| | if (error instanceof Error) { |
| | output += `${error.name}: ${error.message}\n` |
| | if (error.stack) { |
| | output += error.stack |
| | } |
| | } else { |
| | output += String(error) |
| | } |
| | output += '\n```\n\n' |
| | }) |
| |
|
| | output += '---\n\n' |
| | } |
| |
|
| | |
| | if (errorsByUrl.size > 0) { |
| | output += `# Found errors in ${errorsByUrl.size} browser session(s)\n\n` |
| |
|
| | for (const [url, overlayState] of errorsByUrl) { |
| | const totalErrorCount = |
| | overlayState.errors.length + (overlayState.buildError ? 1 : 0) |
| |
|
| | if (totalErrorCount === 0) continue |
| |
|
| | let displayUrl = url |
| | try { |
| | const urlObj = new URL(url) |
| | displayUrl = urlObj.pathname + urlObj.search + urlObj.hash |
| | } catch { |
| | |
| | } |
| |
|
| | output += `## Session: ${displayUrl}\n\n` |
| | output += `**${totalErrorCount} error(s) found**\n\n` |
| |
|
| | |
| | if (overlayState.buildError) { |
| | output += '### Build Error\n\n' |
| | output += '```\n' |
| | output += overlayState.buildError |
| | output += '\n```\n\n' |
| | } |
| |
|
| | |
| | if (overlayState.errors.length > 0) { |
| | const runtimeErrors = await formatRuntimeError( |
| | overlayState.errors, |
| | overlayState.routerType === 'app' |
| | ) |
| | output += runtimeErrors |
| | output += '\n' |
| | } |
| |
|
| | output += '---\n\n' |
| | } |
| | } |
| |
|
| | return output.trim() |
| | } |
| |
|