@tanstack/devtools-vite 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ function generateConsolePipeCode(levels, viteServerUrl) {
2
+ const levelsArray = JSON.stringify(levels);
3
+ return `
4
+ ;(function __tsdConsolePipe() {
5
+ // Detect environment
6
+ var isServer = typeof window === 'undefined';
7
+ var envKey = isServer ? '__TSD_SERVER_CONSOLE_PIPE_INITIALIZED__' : '__TSD_CONSOLE_PIPE_INITIALIZED__';
8
+ var globalObj = isServer ? globalThis : window;
9
+
10
+ // Only run once per environment
11
+ if (globalObj[envKey]) return;
12
+ globalObj[envKey] = true;
13
+
14
+ var CONSOLE_LEVELS = ${levelsArray};
15
+ var VITE_SERVER_URL = ${JSON.stringify(viteServerUrl)};
16
+
17
+ // Store original console methods before we override them
18
+ var originalConsole = {};
19
+ for (var i = 0; i < CONSOLE_LEVELS.length; i++) {
20
+ var level = CONSOLE_LEVELS[i];
21
+ originalConsole[level] = console[level].bind(console);
22
+ }
23
+
24
+ // Simple inline batcher implementation
25
+ var batchedEntries = [];
26
+ var batchTimeout = null;
27
+ var BATCH_WAIT = isServer ? 50 : 100;
28
+ var BATCH_MAX_SIZE = isServer ? 20 : 50;
29
+
30
+ function flushBatch() {
31
+ if (batchedEntries.length === 0) return;
32
+
33
+ var entries = batchedEntries;
34
+ batchedEntries = [];
35
+ batchTimeout = null;
36
+
37
+ // Determine endpoint based on environment
38
+ var endpoint = isServer
39
+ ? VITE_SERVER_URL + '/__tsd/console-pipe/server'
40
+ : '/__tsd/console-pipe';
41
+
42
+ // Send to Vite server via fetch
43
+ fetch(endpoint, {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify({ entries: entries }),
47
+ }).catch(function( ) {
48
+ // Swallow errors
49
+ });
50
+ }
51
+
52
+ function addToBatch(entry) {
53
+ batchedEntries.push(entry);
54
+
55
+ if (batchedEntries.length >= BATCH_MAX_SIZE) {
56
+ if (batchTimeout) {
57
+ clearTimeout(batchTimeout);
58
+ batchTimeout = null;
59
+ }
60
+ flushBatch();
61
+ } else if (!batchTimeout) {
62
+ batchTimeout = setTimeout(flushBatch, BATCH_WAIT);
63
+ }
64
+ }
65
+
66
+ // Override global console methods
67
+ for (var j = 0; j < CONSOLE_LEVELS.length; j++) {
68
+ (function(level) {
69
+ var original = originalConsole[level];
70
+ console[level] = function() {
71
+ var args = Array.prototype.slice.call(arguments);
72
+
73
+ // Always call original first so logs appear normally
74
+ original.apply(console, args);
75
+
76
+ // Skip our own TSD Console Pipe logs to avoid recursion/noise
77
+ if (args.length > 0 && typeof args[0] === 'string' &&
78
+ (args[0].indexOf('[TSD Console Pipe]') !== -1 ||
79
+ args[0].indexOf('[@tanstack/devtools') !== -1)) {
80
+ return;
81
+ }
82
+
83
+ // Serialize args safely
84
+ var safeArgs = args.map(function(arg) {
85
+ if (arg === undefined) return 'undefined';
86
+ if (arg === null) return null;
87
+ if (typeof arg === 'function') return '[Function]';
88
+ if (typeof arg === 'symbol') return arg.toString();
89
+ try {
90
+ JSON.stringify(arg);
91
+ return arg;
92
+ } catch (e) {
93
+ return String(arg);
94
+ }
95
+ });
96
+
97
+ var entry = {
98
+ level: level,
99
+ args: safeArgs,
100
+ source: isServer ? 'server' : 'client',
101
+ timestamp: Date.now(),
102
+ };
103
+
104
+ addToBatch(entry);
105
+ };
106
+ })(CONSOLE_LEVELS[j]);
107
+ }
108
+
109
+ // CLIENT ONLY: Listen for server console logs via SSE
110
+ if (!isServer) {
111
+ // Transform server log args - strip ANSI codes and convert source paths to clickable URLs
112
+ function transformServerLogArgs(args) {
113
+ var escChar = String.fromCharCode(27);
114
+ var transformed = [];
115
+
116
+ for (var k = 0; k < args.length; k++) {
117
+ var arg = args[k];
118
+ if (typeof arg === 'string') {
119
+ // Strip ANSI escape sequences (ESC[...m patterns)
120
+ var cleaned = arg;
121
+ // Remove ESC character followed by [...m] - need to build regex dynamically
122
+ while (cleaned.indexOf(escChar) !== -1) {
123
+ cleaned = cleaned.split(escChar).join('');
124
+ }
125
+ // Also remove any leftover bracket codes like [35m
126
+ cleaned = cleaned.replace(/\\[[0-9;]*m/g, '');
127
+
128
+ // Transform source paths to clickable URLs
129
+ // Match patterns like /src/components/Header.tsx:17:3
130
+ var sourceRegex = /(\\/[^\\s]+:\\d+:\\d+)/g;
131
+ cleaned = cleaned.replace(sourceRegex, function(match) {
132
+ return window.location.origin + '/__tsd/open-source?source=' + encodeURIComponent(match);
133
+ });
134
+
135
+ if (cleaned.trim()) {
136
+ transformed.push(cleaned);
137
+ }
138
+ } else {
139
+ transformed.push(arg);
140
+ }
141
+ }
142
+
143
+ return transformed;
144
+ }
145
+
146
+ var eventSource = new EventSource('/__tsd/console-pipe/sse');
147
+
148
+ eventSource.onmessage = function(event) {
149
+ try {
150
+ var data = JSON.parse(event.data);
151
+ if (data.entries) {
152
+ for (var m = 0; m < data.entries.length; m++) {
153
+ var entry = data.entries[m];
154
+ var transformedArgs = transformServerLogArgs(entry.args);
155
+ var prefix = '%c[Server]%c';
156
+ var prefixStyle = 'color: #9333ea; font-weight: bold;';
157
+ var resetStyle = 'color: inherit;';
158
+ var logMethod = originalConsole[entry.level] || originalConsole.log;
159
+ logMethod.apply(console, [prefix, prefixStyle, resetStyle].concat(transformedArgs));
160
+ }
161
+ }
162
+ } catch (err) {
163
+ // Swallow errors
164
+ }
165
+ };
166
+
167
+ eventSource.onerror = function() {
168
+ // Swallow errors
169
+ };
170
+
171
+ // Flush on page unload
172
+ window.addEventListener('beforeunload', flushBatch);
173
+ }
174
+ })();
175
+ `;
176
+ }
177
+ export {
178
+ generateConsolePipeCode
179
+ };
180
+ //# sourceMappingURL=virtual-console.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"virtual-console.js","sources":["../../src/virtual-console.ts"],"sourcesContent":["import type { ConsoleLevel } from './plugin'\n\n// export const VIRTUAL_MODULE_ID = 'virtual:tanstack-devtools-console'\n// export const RESOLVED_VIRTUAL_MODULE_ID = '\\0' + VIRTUAL_MODULE_ID\n\n/**\n * Generates inline code to inject into entry files (both client and server).\n * This code detects the environment at runtime and:\n *\n * CLIENT:\n * 1. Store original console methods\n * 2. Create batched wrappers that POST to server via fetch\n * 3. Override global console with the wrapped methods\n * 4. Listen for server console logs via SSE\n *\n * SERVER (Nitro/Vinxi runtime):\n * 1. Store original console methods\n * 2. Create batched wrappers that POST to Vite dev server\n * 3. Override global console - original logging still happens, just also pipes to Vite\n *\n * Returns the inline code as a string - no imports needed since we use fetch.\n */\nexport function generateConsolePipeCode(\n levels: Array<ConsoleLevel>,\n viteServerUrl: string,\n): string {\n const levelsArray = JSON.stringify(levels)\n\n return `\n;(function __tsdConsolePipe() {\n // Detect environment\n var isServer = typeof window === 'undefined';\n var envKey = isServer ? '__TSD_SERVER_CONSOLE_PIPE_INITIALIZED__' : '__TSD_CONSOLE_PIPE_INITIALIZED__';\n var globalObj = isServer ? globalThis : window;\n \n // Only run once per environment\n if (globalObj[envKey]) return;\n globalObj[envKey] = true;\n\n var CONSOLE_LEVELS = ${levelsArray};\n var VITE_SERVER_URL = ${JSON.stringify(viteServerUrl)};\n\n // Store original console methods before we override them\n var originalConsole = {};\n for (var i = 0; i < CONSOLE_LEVELS.length; i++) {\n var level = CONSOLE_LEVELS[i];\n originalConsole[level] = console[level].bind(console);\n }\n\n // Simple inline batcher implementation\n var batchedEntries = [];\n var batchTimeout = null;\n var BATCH_WAIT = isServer ? 50 : 100;\n var BATCH_MAX_SIZE = isServer ? 20 : 50;\n\n function flushBatch() {\n if (batchedEntries.length === 0) return;\n \n var entries = batchedEntries;\n batchedEntries = [];\n batchTimeout = null;\n \n // Determine endpoint based on environment\n var endpoint = isServer \n ? VITE_SERVER_URL + '/__tsd/console-pipe/server'\n : '/__tsd/console-pipe';\n \n // Send to Vite server via fetch\n fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ entries: entries }),\n }).catch(function( ) {\n // Swallow errors \n });\n }\n\n function addToBatch(entry) {\n batchedEntries.push(entry);\n \n if (batchedEntries.length >= BATCH_MAX_SIZE) {\n if (batchTimeout) {\n clearTimeout(batchTimeout);\n batchTimeout = null;\n }\n flushBatch();\n } else if (!batchTimeout) {\n batchTimeout = setTimeout(flushBatch, BATCH_WAIT);\n }\n }\n\n // Override global console methods\n for (var j = 0; j < CONSOLE_LEVELS.length; j++) {\n (function(level) {\n var original = originalConsole[level];\n console[level] = function() {\n var args = Array.prototype.slice.call(arguments);\n \n // Always call original first so logs appear normally\n original.apply(console, args);\n \n // Skip our own TSD Console Pipe logs to avoid recursion/noise\n if (args.length > 0 && typeof args[0] === 'string' && \n (args[0].indexOf('[TSD Console Pipe]') !== -1 || \n args[0].indexOf('[@tanstack/devtools') !== -1)) {\n return;\n }\n\n // Serialize args safely\n var safeArgs = args.map(function(arg) {\n if (arg === undefined) return 'undefined';\n if (arg === null) return null;\n if (typeof arg === 'function') return '[Function]';\n if (typeof arg === 'symbol') return arg.toString();\n try {\n JSON.stringify(arg);\n return arg;\n } catch (e) {\n return String(arg);\n }\n });\n\n var entry = {\n level: level,\n args: safeArgs,\n source: isServer ? 'server' : 'client',\n timestamp: Date.now(),\n };\n \n addToBatch(entry);\n };\n })(CONSOLE_LEVELS[j]);\n }\n\n // CLIENT ONLY: Listen for server console logs via SSE\n if (!isServer) {\n // Transform server log args - strip ANSI codes and convert source paths to clickable URLs\n function transformServerLogArgs(args) {\n var escChar = String.fromCharCode(27);\n var transformed = [];\n \n for (var k = 0; k < args.length; k++) {\n var arg = args[k];\n if (typeof arg === 'string') {\n // Strip ANSI escape sequences (ESC[...m patterns)\n var cleaned = arg;\n // Remove ESC character followed by [...m] - need to build regex dynamically\n while (cleaned.indexOf(escChar) !== -1) {\n cleaned = cleaned.split(escChar).join('');\n }\n // Also remove any leftover bracket codes like [35m\n cleaned = cleaned.replace(/\\\\[[0-9;]*m/g, '');\n \n // Transform source paths to clickable URLs\n // Match patterns like /src/components/Header.tsx:17:3\n var sourceRegex = /(\\\\/[^\\\\s]+:\\\\d+:\\\\d+)/g;\n cleaned = cleaned.replace(sourceRegex, function(match) {\n return window.location.origin + '/__tsd/open-source?source=' + encodeURIComponent(match);\n });\n \n if (cleaned.trim()) {\n transformed.push(cleaned);\n }\n } else {\n transformed.push(arg);\n }\n }\n \n return transformed;\n }\n\n var eventSource = new EventSource('/__tsd/console-pipe/sse');\n \n eventSource.onmessage = function(event) {\n try {\n var data = JSON.parse(event.data);\n if (data.entries) {\n for (var m = 0; m < data.entries.length; m++) {\n var entry = data.entries[m];\n var transformedArgs = transformServerLogArgs(entry.args);\n var prefix = '%c[Server]%c';\n var prefixStyle = 'color: #9333ea; font-weight: bold;';\n var resetStyle = 'color: inherit;';\n var logMethod = originalConsole[entry.level] || originalConsole.log;\n logMethod.apply(console, [prefix, prefixStyle, resetStyle].concat(transformedArgs));\n }\n }\n } catch (err) {\n // Swallow errors \n }\n };\n\n eventSource.onerror = function() {\n // Swallow errors \n };\n\n // Flush on page unload\n window.addEventListener('beforeunload', flushBatch);\n }\n})();\n`\n}\n"],"names":[],"mappings":"AAsBO,SAAS,wBACd,QACA,eACQ;AACR,QAAM,cAAc,KAAK,UAAU,MAAM;AAEzC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAWgB,WAAW;AAAA,0BACV,KAAK,UAAU,aAAa,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiKvD;"}
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/devtools-vite",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "TanStack Vite plugin used to enhance the core devtools with additional functionalities",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -5,7 +5,7 @@ const removeEmptySpace = (str: string) => {
5
5
  return str.replace(/\s/g, '').trim()
6
6
  }
7
7
 
8
- describe('remove-devtools', () => {
8
+ describe('enhance-logs', () => {
9
9
  test('it adds enhanced console.logs to console.log()', () => {
10
10
  const output = removeEmptySpace(
11
11
  enhanceConsoleLog(
@@ -94,7 +94,6 @@ describe('remove-devtools', () => {
94
94
  3000,
95
95
  )!.code,
96
96
  )
97
- console.log('output', output)
98
97
  expect(
99
98
  output.includes(
100
99
  'http://localhost:3000/__tsd/open-source?source=test.jsx',
@@ -114,4 +113,20 @@ describe('remove-devtools', () => {
114
113
  )
115
114
  expect(output).toBe(undefined)
116
115
  })
116
+
117
+ test('it adds enhanced console.log with css formatting to console.log()', () => {
118
+ const output = removeEmptySpace(
119
+ enhanceConsoleLog(
120
+ `
121
+ console.log('This is a log')
122
+ `,
123
+ 'test.jsx',
124
+ 3000,
125
+ )!.code,
126
+ )
127
+ expect(output.includes('color:#A0A')).toEqual(true)
128
+ expect(output.includes('color:#FFF')).toEqual(true)
129
+ expect(output.includes('color:#55F')).toEqual(true)
130
+ expect(output.includes('color:#FFF')).toEqual(true)
131
+ })
117
132
  })
@@ -31,11 +31,42 @@ const transform = (
31
31
  location.start.column,
32
32
  ]
33
33
  const finalPath = `${filePath}:${lineNumber}:${column + 1}`
34
- path.node.arguments.unshift(
34
+ const logMessage = `${chalk.magenta('LOG')} ${chalk.blueBright(`${finalPath}`)}\n → `
35
+
36
+ const serverLogMessage = t.arrayExpression([
37
+ t.stringLiteral(logMessage),
38
+ ])
39
+ const browserLogMessage = t.arrayExpression([
40
+ // LOG with css formatting specifiers: %c
35
41
  t.stringLiteral(
36
- `${chalk.magenta('LOG')} ${chalk.blueBright(`${finalPath} - http://localhost:${port}/__tsd/open-source?source=${encodeURIComponent(finalPath)}`)}\n → `,
42
+ `%c${'LOG'}%c %c${`Go to Source: http://localhost:${port}/__tsd/open-source?source=${encodeURIComponent(finalPath)}`}%c \n → `,
43
+ ),
44
+ // magenta
45
+ t.stringLiteral('color:#A0A'),
46
+ t.stringLiteral('color:#FFF'),
47
+ // blueBright
48
+ t.stringLiteral('color:#55F'),
49
+ t.stringLiteral('color:#FFF'),
50
+ ])
51
+
52
+ // typeof window === "undefined"
53
+ const checkServerCondition = t.binaryExpression(
54
+ '===',
55
+ t.unaryExpression('typeof', t.identifier('window')),
56
+ t.stringLiteral('undefined'),
57
+ )
58
+
59
+ // ...(isServer ? serverLogMessage : browserLogMessage)
60
+ path.node.arguments.unshift(
61
+ t.spreadElement(
62
+ t.conditionalExpression(
63
+ checkServerCondition,
64
+ serverLogMessage,
65
+ browserLogMessage,
66
+ ),
37
67
  ),
38
68
  )
69
+
39
70
  didTransform = true
40
71
  }
41
72
  },
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { devtools, defineDevtoolsConfig } from './plugin'
2
- export type { TanStackDevtoolsViteConfig } from './plugin'
2
+ export type { TanStackDevtoolsViteConfig, ConsoleLevel } from './plugin'
package/src/plugin.ts CHANGED
@@ -2,7 +2,11 @@ import { devtoolsEventClient } from '@tanstack/devtools-client'
2
2
  import { ServerEventBus } from '@tanstack/devtools-event-bus/server'
3
3
  import { normalizePath } from 'vite'
4
4
  import chalk from 'chalk'
5
- import { handleDevToolsViteRequest, readPackageJson } from './utils'
5
+ import {
6
+ handleDevToolsViteRequest,
7
+ readPackageJson,
8
+ stripEnhancedLogPrefix,
9
+ } from './utils'
6
10
  import { DEFAULT_EDITOR_CONFIG, handleOpenSource } from './editor'
7
11
  import { removeDevtools } from './remove-devtools'
8
12
  import { addSourceToJsx } from './inject-source'
@@ -13,10 +17,14 @@ import {
13
17
  emitOutdatedDeps,
14
18
  installPackage,
15
19
  } from './package-manager'
20
+ import { generateConsolePipeCode } from './virtual-console'
21
+ import type { ServerResponse } from 'node:http'
16
22
  import type { Plugin } from 'vite'
17
23
  import type { EditorConfig } from './editor'
18
24
  import type { ServerEventBusConfig } from '@tanstack/devtools-event-bus/server'
19
25
 
26
+ export type ConsoleLevel = 'log' | 'warn' | 'error' | 'info' | 'debug'
27
+
20
28
  export type TanStackDevtoolsViteConfig = {
21
29
  /**
22
30
  * Configuration for the editor integration. Defaults to opening in VS code
@@ -70,6 +78,23 @@ export type TanStackDevtoolsViteConfig = {
70
78
  components?: Array<string | RegExp>
71
79
  }
72
80
  }
81
+ /**
82
+ * Configuration for console piping between client and server.
83
+ * When enabled, console logs from the client will appear in the terminal,
84
+ * and server logs will appear in the browser console.
85
+ */
86
+ consolePiping?: {
87
+ /**
88
+ * Whether to enable console piping.
89
+ * @default true
90
+ */
91
+ enabled?: boolean
92
+ /**
93
+ * Which console methods to pipe.
94
+ * @default ['log', 'warn', 'error', 'info', 'debug']
95
+ */
96
+ levels?: Array<ConsoleLevel>
97
+ }
73
98
  }
74
99
 
75
100
  export const defineDevtoolsConfig = (config: TanStackDevtoolsViteConfig) =>
@@ -82,6 +107,9 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
82
107
  const injectSourceConfig = args?.injectSource ?? { enabled: true }
83
108
  const removeDevtoolsOnBuild = args?.removeDevtoolsOnBuild ?? true
84
109
  const serverBusEnabled = args?.eventBusConfig?.enabled ?? true
110
+ const consolePipingConfig = args?.consolePiping ?? { enabled: true }
111
+ const consolePipingLevels: Array<ConsoleLevel> =
112
+ consolePipingConfig.levels ?? ['log', 'warn', 'error', 'info', 'debug']
85
113
 
86
114
  let devtoolsFileId: string | null = null
87
115
  let devtoolsPort: number | null = null
@@ -175,16 +203,77 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
175
203
  }
176
204
  await editor.open(path, lineNum, columnNum)
177
205
  }
206
+
207
+ // SSE clients for broadcasting server logs to browser
208
+ const sseClients: Array<{
209
+ res: ServerResponse
210
+ id: number
211
+ }> = []
212
+ let sseClientId = 0
213
+ const consolePipingEnabled = consolePipingConfig.enabled ?? true
214
+
178
215
  server.middlewares.use((req, res, next) =>
179
- handleDevToolsViteRequest(req, res, next, (parsedData) => {
180
- const { data, routine } = parsedData
181
- if (routine === 'open-source') {
182
- return handleOpenSource({
183
- data: { type: data.type, data },
184
- openInEditor,
185
- })
186
- }
187
- return
216
+ handleDevToolsViteRequest(req, res, next, {
217
+ onOpenSource: (parsedData) => {
218
+ const { data, routine } = parsedData
219
+ if (routine === 'open-source') {
220
+ return handleOpenSource({
221
+ data: { type: data.type, data },
222
+ openInEditor,
223
+ })
224
+ }
225
+ return
226
+ },
227
+ ...(consolePipingEnabled
228
+ ? {
229
+ onConsolePipe: (entries) => {
230
+ for (const entry of entries) {
231
+ const prefix = chalk.cyan('[Client]')
232
+ const logMethod = console[entry.level as ConsoleLevel]
233
+ const cleanedArgs = stripEnhancedLogPrefix(
234
+ entry.args,
235
+ (loc) => chalk.gray(loc),
236
+ )
237
+ logMethod(prefix, ...cleanedArgs)
238
+ }
239
+ },
240
+ onConsolePipeSSE: (res, req) => {
241
+ res.setHeader('Content-Type', 'text/event-stream')
242
+ res.setHeader('Cache-Control', 'no-cache')
243
+ res.setHeader('Connection', 'keep-alive')
244
+ res.setHeader('Access-Control-Allow-Origin', '*')
245
+ res.flushHeaders()
246
+
247
+ const clientId = ++sseClientId
248
+ sseClients.push({ res, id: clientId })
249
+
250
+ req.on('close', () => {
251
+ const index = sseClients.findIndex(
252
+ (c) => c.id === clientId,
253
+ )
254
+ if (index !== -1) {
255
+ sseClients.splice(index, 1)
256
+ }
257
+ })
258
+ },
259
+ onServerConsolePipe: (entries) => {
260
+ try {
261
+ const data = JSON.stringify({
262
+ entries: entries.map((e) => ({
263
+ level: e.level,
264
+ args: e.args,
265
+ source: 'server',
266
+ timestamp: e.timestamp || Date.now(),
267
+ })),
268
+ })
269
+
270
+ for (const client of sseClients) {
271
+ client.res.write(`data: ${data}\n\n`)
272
+ }
273
+ } catch {}
274
+ },
275
+ }
276
+ : {}),
188
277
  }),
189
278
  )
190
279
  },
@@ -202,7 +291,19 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
202
291
  },
203
292
  enforce: 'pre',
204
293
  transform(code, id) {
205
- if (id.includes('node_modules') || id.includes('?raw')) return
294
+ const devtoolPackages = [
295
+ '@tanstack/react-devtools',
296
+ '@tanstack/preact-devtools',
297
+ '@tanstack/solid-devtools',
298
+ '@tanstack/vue-devtools',
299
+ '@tanstack/devtools',
300
+ ]
301
+ if (
302
+ id.includes('node_modules') ||
303
+ id.includes('?raw') ||
304
+ !devtoolPackages.some((pkg) => code.includes(pkg))
305
+ )
306
+ return
206
307
  const transform = removeDevtools(code, id)
207
308
  if (!transform) return
208
309
  if (logging) {
@@ -405,6 +506,8 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
405
506
  packageJson,
406
507
  })
407
508
  })
509
+
510
+ // Console piping is now handled via HTTP endpoints in the custom-server plugin
408
511
  },
409
512
  async handleHotUpdate({ file }) {
410
513
  if (file.endsWith('package.json')) {
@@ -416,6 +519,56 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
416
519
  }
417
520
  },
418
521
  },
522
+ // Inject console piping code into entry files (both client and server)
523
+ {
524
+ name: '@tanstack/devtools:console-pipe-transform',
525
+ enforce: 'pre',
526
+ apply(config, { command }) {
527
+ return (
528
+ config.mode === 'development' &&
529
+ command === 'serve' &&
530
+ (consolePipingConfig.enabled ?? true)
531
+ )
532
+ },
533
+ transform(code, id) {
534
+ // Inject the console pipe code into entry files
535
+ if (
536
+ id.includes('node_modules') ||
537
+ id.includes('dist') ||
538
+ id.includes('?') ||
539
+ !id.match(/\.(tsx?|jsx?)$/)
540
+ ) {
541
+ return
542
+ }
543
+
544
+ // Only inject once - check if already injected
545
+ if (code.includes('__tsdConsolePipe')) {
546
+ return
547
+ }
548
+
549
+ // Check if this is a root entry file (with <html> JSX or client entry points)
550
+ // In SSR frameworks, this file runs on BOTH server (SSR) and client (hydration)
551
+ // so our runtime check (typeof window === 'undefined') handles both environments
552
+ const isRootEntry =
553
+ /<html[\s>]/i.test(code) ||
554
+ code.includes('StartClient') ||
555
+ code.includes('hydrateRoot') ||
556
+ code.includes('createRoot') ||
557
+ (code.includes('solid-js/web') && code.includes('render('))
558
+
559
+ if (isRootEntry) {
560
+ const viteServerUrl = `http://localhost:${port}`
561
+ const inlineCode = generateConsolePipeCode(
562
+ consolePipingLevels,
563
+ viteServerUrl,
564
+ )
565
+
566
+ return `${inlineCode}\n${code}`
567
+ }
568
+
569
+ return undefined
570
+ },
571
+ },
419
572
  {
420
573
  name: '@tanstack/devtools:better-console-logs',
421
574
  enforce: 'pre',