@tanstack/devtools-vite 0.4.1 → 0.5.1

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.1",
3
+ "version": "0.5.1",
4
4
  "description": "TanStack Vite plugin used to enhance the core devtools with additional functionalities",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  "launch-editor": "^2.11.1",
51
51
  "picomatch": "^4.0.3",
52
52
  "@tanstack/devtools-client": "0.0.5",
53
- "@tanstack/devtools-event-bus": "0.4.0"
53
+ "@tanstack/devtools-event-bus": "0.4.1"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/babel__core": "^7.20.5",
@@ -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(
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,9 +17,16 @@ 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
- import type { ServerEventBusConfig } from '@tanstack/devtools-event-bus/server'
24
+ import type {
25
+ HttpServerLike,
26
+ ServerEventBusConfig,
27
+ } from '@tanstack/devtools-event-bus/server'
28
+
29
+ export type ConsoleLevel = 'log' | 'warn' | 'error' | 'info' | 'debug'
19
30
 
20
31
  export type TanStackDevtoolsViteConfig = {
21
32
  /**
@@ -70,6 +81,23 @@ export type TanStackDevtoolsViteConfig = {
70
81
  components?: Array<string | RegExp>
71
82
  }
72
83
  }
84
+ /**
85
+ * Configuration for console piping between client and server.
86
+ * When enabled, console logs from the client will appear in the terminal,
87
+ * and server logs will appear in the browser console.
88
+ */
89
+ consolePiping?: {
90
+ /**
91
+ * Whether to enable console piping.
92
+ * @default true
93
+ */
94
+ enabled?: boolean
95
+ /**
96
+ * Which console methods to pipe.
97
+ * @default ['log', 'warn', 'error', 'info', 'debug']
98
+ */
99
+ levels?: Array<ConsoleLevel>
100
+ }
73
101
  }
74
102
 
75
103
  export const defineDevtoolsConfig = (config: TanStackDevtoolsViteConfig) =>
@@ -82,9 +110,14 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
82
110
  const injectSourceConfig = args?.injectSource ?? { enabled: true }
83
111
  const removeDevtoolsOnBuild = args?.removeDevtoolsOnBuild ?? true
84
112
  const serverBusEnabled = args?.eventBusConfig?.enabled ?? true
113
+ const consolePipingConfig = args?.consolePiping ?? { enabled: true }
114
+ const consolePipingLevels: Array<ConsoleLevel> =
115
+ consolePipingConfig.levels ?? ['log', 'warn', 'error', 'info', 'debug']
85
116
 
86
117
  let devtoolsFileId: string | null = null
87
118
  let devtoolsPort: number | null = null
119
+ let devtoolsHost: string | null = null
120
+ let devtoolsProtocol: 'http' | 'https' | null = null
88
121
 
89
122
  return [
90
123
  {
@@ -142,9 +175,24 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
142
175
  async configureServer(server) {
143
176
  if (serverBusEnabled) {
144
177
  const preferredPort = args?.eventBusConfig?.port ?? 4206
178
+ const isHttps = !!server.config.server.https
179
+ const serverHost =
180
+ typeof server.config.server.host === 'string'
181
+ ? server.config.server.host
182
+ : 'localhost'
183
+
184
+ devtoolsProtocol = isHttps ? 'https' : 'http'
185
+ devtoolsHost = serverHost
186
+
145
187
  const bus = new ServerEventBus({
146
188
  ...args?.eventBusConfig,
147
189
  port: preferredPort,
190
+ host: serverHost,
191
+ // When HTTPS is enabled, piggyback on Vite's server
192
+ // so WebSocket/SSE connections share the same TLS certificate
193
+ ...(isHttps && server.httpServer
194
+ ? { httpServer: server.httpServer as HttpServerLike }
195
+ : {}),
148
196
  })
149
197
  // start() now handles EADDRINUSE and returns the actual port
150
198
  devtoolsPort = await bus.start()
@@ -175,16 +223,77 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
175
223
  }
176
224
  await editor.open(path, lineNum, columnNum)
177
225
  }
226
+
227
+ // SSE clients for broadcasting server logs to browser
228
+ const sseClients: Array<{
229
+ res: ServerResponse
230
+ id: number
231
+ }> = []
232
+ let sseClientId = 0
233
+ const consolePipingEnabled = consolePipingConfig.enabled ?? true
234
+
178
235
  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
236
+ handleDevToolsViteRequest(req, res, next, {
237
+ onOpenSource: (parsedData) => {
238
+ const { data, routine } = parsedData
239
+ if (routine === 'open-source') {
240
+ return handleOpenSource({
241
+ data: { type: data.type, data },
242
+ openInEditor,
243
+ })
244
+ }
245
+ return
246
+ },
247
+ ...(consolePipingEnabled
248
+ ? {
249
+ onConsolePipe: (entries) => {
250
+ for (const entry of entries) {
251
+ const prefix = chalk.cyan('[Client]')
252
+ const logMethod = console[entry.level as ConsoleLevel]
253
+ const cleanedArgs = stripEnhancedLogPrefix(
254
+ entry.args,
255
+ (loc) => chalk.gray(loc),
256
+ )
257
+ logMethod(prefix, ...cleanedArgs)
258
+ }
259
+ },
260
+ onConsolePipeSSE: (res, req) => {
261
+ res.setHeader('Content-Type', 'text/event-stream')
262
+ res.setHeader('Cache-Control', 'no-cache')
263
+ res.setHeader('Connection', 'keep-alive')
264
+ res.setHeader('Access-Control-Allow-Origin', '*')
265
+ res.flushHeaders()
266
+
267
+ const clientId = ++sseClientId
268
+ sseClients.push({ res, id: clientId })
269
+
270
+ req.on('close', () => {
271
+ const index = sseClients.findIndex(
272
+ (c) => c.id === clientId,
273
+ )
274
+ if (index !== -1) {
275
+ sseClients.splice(index, 1)
276
+ }
277
+ })
278
+ },
279
+ onServerConsolePipe: (entries) => {
280
+ try {
281
+ const data = JSON.stringify({
282
+ entries: entries.map((e) => ({
283
+ level: e.level,
284
+ args: e.args,
285
+ source: 'server',
286
+ timestamp: e.timestamp || Date.now(),
287
+ })),
288
+ })
289
+
290
+ for (const client of sseClients) {
291
+ client.res.write(`data: ${data}\n\n`)
292
+ }
293
+ } catch {}
294
+ },
295
+ }
296
+ : {}),
188
297
  }),
189
298
  )
190
299
  },
@@ -417,6 +526,8 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
417
526
  packageJson,
418
527
  })
419
528
  })
529
+
530
+ // Console piping is now handled via HTTP endpoints in the custom-server plugin
420
531
  },
421
532
  async handleHotUpdate({ file }) {
422
533
  if (file.endsWith('package.json')) {
@@ -428,6 +539,56 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
428
539
  }
429
540
  },
430
541
  },
542
+ // Inject console piping code into entry files (both client and server)
543
+ {
544
+ name: '@tanstack/devtools:console-pipe-transform',
545
+ enforce: 'pre',
546
+ apply(config, { command }) {
547
+ return (
548
+ config.mode === 'development' &&
549
+ command === 'serve' &&
550
+ (consolePipingConfig.enabled ?? true)
551
+ )
552
+ },
553
+ transform(code, id) {
554
+ // Inject the console pipe code into entry files
555
+ if (
556
+ id.includes('node_modules') ||
557
+ id.includes('dist') ||
558
+ id.includes('?') ||
559
+ !id.match(/\.(tsx?|jsx?)$/)
560
+ ) {
561
+ return
562
+ }
563
+
564
+ // Only inject once - check if already injected
565
+ if (code.includes('__tsdConsolePipe')) {
566
+ return
567
+ }
568
+
569
+ // Check if this is a root entry file (with <html> JSX or client entry points)
570
+ // In SSR frameworks, this file runs on BOTH server (SSR) and client (hydration)
571
+ // so our runtime check (typeof window === 'undefined') handles both environments
572
+ const isRootEntry =
573
+ /<html[\s>]/i.test(code) ||
574
+ code.includes('StartClient') ||
575
+ code.includes('hydrateRoot') ||
576
+ code.includes('createRoot') ||
577
+ (code.includes('solid-js/web') && code.includes('render('))
578
+
579
+ if (isRootEntry) {
580
+ const viteServerUrl = `http://localhost:${port}`
581
+ const inlineCode = generateConsolePipeCode(
582
+ consolePipingLevels,
583
+ viteServerUrl,
584
+ )
585
+
586
+ return `${inlineCode}\n${code}`
587
+ }
588
+
589
+ return undefined
590
+ },
591
+ },
431
592
  {
432
593
  name: '@tanstack/devtools:better-console-logs',
433
594
  enforce: 'pre',
@@ -467,22 +628,42 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
467
628
  },
468
629
  },
469
630
  {
470
- name: '@tanstack/devtools:port-injection',
631
+ name: '@tanstack/devtools:connection-injection',
471
632
  apply(config, { command }) {
472
633
  return config.mode === 'development' && command === 'serve'
473
634
  },
474
635
  transform(code, id) {
475
- // Only transform @tanstack packages that contain the port placeholder
476
- if (!code.includes('__TANSTACK_DEVTOOLS_PORT__')) return
636
+ // Only transform @tanstack packages that contain the connection placeholders
637
+ const hasPlaceholder =
638
+ code.includes('__TANSTACK_DEVTOOLS_PORT__') ||
639
+ code.includes('__TANSTACK_DEVTOOLS_HOST__') ||
640
+ code.includes('__TANSTACK_DEVTOOLS_PROTOCOL__')
641
+ if (!hasPlaceholder) return
477
642
  if (
478
643
  !id.includes('@tanstack/devtools') &&
479
644
  !id.includes('@tanstack/event-bus')
480
645
  )
481
646
  return
482
647
 
483
- // Replace placeholder with actual port (or fallback to 4206 if not resolved yet)
648
+ // Replace placeholders with actual values (or fallback defaults)
484
649
  const portValue = devtoolsPort ?? 4206
485
- return code.replace(/__TANSTACK_DEVTOOLS_PORT__/g, String(portValue))
650
+ const hostValue = devtoolsHost ?? 'localhost'
651
+ const protocolValue = devtoolsProtocol ?? 'http'
652
+
653
+ let result = code
654
+ result = result.replace(
655
+ /__TANSTACK_DEVTOOLS_PORT__/g,
656
+ String(portValue),
657
+ )
658
+ result = result.replace(
659
+ /__TANSTACK_DEVTOOLS_HOST__/g,
660
+ JSON.stringify(hostValue),
661
+ )
662
+ result = result.replace(
663
+ /__TANSTACK_DEVTOOLS_PROTOCOL__/g,
664
+ JSON.stringify(protocolValue),
665
+ )
666
+ return result
486
667
  },
487
668
  },
488
669
  ]