Skip to content

Commit 4c62c30

Browse files
refactor(error-overlay): improve server code for webpack/Turbopack middleware (#62396)
1 parent 1ad3963 commit 4c62c30

File tree

7 files changed

+232
-328
lines changed

7 files changed

+232
-328
lines changed

packages/next/src/build/swc/index.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -600,12 +600,16 @@ export interface HmrIdentifiers {
600600
identifiers: string[]
601601
}
602602

603-
interface TurbopackStackFrame {
604-
column: number | null
605-
file: string
603+
/** @see https://github.com/vercel/next.js/blob/415cd74b9a220b6f50da64da68c13043e9b02995/packages/next-swc/crates/napi/src/next_api/project.rs#L824-L833 */
604+
export interface TurbopackStackFrame {
606605
isServer: boolean
607-
line: number
608-
methodName: string | null
606+
isInternal?: boolean
607+
file: string
608+
/** 1-indexed, unlike source map tokens */
609+
line?: number
610+
/** 1-indexed, unlike source map tokens */
611+
column?: number | null
612+
methodName?: string
609613
}
610614

611615
export type UpdateMessage =

packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,21 @@ async function getSourceFrame(
4949
compilation: any
5050
): Promise<{ frame: string; lineNumber: string; column: string }> {
5151
try {
52-
const loc = input.loc
53-
? input.loc
54-
: input.dependencies.map((d: any) => d.loc).filter(Boolean)[0]
52+
const loc =
53+
input.loc || input.dependencies.map((d: any) => d.loc).filter(Boolean)[0]
5554
const originalSource = input.module.originalSource()
5655

5756
const result = await createOriginalStackFrame({
58-
line: loc.start.line,
59-
column: loc.start.column,
6057
source: originalSource,
6158
rootDirectory: compilation.options.context!,
6259
modulePath: fileName,
63-
frame: {},
60+
frame: {
61+
arguments: [],
62+
file: fileName,
63+
methodName: '',
64+
lineNumber: loc.start.line,
65+
column: loc.start.column,
66+
},
6467
})
6568

6669
return {

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import React from 'react'
21
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
32
import {
43
getFrameSource,
Lines changed: 37 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,44 @@
11
import type { IncomingMessage, ServerResponse } from 'http'
2-
import { findSourcePackage, type OriginalStackFrameResponse } from './shared'
2+
import {
3+
badRequest,
4+
findSourcePackage,
5+
getOriginalCodeFrame,
6+
internalServerError,
7+
json,
8+
noContent,
9+
type OriginalStackFrameResponse,
10+
} from './shared'
311

412
import fs, { constants as FS } from 'fs/promises'
5-
import { codeFrameColumns } from 'next/dist/compiled/babel/code-frame'
613
import { launchEditor } from '../internal/helpers/launchEditor'
7-
8-
interface Project {
9-
getSourceForAsset(filePath: string): Promise<string | null>
10-
traceSource(
11-
stackFrame: TurbopackStackFrame
12-
): Promise<TurbopackStackFrame | null>
13-
}
14-
15-
interface TurbopackStackFrame {
16-
// 1-based
17-
column: number | null
18-
// 1-based
19-
file: string
20-
isServer: boolean
21-
line: number | null
22-
methodName: string | null
23-
isInternal?: boolean
24-
}
14+
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
15+
import type { Project, TurbopackStackFrame } from '../../../../build/swc'
2516

2617
const currentSourcesByFile: Map<string, Promise<string | null>> = new Map()
2718
export async function batchedTraceSource(
2819
project: Project,
2920
frame: TurbopackStackFrame
30-
) {
21+
): Promise<{ frame: StackFrame; source: string | null } | undefined> {
3122
const file = frame.file ? decodeURIComponent(frame.file) : undefined
32-
if (!file) {
33-
return
34-
}
23+
if (!file) return
3524

3625
const sourceFrame = await project.traceSource(frame)
37-
38-
if (!sourceFrame) {
39-
return
40-
}
41-
42-
let source
43-
// Don't show code frames for node_modules. These can also often be large bundled files.
44-
if (!sourceFrame.file.includes('node_modules') && !sourceFrame.isInternal) {
26+
if (!sourceFrame) return
27+
28+
let source = null
29+
// Don't look up source for node_modules or internals. These can often be large bundled files.
30+
if (
31+
sourceFrame.file &&
32+
!(sourceFrame.file.includes('node_modules') || sourceFrame.isInternal)
33+
) {
4534
let sourcePromise = currentSourcesByFile.get(sourceFrame.file)
4635
if (!sourcePromise) {
4736
sourcePromise = project.getSourceForAsset(sourceFrame.file)
4837
currentSourcesByFile.set(sourceFrame.file, sourcePromise)
4938
setTimeout(() => {
5039
// Cache file reads for 100ms, as frames will often reference the same
5140
// files and can be large.
52-
currentSourcesByFile.delete(sourceFrame.file)
41+
currentSourcesByFile.delete(sourceFrame.file!)
5342
}, 100)
5443
}
5544

@@ -59,12 +48,12 @@ export async function batchedTraceSource(
5948
return {
6049
frame: {
6150
file: sourceFrame.file,
62-
lineNumber: sourceFrame.line,
63-
column: sourceFrame.column,
51+
lineNumber: sourceFrame.line ?? 0,
52+
column: sourceFrame.column ?? 0,
6453
methodName: sourceFrame.methodName ?? frame.methodName ?? '<unknown>',
6554
arguments: [],
6655
},
67-
source: source ?? null,
56+
source,
6857
}
6958
}
7059

@@ -74,29 +63,15 @@ export async function createOriginalStackFrame(
7463
): Promise<OriginalStackFrameResponse | null> {
7564
const traced = await batchedTraceSource(project, frame)
7665
if (!traced) {
77-
const sourcePackage = findSourcePackage(frame.file)
66+
const sourcePackage = findSourcePackage(frame)
7867
if (sourcePackage) return { sourcePackage }
7968
return null
8069
}
8170

8271
return {
8372
originalStackFrame: traced.frame,
84-
originalCodeFrame:
85-
traced.source === null
86-
? null
87-
: codeFrameColumns(
88-
traced.source,
89-
{
90-
start: {
91-
// 1-based, but -1 means start line without highlighting
92-
line: traced.frame.lineNumber ?? -1,
93-
// 1-based, but 0 means whole line without column highlighting
94-
column: traced.frame.column ?? 0,
95-
},
96-
},
97-
{ forceColor: true }
98-
),
99-
sourcePackage: findSourcePackage(traced.frame.file),
73+
originalCodeFrame: getOriginalCodeFrame(traced.frame, traced.source),
74+
sourcePackage: findSourcePackage(traced.frame),
10075
}
10176
}
10277

@@ -106,7 +81,7 @@ export function getOverlayMiddleware(project: Project) {
10681

10782
const frame = {
10883
file: searchParams.get('file') as string,
109-
methodName: searchParams.get('methodName'),
84+
methodName: searchParams.get('methodName') ?? '<unknown>',
11085
line: parseInt(searchParams.get('lineNumber') ?? '0', 10) || 0,
11186
column: parseInt(searchParams.get('column') ?? '0', 10) || 0,
11287
isServer: searchParams.get('isServer') === 'true',
@@ -117,55 +92,32 @@ export function getOverlayMiddleware(project: Project) {
11792
try {
11893
originalStackFrame = await createOriginalStackFrame(project, frame)
11994
} catch (e: any) {
120-
res.statusCode = 500
121-
res.write(e.message)
122-
res.end()
123-
return
95+
return internalServerError(res, e.message)
12496
}
12597

126-
if (originalStackFrame === null) {
98+
if (!originalStackFrame) {
12799
res.statusCode = 404
128-
res.write('Unable to resolve sourcemap')
129-
res.end()
130-
return
100+
return res.end('Unable to resolve sourcemap')
131101
}
132102

133-
res.statusCode = 200
134-
res.setHeader('Content-Type', 'application/json')
135-
res.write(Buffer.from(JSON.stringify(originalStackFrame)))
136-
res.end()
137-
return
103+
return json(res, originalStackFrame)
138104
} else if (pathname === '/__nextjs_launch-editor') {
139-
if (!frame.file) {
140-
res.statusCode = 400
141-
res.write('Bad Request')
142-
res.end()
143-
return
144-
}
105+
if (!frame.file) return badRequest(res)
145106

146107
const fileExists = await fs.access(frame.file, FS.F_OK).then(
147108
() => true,
148109
() => false
149110
)
150-
if (!fileExists) {
151-
res.statusCode = 204
152-
res.write('No Content')
153-
res.end()
154-
return
155-
}
111+
if (!fileExists) return noContent(res)
156112

157113
try {
158114
launchEditor(frame.file, frame.line ?? 1, frame.column ?? 1)
159115
} catch (err) {
160116
console.log('Failed to launch editor:', err)
161-
res.statusCode = 500
162-
res.write('Internal Server Error')
163-
res.end()
164-
return
117+
return internalServerError(res)
165118
}
166119

167-
res.statusCode = 204
168-
res.end()
120+
noContent(res)
169121
}
170122
}
171123
}

0 commit comments

Comments
 (0)