@@ -18,6 +18,12 @@ import type { OverlayState } from '../../../../shared'
18
18
import { extractNextErrorCode } from '../../../../../../lib/error-telemetry-utils'
19
19
import { css } from '../../../../utils/css'
20
20
import { getFrameSource } from '../../../../../shared/stack-frame'
21
+ import { Terminal } from '../../../terminal'
22
+ import { HotlinkedText } from '../../../hot-linked-text'
23
+ import { NEXTJS_HYDRATION_ERROR_LINK } from '../../../../../shared/react-19-hydration-error'
24
+ import { PseudoHtmlDiff } from '../../../../container/runtime-error/component-stack-pseudo-html'
25
+ import { CodeFrame } from '../../../code-frame/code-frame'
26
+ import { CallStack } from '../../../call-stack/call-stack'
21
27
22
28
export function IssuesTab ( {
23
29
state,
@@ -38,14 +44,24 @@ export function IssuesTab({
38
44
} ,
39
45
[ activeIdx , runtimeErrors ]
40
46
)
41
- const error = activeError ?. error
42
47
43
- const errorCode = extractNextErrorCode ( error )
44
- const errorType = getErrorTypeLabel ( error , activeError . type )
48
+ if ( ! activeError ) {
49
+ return null
50
+ }
51
+
52
+ // eslint-disable-next-line react-hooks/rules-of-hooks
45
53
const errorDetails = useErrorDetails (
46
54
activeError ?. error ,
47
55
getSquashedHydrationErrorDetails
48
56
)
57
+
58
+ const error = activeError ?. error
59
+
60
+ const errorCode = extractNextErrorCode ( error )
61
+ const errorType = getErrorTypeLabel ( error , activeError . type )
62
+ // TOOD: May be better to always treat everything past the first blank line as notes
63
+ // We're currently only special casing hydration error messages.
64
+ const notes = errorDetails . notes
49
65
const hydrationWarning = errorDetails . hydrationWarning
50
66
const errorMessage = hydrationWarning ? (
51
67
< HydrationErrorDescription message = { hydrationWarning } />
@@ -71,26 +87,129 @@ export function IssuesTab({
71
87
} ) }
72
88
</ Suspense >
73
89
</ aside >
74
- < div className = "nextjs-container-errors-header" >
75
- < div
76
- className = "nextjs__container_errors__error_title"
77
- // allow assertion in tests before error rating is implemented
78
- data-nextjs-error-code = { errorCode }
79
- >
80
- < span data-nextjs-error-label-group >
81
- < ErrorTypeLabel errorType = { errorType } />
82
- { error . environmentName && (
83
- < EnvironmentNameLabel environmentName = { error . environmentName } />
84
- ) }
85
- </ span >
86
- < ErrorOverlayToolbar error = { error } debugInfo = { state . debugInfo } />
90
+ < div data-nextjs-devtools-panel-tab-issues-content >
91
+ < div className = "nextjs-container-errors-header" >
92
+ < div
93
+ className = "nextjs__container_errors__error_title"
94
+ // allow assertion in tests before error rating is implemented
95
+ data-nextjs-error-code = { errorCode }
96
+ >
97
+ < span data-nextjs-error-label-group >
98
+ < ErrorTypeLabel errorType = { errorType } />
99
+ { error . environmentName && (
100
+ < EnvironmentNameLabel environmentName = { error . environmentName } />
101
+ ) }
102
+ </ span >
103
+ < ErrorOverlayToolbar error = { error } debugInfo = { state . debugInfo } />
104
+ </ div >
105
+ < ErrorMessage errorMessage = { errorMessage } />
87
106
</ div >
88
- < ErrorMessage errorMessage = { errorMessage } />
107
+
108
+ < ErrorContent
109
+ buildError = { state . buildError }
110
+ notes = { notes }
111
+ hydrationWarning = { hydrationWarning }
112
+ errorDetails = { errorDetails }
113
+ activeError = { activeError }
114
+ />
89
115
</ div >
90
116
</ div >
91
117
)
92
118
}
93
119
120
+ function ErrorContent ( {
121
+ notes,
122
+ buildError,
123
+ hydrationWarning,
124
+ errorDetails,
125
+ activeError,
126
+ } : {
127
+ notes : string | null
128
+ buildError : OverlayState [ 'buildError' ]
129
+ hydrationWarning : string | null
130
+ errorDetails : {
131
+ hydrationWarning : string | null
132
+ notes : string | null
133
+ reactOutputComponentDiff : string | null
134
+ }
135
+ activeError : ReadyRuntimeError
136
+ } ) {
137
+ if ( buildError ) {
138
+ return < Terminal content = { buildError } />
139
+ }
140
+
141
+ return (
142
+ < >
143
+ < div className = "error-overlay-notes-container" >
144
+ { notes ? (
145
+ < >
146
+ < p
147
+ id = "nextjs__container_errors__notes"
148
+ className = "nextjs__container_errors__notes"
149
+ >
150
+ { notes }
151
+ </ p >
152
+ </ >
153
+ ) : null }
154
+ { hydrationWarning ? (
155
+ < p
156
+ id = "nextjs__container_errors__link"
157
+ className = "nextjs__container_errors__link"
158
+ >
159
+ < HotlinkedText
160
+ text = { `See more info here: ${ NEXTJS_HYDRATION_ERROR_LINK } ` }
161
+ />
162
+ </ p >
163
+ ) : null }
164
+ </ div >
165
+ { errorDetails . reactOutputComponentDiff ? (
166
+ < PseudoHtmlDiff
167
+ reactOutputComponentDiff = { errorDetails . reactOutputComponentDiff || '' }
168
+ />
169
+ ) : null }
170
+ < Suspense fallback = { < div data-nextjs-error-suspended /> } >
171
+ < RuntimeError key = { activeError . id . toString ( ) } error = { activeError } />
172
+ </ Suspense >
173
+ </ >
174
+ )
175
+ }
176
+
177
+ function RuntimeError ( { error } : { error : ReadyRuntimeError } ) {
178
+ const frames = useFrames ( error )
179
+
180
+ const firstFrame = useMemo ( ( ) => {
181
+ const firstFirstPartyFrameIndex = frames . findIndex (
182
+ ( entry ) =>
183
+ ! entry . ignored &&
184
+ Boolean ( entry . originalCodeFrame ) &&
185
+ Boolean ( entry . originalStackFrame )
186
+ )
187
+
188
+ return frames [ firstFirstPartyFrameIndex ] ?? null
189
+ } , [ frames ] )
190
+
191
+ if ( ! firstFrame . originalStackFrame ) {
192
+ return null
193
+ }
194
+
195
+ if ( ! firstFrame . originalCodeFrame ) {
196
+ return null
197
+ }
198
+
199
+ return (
200
+ < >
201
+ { firstFrame && (
202
+ < CodeFrame
203
+ stackFrame = { firstFrame . originalStackFrame }
204
+ codeFrame = { firstFrame . originalCodeFrame }
205
+ />
206
+ ) }
207
+
208
+ { frames . length > 0 && < CallStack frames = { frames } /> }
209
+ </ >
210
+ )
211
+ }
212
+
94
213
function Foo ( {
95
214
runtimeError,
96
215
errorType,
@@ -119,20 +238,20 @@ function Foo({
119
238
120
239
const frameSource = getFrameSource ( firstFrame . originalStackFrame ! )
121
240
return (
122
- < div
241
+ < button
123
242
data-nextjs-devtools-panel-tab-issues-sidebar-frame
124
243
data-nextjs-devtools-panel-tab-issues-sidebar-frame-active = {
125
244
idx === activeIdx
126
245
}
127
246
onClick = { ( ) => setActiveIndex ( idx ) }
128
247
>
129
- < div data-nextjs-devtools-panel-tab-issues-sidebar-frame-error-type >
248
+ < span data-nextjs-devtools-panel-tab-issues-sidebar-frame-error-type >
130
249
{ errorType }
131
- </ div >
132
- < div data-nextjs-devtools-panel-tab-issues-sidebar-frame-source >
250
+ </ span >
251
+ < span data-nextjs-devtools-panel-tab-issues-sidebar-frame-source >
133
252
{ frameSource }
134
- </ div >
135
- </ div >
253
+ </ span >
254
+ </ button >
136
255
)
137
256
}
138
257
@@ -167,9 +286,10 @@ export const DEVTOOLS_PANEL_TAB_ISSUES_STYLES = css`
167
286
}
168
287
169
288
[data-nextjs-devtools-panel-tab-issues-sidebar-frame ] {
289
+ display : flex;
290
+ flex-direction : column;
170
291
padding : 10px 8px ;
171
292
border-radius : var (--rounded-lg );
172
- cursor : pointer;
173
293
transition : background-color 0.2s ease-in-out;
174
294
175
295
& : hover {
@@ -186,18 +306,28 @@ export const DEVTOOLS_PANEL_TAB_ISSUES_STYLES = css`
186
306
}
187
307
188
308
[data-nextjs-devtools-panel-tab-issues-sidebar-frame-error-type ] {
309
+ display : inline-block;
310
+ align-self : flex-start;
189
311
color : var (--color-gray-1000 );
190
312
font-size : var (--size-14 );
191
313
font-weight : 500 ;
192
314
line-height : var (--size-20 );
193
315
}
194
316
195
317
[data-nextjs-devtools-panel-tab-issues-sidebar-frame-source ] {
318
+ display : inline-block;
319
+ align-self : flex-start;
196
320
color : var (--color-gray-900 );
197
321
font-size : var (--size-13 );
198
322
line-height : var (--size-18 );
199
323
}
200
324
325
+ [data-nextjs-devtools-panel-tab-issues-content ] {
326
+ width : 100% ;
327
+ padding : 14px ;
328
+ }
329
+
330
+ /* errors/dialog/header.tsx */
201
331
.nextjs-container-errors-header {
202
332
position : relative;
203
333
}
@@ -230,4 +360,64 @@ export const DEVTOOLS_PANEL_TAB_ISSUES_STYLES = css`
230
360
top : 16px ;
231
361
right : 16px ;
232
362
}
363
+
364
+ /* errors.tsx */
365
+ .nextjs-error-with-static {
366
+ bottom : calc (16px * 4.5 );
367
+ }
368
+ p .nextjs__container_errors__link {
369
+ font-size : var (--size-14 );
370
+ }
371
+ p .nextjs__container_errors__notes {
372
+ color : var (--color-stack-notes );
373
+ font-size : var (--size-14 );
374
+ line-height : 1.5 ;
375
+ }
376
+ .nextjs-container-errors-body > h2 : not (: first-child ) {
377
+ margin-top : calc (16px + 8px );
378
+ }
379
+ .nextjs-container-errors-body > h2 {
380
+ color : var (--color-title-color );
381
+ margin-bottom : 8px ;
382
+ font-size : var (--size-20 );
383
+ }
384
+ .nextjs-toast-errors-parent {
385
+ cursor : pointer;
386
+ transition : transform 0.2s ease;
387
+ }
388
+ .nextjs-toast-errors-parent : hover {
389
+ transform : scale (1.1 );
390
+ }
391
+ .nextjs-toast-errors {
392
+ display : flex;
393
+ align-items : center;
394
+ justify-content : flex-start;
395
+ }
396
+ .nextjs-toast-errors > svg {
397
+ margin-right : 8px ;
398
+ }
399
+ .nextjs-toast-hide-button {
400
+ margin-left : 24px ;
401
+ border : none;
402
+ background : none;
403
+ color : var (--color-ansi-bright-white );
404
+ padding : 0 ;
405
+ transition : opacity 0.25s ease;
406
+ opacity : 0.7 ;
407
+ }
408
+ .nextjs-toast-hide-button : hover {
409
+ opacity : 1 ;
410
+ }
411
+ .nextjs__container_errors__error_title {
412
+ display : flex;
413
+ align-items : center;
414
+ justify-content : space-between;
415
+ margin-bottom : 14px ;
416
+ }
417
+ .error-overlay-notes-container {
418
+ margin : 8px 2px ;
419
+ }
420
+ .error-overlay-notes-container p {
421
+ white-space : pre-wrap;
422
+ }
233
423
`
0 commit comments