From cdf7f66ab42f2db56b4f5fd8c2f6176e2eb171ab Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 2 Oct 2026 15:08:57 +0200 Subject: [PATCH 1/2] fix(devtools-bundler-core): do not pipe Vite-forwarded browser logs back to the browser Vite 8 `server.forwardConsole` prints browser console calls in the terminal as "[console.] ...". In an SSR app the console pipe also patches the server console, so it sent that line to the browser as a server log. The browser printed it, Vite forwarded it again, and the loop never stopped. The server side of the pipe now skips lines in Vite's forwarded format. Vite turns forwardConsole on by default when it detects an AI agent terminal, which is why the flood showed up in Cursor and not in iTerm. Fixes #482 --- .changeset/console-pipe-vite-forward-loop.md | 6 +++ .../src/virtual-console.test.ts | 53 +++++++++++++++++++ .../src/virtual-console.ts | 9 ++++ 3 files changed, 68 insertions(+) create mode 100644 .changeset/console-pipe-vite-forward-loop.md diff --git a/.changeset/console-pipe-vite-forward-loop.md b/.changeset/console-pipe-vite-forward-loop.md new file mode 100644 index 000000000..306bddb99 --- /dev/null +++ b/.changeset/console-pipe-vite-forward-loop.md @@ -0,0 +1,6 @@ +--- +'@tanstack/devtools-bundler-core': patch +'@tanstack/devtools-vite': patch +--- + +Stop the console pipe feedback loop with Vite 8 `server.forwardConsole`. The server side of the pipe no longer sends browser logs that Vite printed in the terminal back to the browser. diff --git a/packages/devtools-bundler-core/src/virtual-console.test.ts b/packages/devtools-bundler-core/src/virtual-console.test.ts index 31788c278..5d4cb980a 100644 --- a/packages/devtools-bundler-core/src/virtual-console.test.ts +++ b/packages/devtools-bundler-core/src/virtual-console.test.ts @@ -7,8 +7,30 @@ afterEach(() => { vi.useRealTimers() vi.unstubAllGlobals() delete (window as any).__TSD_CONSOLE_PIPE_INITIALIZED__ + delete (globalThis as any).__TSD_SERVER_CONSOLE_PIPE_INITIALIZED__ }) +function setupServerErrorConsolePipe() { + const originalError = console.error + const originalErrorMock = vi.fn() + const fetchMock = vi.fn().mockResolvedValue(undefined) + + console.error = originalErrorMock + vi.stubGlobal('fetch', fetchMock) + + // Shadow `window` so the generated code takes its server branch. + const code = generateConsolePipeCode(['error'], TEST_VITE_URL) + new Function('window', code)(undefined) + + return { + fetchMock, + originalErrorMock, + restore: () => { + console.error = originalError + }, + } +} + function setupWarnConsolePipe() { const originalWarn = console.warn const originalWarnMock = vi.fn() @@ -241,4 +263,35 @@ describe('virtual-console', () => { restore() } }) + + test('does not send browser logs that Vite forwarded to the terminal back to the browser', async () => { + vi.useFakeTimers() + + const { fetchMock, originalErrorMock, restore } = + setupServerErrorConsolePipe() + + try { + // The shape of a Vite 8 `server.forwardConsole` line. + const forwarded = + '\x1b[2m9:27:59 PM\x1b[22m \x1b[36m\x1b[1m[vite]\x1b[22m\x1b[39m \x1b[2m(client)\x1b[22m \x1b[2m[console.error] \x1b[22m[Server] seed' + + console.error(forwarded) + await vi.advanceTimersByTimeAsync(100) + + expect(originalErrorMock).toHaveBeenCalledWith(forwarded) + expect(fetchMock).not.toHaveBeenCalled() + + console.error('real server error') + await vi.advanceTimersByTimeAsync(100) + + expect(fetchMock).toHaveBeenCalledTimes(1) + expect(getFirstFetchBody(fetchMock).entries[0]).toMatchObject({ + level: 'error', + source: 'server', + args: ['real server error'], + }) + } finally { + restore() + } + }) }) diff --git a/packages/devtools-bundler-core/src/virtual-console.ts b/packages/devtools-bundler-core/src/virtual-console.ts index 04aa02527..e4ce4451b 100644 --- a/packages/devtools-bundler-core/src/virtual-console.ts +++ b/packages/devtools-bundler-core/src/virtual-console.ts @@ -251,6 +251,15 @@ export function generateConsolePipeCode( return; } + // Vite 8 \`server.forwardConsole\` prints browser console calls in the + // terminal as "[console.] ...". Sending those back to the browser + // as server logs makes the browser log them again, and Vite forwards + // them again, without end. + if (isServer && args.length > 0 && typeof args[0] === 'string' && + /\\[console\\.[a-z]+\\] /.test(args[0])) { + return; + } + var safeArgs = args.map(function(arg) { try { return serializeConsoleArg(arg, [], 0); From 0178b2a0130a893d581431f4479cc4549b50b9aa Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 2 Oct 2026 17:35:35 +0200 Subject: [PATCH 2/2] fix(devtools-bundler-core): match the whole Vite-forwarded log shape The guard skipped any server log with "[console.] " in it, so a real server log such as "request hit [console.error] detail" never reached the browser. It now needs "[vite]", "(client)", and "[console.] " in that order, which is how Vite 8 prints a forwarded browser log. --- .../devtools-bundler-core/src/virtual-console.test.ts | 5 +++-- packages/devtools-bundler-core/src/virtual-console.ts | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/devtools-bundler-core/src/virtual-console.test.ts b/packages/devtools-bundler-core/src/virtual-console.test.ts index 5d4cb980a..ec5ff9001 100644 --- a/packages/devtools-bundler-core/src/virtual-console.test.ts +++ b/packages/devtools-bundler-core/src/virtual-console.test.ts @@ -281,14 +281,15 @@ describe('virtual-console', () => { expect(originalErrorMock).toHaveBeenCalledWith(forwarded) expect(fetchMock).not.toHaveBeenCalled() - console.error('real server error') + // A server log that only mentions the marker is still sent. + console.error('request hit [console.error] detail') await vi.advanceTimersByTimeAsync(100) expect(fetchMock).toHaveBeenCalledTimes(1) expect(getFirstFetchBody(fetchMock).entries[0]).toMatchObject({ level: 'error', source: 'server', - args: ['real server error'], + args: ['request hit [console.error] detail'], }) } finally { restore() diff --git a/packages/devtools-bundler-core/src/virtual-console.ts b/packages/devtools-bundler-core/src/virtual-console.ts index e4ce4451b..a221006e6 100644 --- a/packages/devtools-bundler-core/src/virtual-console.ts +++ b/packages/devtools-bundler-core/src/virtual-console.ts @@ -252,11 +252,11 @@ export function generateConsolePipeCode( } // Vite 8 \`server.forwardConsole\` prints browser console calls in the - // terminal as "[console.] ...". Sending those back to the browser - // as server logs makes the browser log them again, and Vite forwards - // them again, without end. + // terminal as "