Turbopack: Trace server app render errors through source maps (#62611)

Previously, errors shown in the error overlay, these stir were left
untraced through source maps.

Test Plan: `TURBOPACK=1 pnpm test-dev
test/development/app-render-error-log/app-render-error-log.test.ts`

Closes PACK-2608
This commit is contained in:
Will Binns-Smith 2024-03-01 16:31:02 -08:00 committed by GitHub
parent d70a554032
commit be87132327
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 105 additions and 16 deletions

View file

@ -24,7 +24,7 @@ interface TurbopackStackFrame {
}
const currentSourcesByFile: Map<string, Promise<string | null>> = new Map()
async function batchedTraceSource(
export async function batchedTraceSource(
project: Project,
frame: TurbopackStackFrame
) {

View file

@ -6,6 +6,7 @@ export interface NextError extends Error {
page?: string
code?: string | number
cancelled?: boolean
digest?: number
}
export default function isError(err: unknown): err is NextError {

View file

@ -6,14 +6,14 @@ import type { MiddlewareRouteMatch } from '../../../shared/lib/router/utils/midd
import type { PropagateToWorkersField } from './types'
import type { NextJsHotReloaderInterface } from '../../dev/hot-reloader-types'
import { createDefineEnv } from '../../../build/swc'
import { createDefineEnv, type Project } from '../../../build/swc'
import fs from 'fs'
import url from 'url'
import path from 'path'
import qs from 'querystring'
import Watchpack from 'next/dist/compiled/watchpack'
import { loadEnvConfig } from '@next/env'
import isError from '../../../lib/is-error'
import isError, { type NextError } from '../../../lib/is-error'
import findUp from 'next/dist/compiled/find-up'
import { buildCustomRoute } from './filesystem'
import * as Log from '../../../build/output/log'
@ -64,13 +64,17 @@ import {
getSourceById,
parseStack,
} from '../../../client/components/react-dev-overlay/server/middleware'
import { createOriginalStackFrame as createOriginalTurboStackFrame } from '../../../client/components/react-dev-overlay/server/middleware-turbopack'
import {
batchedTraceSource,
createOriginalStackFrame as createOriginalTurboStackFrame,
} from '../../../client/components/react-dev-overlay/server/middleware-turbopack'
import { devPageFiles } from '../../../build/webpack/plugins/next-types-plugin/shared'
import type { LazyRenderServerInstance } from '../router-server'
import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../dev/hot-reloader-types'
import { PAGE_TYPES } from '../../../lib/page-types'
import { createHotReloaderTurbopack } from '../../dev/hot-reloader-turbopack'
import { getErrorSource } from '../../../shared/lib/error-source'
import type { StackFrame } from 'stacktrace-parser'
export type SetupOpts = {
renderServer: LazyRenderServerInstance
@ -953,17 +957,33 @@ async function startWatcher(opts: SetupOpts) {
Log[type === 'warning' ? 'warn' : 'error'](
`${file} (${lineNumber}:${column}) @ ${methodName}`
)
let errorToLog
if (isEdgeCompiler) {
err = err.message
}
if (type === 'warning') {
Log.warn(err)
} else if (type === 'app-dir') {
logAppDirError(err)
} else if (type) {
Log.error(`${type}:`, err)
errorToLog = err.message
} else if (isError(err) && hotReloader.turbopackProject) {
const stack = await traceTurbopackErrorStack(
hotReloader.turbopackProject,
err,
frames
)
const error: NextError = new Error(err.message)
error.stack = stack
error.digest = err.digest
errorToLog = error
} else {
Log.error(err)
errorToLog = err
}
if (type === 'warning') {
Log.warn(errorToLog)
} else if (type === 'app-dir') {
logAppDirError(errorToLog)
} else if (type) {
Log.error(`${type}:`, errorToLog)
} else {
Log.error(errorToLog)
}
console[type === 'warning' ? 'warn' : 'error'](originalCodeFrame)
usedOriginalStack = true
@ -1036,3 +1056,70 @@ export async function setupDevBundler(opts: SetupOpts) {
}
export type DevBundler = Awaited<ReturnType<typeof setupDevBundler>>
// Returns a trace rewritten through Turbopack's sourcemaps
async function traceTurbopackErrorStack(
project: Project,
error: Error,
frames: StackFrame[]
): Promise<string> {
let originalFrames = await Promise.all(
frames.map(async (f) => {
try {
const traced = await batchedTraceSource(project, {
file: f.file!,
methodName: f.methodName,
line: f.lineNumber ?? 0,
column: f.column,
isServer: true,
})
return traced?.frame ?? f
} catch {
return f
}
})
)
return (
error.name +
': ' +
error.message +
'\n' +
originalFrames
.map((f) => {
if (f == null) {
return null
}
let line = ' at'
if (f.methodName != null) {
line += ' ' + f.methodName
}
if (f.file != null) {
const file =
f.file.startsWith('/') ||
// Built-in "filenames" like `<anonymous>` shouldn't be made relative
f.file.startsWith('<') ||
f.file.startsWith('node:')
? f.file
: `./${f.file}`
line += ` (${file}`
if (f.lineNumber != null) {
line += ':' + f.lineNumber
if (f.column != null) {
line += ':' + f.column
}
}
line += ')'
}
return line
})
.filter(Boolean)
.join('\n')
)
}

View file

@ -17,7 +17,7 @@ createNextDescribe(
await check(() => cliOutput, /digest:/)
expect(cliOutput).toInclude('Error: boom')
expect(cliOutput).toInclude('at fn2 (./app/fn.ts')
expect(cliOutput).toInclude('at fn1 (./app/fn.ts')
expect(cliOutput).toMatch(/at (Module\.)?fn1 \(\.\/app\/fn\.ts/)
expect(cliOutput).toInclude('at Page (./app/page.tsx')
expect(cliOutput).not.toInclude('webpack-internal')

View file

@ -1513,9 +1513,10 @@
"runtimeError": false
},
"test/development/app-render-error-log/app-render-error-log.test.ts": {
"passed": [],
"passed": [
"app-render-error-log should log the correct values on app-render error"
],
"failed": [
"app-render-error-log should log the correct values on app-render error",
"app-render-error-log should log the correct values on app-render error with edge runtime"
],
"pending": [],