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:
parent
d70a554032
commit
be87132327
5 changed files with 105 additions and 16 deletions
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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')
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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": [],
|
||||
|
|
Loading…
Reference in a new issue