mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
[devtools] Force ANSI colors on overlay code frames regardless of TTY (#93550)
### What? The dev overlay's code frame loses syntax highlighting whenever `next dev` runs under a non-TTY parent (VS Code / Cursor's integrated terminal, Docker without `-t`, `concurrently`, the JavaScript Debug Terminal, CI logs, etc.). Tokens render as plain monochrome text instead of the usual colored output. ### Why? The "syntax highlighting" in the overlay isn't real highlighting. The server embeds ANSI escape codes into the code frame string, and the browser overlay parses them via Anser to render colored `<span>`s. Whether the dev server's stdout is a TTY is irrelevant on this path — the consumer is the browser. Since #71860, `getOriginalCodeFrame` defaults `colors` to `process.stdout.isTTY`, and the overlay middlewares relied on that default. The moment stdout was a pipe, the overlay had nothing to color. ### How? `createOriginalStackFrame` and `getOriginalStackFrames` are shared by five consumers (overlay HTTP endpoint, webpack `Module not found` plugin, browser-log forwarding, MCP error formatter, hot-reloader resolver). Hardcoding `colors: true` would leak escape sequences into the four non-overlay paths. Thread a `codeFrameOptions: { colors?, maxWidth? }` parameter through both functions. The overlay HTTP middlewares opt in to `colors: true` and `maxWidth: DEVTOOLS_CODE_FRAME_MAX_WIDTH`; everyone else keeps the existing TTY-gated colors and terminal-width wrapping. (The 1000-column override from #92621 was suffering from the same leak — also scoped to the overlay now.) Tested from Cursor's integrated terminal with/without fix: Before: <img width="1174" height="1070" alt="CleanShot 2026-05-06 at 22 34 45@2x" src="https://github.com/user-attachments/assets/16ef1ac2-749b-443d-9e76-42f1318e9cd1" /> After: <img width="1254" height="1066" alt="CleanShot 2026-05-06 at 22 34 30@2x" src="https://github.com/user-attachments/assets/577ef3c2-3f02-4bb5-9825-63aa5564c593" />
This commit is contained in:
@@ -283,10 +283,22 @@ async function nativeTraceSource(
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Code frame rendering options. The defaults match terminal consumers; only
|
||||
* the overlay HTTP path opts in to always-on colors and the wide max width.
|
||||
*/
|
||||
type CodeFrameOptions = {
|
||||
/** Defaults to `process.stdout.isTTY`. */
|
||||
colors?: boolean
|
||||
/** Defaults to the dev server's terminal width. */
|
||||
maxWidth?: number
|
||||
}
|
||||
|
||||
async function createOriginalStackFrame(
|
||||
project: Project,
|
||||
projectPath: string,
|
||||
frame: TurbopackStackFrame
|
||||
frame: TurbopackStackFrame,
|
||||
codeFrameOptions?: CodeFrameOptions
|
||||
): Promise<OriginalStackFrameResponse | null> {
|
||||
const traced =
|
||||
(await nativeTraceSource(frame)) ??
|
||||
@@ -324,9 +336,8 @@ async function createOriginalStackFrame(
|
||||
get originalCodeFrame() {
|
||||
if (originalCodeFrame === undefined) {
|
||||
originalCodeFrame = getOriginalCodeFrame(tracedFrame, traced.source, {
|
||||
// The overlay renders in a browser with horizontal scrolling,
|
||||
// so don't truncate lines to the server's terminal width.
|
||||
maxWidth: DEVTOOLS_CODE_FRAME_MAX_WIDTH,
|
||||
colors: codeFrameOptions?.colors,
|
||||
maxWidth: codeFrameOptions?.maxWidth,
|
||||
})
|
||||
}
|
||||
return originalCodeFrame
|
||||
@@ -372,6 +383,13 @@ export function getOverlayMiddleware({
|
||||
isServer: request.isServer,
|
||||
isEdgeServer: request.isEdgeServer,
|
||||
isAppDirectory: request.isAppDirectory,
|
||||
codeFrameOptions: {
|
||||
// Overlay parses ANSI in JS and renders in a scrollable
|
||||
// `<pre>`, so colors are always wanted and terminal width is
|
||||
// irrelevant.
|
||||
colors: true,
|
||||
maxWidth: DEVTOOLS_CODE_FRAME_MAX_WIDTH,
|
||||
},
|
||||
})
|
||||
|
||||
ignoreListAnonymousStackFramesIfSandwiched(result)
|
||||
@@ -492,6 +510,7 @@ export async function getOriginalStackFrames({
|
||||
isServer,
|
||||
isEdgeServer,
|
||||
isAppDirectory,
|
||||
codeFrameOptions,
|
||||
}: {
|
||||
project: Project
|
||||
projectPath: string
|
||||
@@ -499,6 +518,7 @@ export async function getOriginalStackFrames({
|
||||
isServer: boolean
|
||||
isEdgeServer: boolean
|
||||
isAppDirectory: boolean
|
||||
codeFrameOptions?: CodeFrameOptions
|
||||
}): Promise<OriginalStackFramesResponse> {
|
||||
const stackFrames = createStackFrames({
|
||||
frames,
|
||||
@@ -513,7 +533,8 @@ export async function getOriginalStackFrames({
|
||||
const stackFrame = await createOriginalStackFrame(
|
||||
project,
|
||||
projectPath,
|
||||
frame
|
||||
frame,
|
||||
codeFrameOptions
|
||||
)
|
||||
if (stackFrame === null) {
|
||||
return {
|
||||
|
||||
@@ -182,12 +182,24 @@ function findOriginalSourcePositionAndContentFromCompilation(
|
||||
return module?.buildInfo?.importLocByPath?.get(importedModule) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Code frame rendering options. The defaults match terminal consumers; only
|
||||
* the overlay HTTP path opts in to always-on colors and the wide max width.
|
||||
*/
|
||||
type CodeFrameOptions = {
|
||||
/** Defaults to `process.stdout.isTTY`. */
|
||||
colors?: boolean
|
||||
/** Defaults to the dev server's terminal width. */
|
||||
maxWidth?: number
|
||||
}
|
||||
|
||||
export async function createOriginalStackFrame({
|
||||
ignoredByDefault,
|
||||
source,
|
||||
rootDirectory,
|
||||
frame,
|
||||
errorMessage,
|
||||
codeFrameOptions,
|
||||
}: {
|
||||
/** setting this to true will not consult ignoreList */
|
||||
ignoredByDefault: boolean
|
||||
@@ -195,6 +207,7 @@ export async function createOriginalStackFrame({
|
||||
rootDirectory: string
|
||||
frame: StackFrame
|
||||
errorMessage?: string
|
||||
codeFrameOptions?: CodeFrameOptions
|
||||
}): Promise<OriginalStackFrameResponse | null> {
|
||||
const moduleNotFound = findModuleNotFoundFromError(errorMessage)
|
||||
const result = await (() => {
|
||||
@@ -263,9 +276,8 @@ export async function createOriginalStackFrame({
|
||||
get originalCodeFrame() {
|
||||
if (originalCodeFrame === undefined) {
|
||||
originalCodeFrame = getOriginalCodeFrame(traced, sourceContent, {
|
||||
// The overlay renders in a browser with horizontal scrolling,
|
||||
// so don't truncate lines to the server's terminal width.
|
||||
maxWidth: DEVTOOLS_CODE_FRAME_MAX_WIDTH,
|
||||
colors: codeFrameOptions?.colors,
|
||||
maxWidth: codeFrameOptions?.maxWidth,
|
||||
})
|
||||
}
|
||||
return originalCodeFrame
|
||||
@@ -393,6 +405,7 @@ export async function getOriginalStackFrames({
|
||||
serverStats,
|
||||
edgeServerStats,
|
||||
rootDirectory,
|
||||
codeFrameOptions,
|
||||
}: {
|
||||
isServer: boolean
|
||||
isEdgeServer: boolean
|
||||
@@ -402,6 +415,7 @@ export async function getOriginalStackFrames({
|
||||
serverStats: () => webpack.Stats | null
|
||||
edgeServerStats: () => webpack.Stats | null
|
||||
rootDirectory: string
|
||||
codeFrameOptions?: CodeFrameOptions
|
||||
}): Promise<OriginalStackFramesResponse> {
|
||||
const frameResponses = await Promise.all(
|
||||
frames.map(
|
||||
@@ -415,6 +429,7 @@ export async function getOriginalStackFrames({
|
||||
serverStats,
|
||||
edgeServerStats,
|
||||
rootDirectory,
|
||||
codeFrameOptions,
|
||||
}).then(
|
||||
(value) => {
|
||||
return {
|
||||
@@ -446,6 +461,7 @@ async function getOriginalStackFrame({
|
||||
serverStats,
|
||||
edgeServerStats,
|
||||
rootDirectory,
|
||||
codeFrameOptions,
|
||||
}: {
|
||||
isServer: boolean
|
||||
isEdgeServer: boolean
|
||||
@@ -455,6 +471,7 @@ async function getOriginalStackFrame({
|
||||
serverStats: () => webpack.Stats | null
|
||||
edgeServerStats: () => webpack.Stats | null
|
||||
rootDirectory: string
|
||||
codeFrameOptions?: CodeFrameOptions
|
||||
}): Promise<OriginalStackFrameResponse> {
|
||||
const filename = frame.file ?? ''
|
||||
const source = await getSource(frame, {
|
||||
@@ -534,6 +551,7 @@ async function getOriginalStackFrame({
|
||||
frame,
|
||||
source,
|
||||
rootDirectory,
|
||||
codeFrameOptions,
|
||||
})
|
||||
|
||||
if (!originalStackFrameResponse) {
|
||||
@@ -601,6 +619,13 @@ export function getOverlayMiddleware(options: {
|
||||
serverStats,
|
||||
edgeServerStats,
|
||||
rootDirectory,
|
||||
codeFrameOptions: {
|
||||
// Overlay parses ANSI in JS and renders in a scrollable
|
||||
// `<pre>`, so colors are always wanted and terminal width
|
||||
// is irrelevant.
|
||||
colors: true,
|
||||
maxWidth: DEVTOOLS_CODE_FRAME_MAX_WIDTH,
|
||||
},
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user