@tanstack/devtools-vite 0.4.1 → 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.1",
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(
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
  },
@@ -417,6 +506,8 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
417
506
  packageJson,
418
507
  })
419
508
  })
509
+
510
+ // Console piping is now handled via HTTP endpoints in the custom-server plugin
420
511
  },
421
512
  async handleHotUpdate({ file }) {
422
513
  if (file.endsWith('package.json')) {
@@ -428,6 +519,56 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
428
519
  }
429
520
  },
430
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
+ },
431
572
  {
432
573
  name: '@tanstack/devtools:better-console-logs',
433
574
  enforce: 'pre',
@@ -0,0 +1,184 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { parseOpenSourceParam, stripEnhancedLogPrefix } from './utils'
3
+
4
+ describe('parseOpenSourceParam', () => {
5
+ test('parses simple file:line:column format', () => {
6
+ const result = parseOpenSourceParam('src/file.tsx:26:13')
7
+ expect(result).toEqual({ file: 'src/file.tsx', line: '26', column: '13' })
8
+ })
9
+
10
+ test('parses file path with multiple slashes', () => {
11
+ const result = parseOpenSourceParam('src/components/Header.tsx:100:5')
12
+ expect(result).toEqual({
13
+ file: 'src/components/Header.tsx',
14
+ line: '100',
15
+ column: '5',
16
+ })
17
+ })
18
+
19
+ test('parses file path with colons in filename (Windows-style)', () => {
20
+ const result = parseOpenSourceParam('C:/Users/test/file.tsx:10:20')
21
+ expect(result).toEqual({
22
+ file: 'C:/Users/test/file.tsx',
23
+ line: '10',
24
+ column: '20',
25
+ })
26
+ })
27
+
28
+ test('returns null for invalid format without line/column', () => {
29
+ const result = parseOpenSourceParam('src/file.tsx')
30
+ expect(result).toBeNull()
31
+ })
32
+
33
+ test('returns null for invalid format with only one number', () => {
34
+ const result = parseOpenSourceParam('src/file.tsx:26')
35
+ expect(result).toBeNull()
36
+ })
37
+ })
38
+
39
+ describe('stripEnhancedLogPrefix', () => {
40
+ test('returns empty array for empty input', () => {
41
+ const result = stripEnhancedLogPrefix([])
42
+ expect(result).toEqual([])
43
+ })
44
+
45
+ test('returns args unchanged when no enhanced log URL is present', () => {
46
+ const args = ['hello', 'world', 123]
47
+ const result = stripEnhancedLogPrefix(args)
48
+ expect(result).toEqual(args)
49
+ })
50
+
51
+ test('extracts source location and user args from enhanced log format', () => {
52
+ // Simulated enhanced log format:
53
+ // ['%cLOG%c %cGo to Source: http://localhost:3000/__tsd/open-source?source=%2Fsrc%2Ffile.tsx%3A26%3A13%c \n → ', 'style1', 'style2', 'style3', 'style4', 'user message']
54
+ const args = [
55
+ '%cLOG%c %cGo to Source: http://localhost:3000/__tsd/open-source?source=%2Fsrc%2Ffile.tsx%3A26%3A13%c \n → ',
56
+ 'color: blue',
57
+ 'color: reset',
58
+ 'color: green',
59
+ 'color: reset',
60
+ 'user message',
61
+ ]
62
+ const result = stripEnhancedLogPrefix(args)
63
+ expect(result).toEqual(['src/file.tsx:26:13', 'user message'])
64
+ })
65
+
66
+ test('handles multiple user args after enhanced log prefix', () => {
67
+ const args = [
68
+ '%cLOG%c %cGo to Source: http://localhost:3000/__tsd/open-source?source=%2Fsrc%2Fcomponents%2FHeader.tsx%3A100%3A5%c \n → ',
69
+ 'style1',
70
+ 'style2',
71
+ 'style3',
72
+ 'style4',
73
+ 'message 1',
74
+ { key: 'value' },
75
+ 42,
76
+ ]
77
+ const result = stripEnhancedLogPrefix(args)
78
+ expect(result).toEqual([
79
+ 'src/components/Header.tsx:100:5',
80
+ 'message 1',
81
+ { key: 'value' },
82
+ 42,
83
+ ])
84
+ })
85
+
86
+ test('handles source location with leading slash by removing it', () => {
87
+ const args = [
88
+ '%cINFO%c %cGo to Source: http://localhost:3000/__tsd/open-source?source=%2Fapp%2Fpage.tsx%3A10%3A1%c \n → ',
89
+ 's1',
90
+ 's2',
91
+ 's3',
92
+ 's4',
93
+ 'info log',
94
+ ]
95
+ const result = stripEnhancedLogPrefix(args)
96
+ // Leading slash is removed
97
+ expect(result).toEqual(['app/page.tsx:10:1', 'info log'])
98
+ })
99
+
100
+ test('applies custom formatter to source location', () => {
101
+ const args = [
102
+ '%cWARN%c %cGo to Source: http://localhost:3000/__tsd/open-source?source=%2Fsrc%2Findex.ts%3A5%3A2%c \n → ',
103
+ 's1',
104
+ 's2',
105
+ 's3',
106
+ 's4',
107
+ 'warning message',
108
+ ]
109
+ const customFormatter = (loc: string) => `[${loc}]`
110
+ const result = stripEnhancedLogPrefix(args, customFormatter)
111
+ expect(result).toEqual(['[src/index.ts:5:2]', 'warning message'])
112
+ })
113
+
114
+ test('handles ANSI prefix format (second variant)', () => {
115
+ // Format: ['\x1b[...]%s\x1b[...]', '%cLOG%c %cGo to Source: ...%c \n → ', 'style...', ..., 'message']
116
+ const args = [
117
+ '\x1b[90m%s\x1b[0m',
118
+ '%cDEBUG%c %cGo to Source: http://localhost:3000/__tsd/open-source?source=%2Fsrc%2Fdebug.ts%3A1%3A1%c \n → ',
119
+ 's1',
120
+ 's2',
121
+ 's3',
122
+ 's4',
123
+ 'debug output',
124
+ ]
125
+ const result = stripEnhancedLogPrefix(args)
126
+ // The source URL is in index 1, so source location should be extracted from there
127
+ expect(result).toEqual(['src/debug.ts:1:1', 'debug output'])
128
+ })
129
+
130
+ test('returns original args when source URL decoding fails', () => {
131
+ const args = [
132
+ '%cLOG%c %cGo to Source: http://localhost:3000/__tsd/open-source?source=%INVALID%c \n → ',
133
+ 's1',
134
+ 's2',
135
+ 's3',
136
+ 's4',
137
+ 'message',
138
+ ]
139
+ // Should gracefully handle decoding error and return user args without source
140
+ const result = stripEnhancedLogPrefix(args)
141
+ // Since decoding fails, sourceLocation will be empty, so result should just have user args
142
+ expect(result).toEqual(['message'])
143
+ })
144
+
145
+ test('handles enhanced log with no user args after style markers', () => {
146
+ const args = [
147
+ 'Go to Source: http://localhost:3000/__tsd/open-source?source=%2Fsrc%2Ftest.ts%3A1%3A1%c',
148
+ 'style-arg', // This is consumed as a style arg due to %c marker
149
+ ]
150
+ const result = stripEnhancedLogPrefix(args)
151
+ // One %c marker means one style arg follows the source arg, leaving no user args
152
+ expect(result).toEqual(['src/test.ts:1:1'])
153
+ })
154
+
155
+ test('handles enhanced log at non-zero index in args array', () => {
156
+ const args = [
157
+ 'some prefix',
158
+ '%cERROR%c %cGo to Source: http://localhost:3000/__tsd/open-source?source=%2Fsrc%2Ferror.ts%3A50%3A10%c \n → ',
159
+ 's1',
160
+ 's2',
161
+ 's3',
162
+ 's4',
163
+ 'error details',
164
+ ]
165
+ const result = stripEnhancedLogPrefix(args)
166
+ expect(result).toEqual(['src/error.ts:50:10', 'error details'])
167
+ })
168
+
169
+ test('handles deeply nested path in source location', () => {
170
+ const args = [
171
+ '%cLOG%c %cGo to Source: http://localhost:5173/__tsd/open-source?source=%2Fpackages%2Fdevtools-vite%2Fsrc%2Fplugin.ts%3A123%3A45%c \n → ',
172
+ 's1',
173
+ 's2',
174
+ 's3',
175
+ 's4',
176
+ 'deep path log',
177
+ ]
178
+ const result = stripEnhancedLogPrefix(args)
179
+ expect(result).toEqual([
180
+ 'packages/devtools-vite/src/plugin.ts:123:45',
181
+ 'deep path log',
182
+ ])
183
+ })
184
+ })