rsnext/packages/next/client/components/react-dev-overlay/internal/container/RuntimeError.tsx

216 lines
6.1 KiB
TypeScript
Raw Normal View History

import * as React from 'react'
import { StackFrame } from 'next/dist/compiled/stacktrace-parser'
import { CodeFrame } from '../components/CodeFrame'
import { ReadyRuntimeError } from '../helpers/getErrorByType'
import { noop as css } from '../helpers/noop-template'
import { getFrameSource, OriginalStackFrame } from '../helpers/stack-frame'
export type RuntimeErrorProps = { error: ReadyRuntimeError }
const CallStackFrame: React.FC<{
frame: OriginalStackFrame
}> = function CallStackFrame({ frame }) {
// TODO: ability to expand resolved frames
// TODO: render error or external indicator
const f: StackFrame = frame.originalStackFrame ?? frame.sourceStackFrame
const hasSource = Boolean(frame.originalCodeFrame)
const open = React.useCallback(() => {
if (!hasSource) return
const params = new URLSearchParams()
for (const key in f) {
params.append(key, ((f as any)[key] ?? '').toString())
}
self
.fetch(
`${
process.env.__NEXT_ROUTER_BASEPATH || ''
}/__nextjs_launch-editor?${params.toString()}`
)
.then(
() => {},
() => {
console.error('There was an issue opening this code in your editor.')
}
)
}, [hasSource, f])
return (
<div data-nextjs-call-stack-frame>
<h6 data-nextjs-frame-expanded={Boolean(frame.expanded)}>
{f.methodName}
</h6>
<div
data-has-source={hasSource ? 'true' : undefined}
tabIndex={hasSource ? 10 : undefined}
role={hasSource ? 'link' : undefined}
onClick={open}
title={hasSource ? 'Click to open in your editor' : undefined}
>
<span>{getFrameSource(f)}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</div>
</div>
)
}
const RuntimeError: React.FC<RuntimeErrorProps> = function RuntimeError({
error,
}) {
const firstFirstPartyFrameIndex = React.useMemo<number>(() => {
return error.frames.findIndex(
(entry) =>
entry.expanded &&
Boolean(entry.originalCodeFrame) &&
Boolean(entry.originalStackFrame)
)
}, [error.frames])
const firstFrame = React.useMemo<OriginalStackFrame | null>(() => {
return error.frames[firstFirstPartyFrameIndex] ?? null
}, [error.frames, firstFirstPartyFrameIndex])
const allLeadingFrames = React.useMemo<OriginalStackFrame[]>(
() =>
firstFirstPartyFrameIndex < 0
? []
: error.frames.slice(0, firstFirstPartyFrameIndex),
[error.frames, firstFirstPartyFrameIndex]
)
const [all, setAll] = React.useState(firstFrame == null)
const toggleAll = React.useCallback(() => {
setAll((v) => !v)
}, [])
const leadingFrames = React.useMemo(
() => allLeadingFrames.filter((f) => f.expanded || all),
[all, allLeadingFrames]
)
const allCallStackFrames = React.useMemo<OriginalStackFrame[]>(
() => error.frames.slice(firstFirstPartyFrameIndex + 1),
[error.frames, firstFirstPartyFrameIndex]
)
const visibleCallStackFrames = React.useMemo<OriginalStackFrame[]>(
() => allCallStackFrames.filter((f) => f.expanded || all),
[all, allCallStackFrames]
)
const canShowMore = React.useMemo<boolean>(() => {
return (
allCallStackFrames.length !== visibleCallStackFrames.length ||
(all && firstFrame != null)
)
}, [
all,
allCallStackFrames.length,
firstFrame,
visibleCallStackFrames.length,
])
return (
<React.Fragment>
{firstFrame ? (
<React.Fragment>
<h5>Source</h5>
{leadingFrames.map((frame, index) => (
<CallStackFrame
key={`leading-frame-${index}-${all}`}
frame={frame}
/>
))}
<CodeFrame
stackFrame={firstFrame.originalStackFrame!}
codeFrame={firstFrame.originalCodeFrame!}
/>
</React.Fragment>
) : undefined}
{visibleCallStackFrames.length ? (
<React.Fragment>
<h5>Call Stack</h5>
{visibleCallStackFrames.map((frame, index) => (
<CallStackFrame key={`call-stack-${index}-${all}`} frame={frame} />
))}
</React.Fragment>
) : undefined}
{canShowMore ? (
<React.Fragment>
<button
tabIndex={10}
data-nextjs-data-runtime-error-collapsed-action
type="button"
onClick={toggleAll}
>
{all ? 'Hide' : 'Show'} collapsed frames
</button>
</React.Fragment>
) : undefined}
</React.Fragment>
)
}
export const styles = css`
button[data-nextjs-data-runtime-error-collapsed-action] {
background: none;
border: none;
padding: 0;
font-size: var(--size-font-small);
line-height: var(--size-font-bigger);
color: var(--color-accents-3);
}
[data-nextjs-call-stack-frame]:not(:last-child) {
margin-bottom: var(--size-gap-double);
}
[data-nextjs-call-stack-frame] > h6 {
margin-top: 0;
margin-bottom: var(--size-gap);
font-family: var(--font-stack-monospace);
color: #222;
}
[data-nextjs-call-stack-frame] > h6[data-nextjs-frame-expanded='false'] {
color: #666;
}
[data-nextjs-call-stack-frame] > div {
display: flex;
align-items: center;
padding-left: calc(var(--size-gap) + var(--size-gap-half));
font-size: var(--size-font-small);
color: #999;
}
[data-nextjs-call-stack-frame] > div > svg {
width: auto;
height: var(--size-font-small);
margin-left: var(--size-gap);
display: none;
}
[data-nextjs-call-stack-frame] > div[data-has-source] {
cursor: pointer;
}
[data-nextjs-call-stack-frame] > div[data-has-source]:hover {
text-decoration: underline dotted;
}
[data-nextjs-call-stack-frame] > div[data-has-source] > svg {
display: unset;
}
`
export { RuntimeError }