Repository: vercel/next.js
Pull request: Skip dimming when `--inspect` is used

When the Node.js inspector is active (e.g. via `next dev --inspect`), dimming wraps console arguments in a format string which defeats inspector affordances such as collapsible objects and clickable/linkified stack traces.

This adds an early return in `convertToDimmedArgs` that skips dimming entirely when `inspector.url()` is defined. Terminal output without `--inspect` is unchanged.

Ideally we would only skip dimming when a debugger frontend is actually attached, but Node.js does not expose a synchronous API for that — detecting it would require async polling of the `/json/list` HTTP endpoint.

Test plan:
- Unit tests in `console-dim.external.test.ts`
- Manual: dimming almost never triggers now since we switched to in-band validation (no separate render pass). To reproduce manually, use a client component that calls `console.error(new Error(...))` during SSR, e.g.:
  1. `__NEXT_CACHE_COMPONENTS=true pnpm next dev test/e2e/app-dir/server-source-maps/fixtures/default --inspect`
  2. Open `http://localhost:3000/` in Chrome
  3. Click the Node.js DevTools button
  4. Load `http://localhost:3000/ssr-error-log`
  5. Verify the first error in DevTools has source-mapped, expandable stack frames

Before:

<img width="1496" height="848" alt="before" src="https://github.com/user-attachments/assets/8b4358e1-06ec-4078-b6cc-269986e295e7" />

After:

<img width="1496" height="842" alt="after" src="https://github.com/user-attachments/assets/42f33c6f-f025-410f-b44c-2ccd88fe0cfe" />

Changed files:
filename: packages/next/src/server/node-environment-extensions/console-dim.external.test.ts
patch: @@ -357,4 +357,61 @@ describe('console-exit patches', () => {
       ])
     })
   })
+
+  describe('inspector-aware dimming', () => {
+    it('should skip dimming when inspector is open', async () => {
+      async function testForWorker() {
+        const nodeInspector = require('node:inspector')
+        nodeInspector.open(0)
+
+        const {
+          consoleAsyncStorage,
+        } = require('next/dist/server/app-render/console-async-storage.external')
+
+        const capturedCalls: Array<{ args: any[] }> = []
+        console.error = function (...args) {
+          capturedCalls.push({ args })
+        }
+
+        require('next/dist/server/node-environment-extensions/console-dim.external')
+
+        consoleAsyncStorage.run({ dim: true }, () => {
+          console.error('prefix', { key: 'value' }, 42)
+        })
+
+        nodeInspector.close()
+
+        const call = capturedCalls[0]
+
+        reportResult({
+          type: 'serialized',
+          key: 'argCount',
+          data: JSON.stringify(call.args.length),
+        })
+        reportResult({
+          type: 'serialized',
+          key: 'firstArg',
+          data: JSON.stringify(call.args[0]),
+        })
+        reportResult({
+          type: 'serialized',
+          key: 'secondArg',
+          data: JSON.stringify(call.args[1]),
+        })
+        reportResult({
+          type: 'serialized',
+          key: 'thirdArg',
+          data: JSON.stringify(call.args[2]),
+        })
+      }
+
+      const { data, exitCode } = await runWorkerCode(testForWorker)
+
+      expect(exitCode).toBe(0)
+      expect(data.argCount).toBe(3)
+      expect(data.firstArg).toBe('prefix')
+      expect(data.secondArg).toEqual({ key: 'value' })
+      expect(data.thirdArg).toBe(42)
+    })
+  })
 })

filename: packages/next/src/server/node-environment-extensions/console-dim.external.tsx
patch: @@ -1,3 +1,4 @@
+import * as inspector from 'node:inspector'
 import { dim } from '../../lib/picocolors'
 import {
   consoleAsyncStorage,
@@ -142,6 +143,16 @@ function convertToDimmedArgs(
   methodName: InterceptableConsoleMethod,
   args: any[]
 ): any[] {
+  // When the Node.js inspector is open (e.g. --inspect), skip dimming entirely.
+  // Dimming wraps arguments in a format string which defeats inspector
+  // affordances such as collapsible objects and clickable/linkified stack
+  // traces. Ideally we would only skip dimming when a debugger frontend is
+  // actually attached, but Node.js does not expose a synchronous API for that.
+  // Detecting would require async polling of the /json/list HTTP endpoint.
+  if (inspector.url() !== undefined) {
+    return args
+  }
+
   switch (methodName) {
     case 'dir':
     case 'dirxml':

filename: test/e2e/app-dir/server-source-maps/fixtures/default/app/ssr-error-log/page.js
patch: @@ -0,0 +1,11 @@
+'use client'
+
+function logError() {
+  const error = new Error('ssr-error-log')
+  console.error(error)
+}
+
+export default function Page() {
+  logError()
+  return null
+}

filename: test/e2e/app-dir/server-source-maps/server-source-maps.test.ts
patch: @@ -269,6 +269,50 @@ describe('app-dir - server source maps', () => {
     }
   })
 
+  it('logged errors in client components during ssr have a sourcemapped stack with a codeframe', async () => {
+    if (isNextDev) {
+      const outputIndex = next.cliOutput.length
+      await next.render('/ssr-error-log')
+
+      await retry(() => {
+        expect(next.cliOutput.slice(outputIndex)).toContain(
+          'Error: ssr-error-log'
+        )
+      })
+      expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
+        'Error: ssr-error-log' +
+          '\n    at logError (app/ssr-error-log/page.js:4:17)' +
+          '\n    at Page (app/ssr-error-log/page.js:9:3)' +
+          '\n  2 |' +
+          '\n  3 | function logError() {' +
+          "\n> 4 |   const error = new Error('ssr-error-log')" +
+          '\n    |                 ^' +
+          '\n  5 |   console.error(error)' +
+          '\n  6 | }' +
+          '\n  7 |' +
+          '\n'
+      )
+    } else {
+      if (isTurbopack) {
+        // TODO(veil): Sourcemap names
+        expect(normalizeCliOutput(next.cliOutput)).toContain(
+          'Error: ssr-error-log' +
+            '\n    at <unknown> (app/ssr-error-log/page.js:4:17)' +
+            '\n  2 |' +
+            '\n  3 | function logError() {' +
+            "\n> 4 |   const error = new Error('ssr-error-log')" +
+            '\n    |                 ^' +
+            '\n  5 |   console.error(error)' +
+            '\n  6 | }' +
+            '\n  7 |' +
+            '\n'
+        )
+      } else {
+        // TODO(veil): line/column numbers are flaky in Webpack
+      }
+    }
+  })
+
   it('stack frames are ignore-listed in ssr', async () => {
     if (isNextDev) {
       const outputIndex = next.cliOutput.length