@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.
package/src/utils.ts CHANGED
@@ -4,12 +4,31 @@ import type { Connect } from 'vite'
4
4
  import type { IncomingMessage, ServerResponse } from 'node:http'
5
5
  import type { PackageJson } from '@tanstack/devtools-client'
6
6
 
7
+ type DevToolsRequestHandler = (data: any) => void
8
+
9
+ type DevToolsViteRequestOptions = {
10
+ onOpenSource?: DevToolsRequestHandler
11
+ onConsolePipe?: (entries: Array<any>) => void
12
+ onServerConsolePipe?: (entries: Array<any>) => void
13
+ onConsolePipeSSE?: (
14
+ res: ServerResponse<IncomingMessage>,
15
+ req: Connect.IncomingMessage,
16
+ ) => void
17
+ }
18
+
7
19
  export const handleDevToolsViteRequest = (
8
20
  req: Connect.IncomingMessage,
9
21
  res: ServerResponse<IncomingMessage>,
10
22
  next: Connect.NextFunction,
11
- cb: (data: any) => void,
23
+ cbOrOptions: DevToolsRequestHandler | DevToolsViteRequestOptions,
12
24
  ) => {
25
+ // Normalize to options object for backward compatibility
26
+ const options: DevToolsViteRequestOptions =
27
+ typeof cbOrOptions === 'function'
28
+ ? { onOpenSource: cbOrOptions }
29
+ : cbOrOptions
30
+
31
+ // Handle open-source requests
13
32
  if (req.url?.includes('__tsd/open-source')) {
14
33
  const searchParams = new URLSearchParams(req.url.split('?')[1])
15
34
 
@@ -24,7 +43,7 @@ export const handleDevToolsViteRequest = (
24
43
  }
25
44
  const { file, line, column } = parsed
26
45
 
27
- cb({
46
+ options.onOpenSource?.({
28
47
  type: 'open-source',
29
48
  routine: 'open-source',
30
49
  data: {
@@ -38,6 +57,62 @@ export const handleDevToolsViteRequest = (
38
57
  res.end()
39
58
  return
40
59
  }
60
+
61
+ // Handle console-pipe SSE endpoint (browser subscribes to server logs)
62
+ if (req.url?.includes('__tsd/console-pipe/sse') && req.method === 'GET') {
63
+ if (options.onConsolePipeSSE) {
64
+ options.onConsolePipeSSE(res, req)
65
+ return
66
+ }
67
+ return next()
68
+ }
69
+
70
+ // Handle server console-pipe POST endpoint (from app server runtime)
71
+ if (req.url?.includes('__tsd/console-pipe/server') && req.method === 'POST') {
72
+ if (options.onServerConsolePipe) {
73
+ let body = ''
74
+ req.on('data', (chunk: Buffer) => {
75
+ body += chunk.toString()
76
+ })
77
+ req.on('end', () => {
78
+ try {
79
+ const { entries } = JSON.parse(body)
80
+ options.onServerConsolePipe!(entries)
81
+ res.statusCode = 200
82
+ res.end('OK')
83
+ } catch {
84
+ res.statusCode = 400
85
+ res.end('Bad Request')
86
+ }
87
+ })
88
+ return
89
+ }
90
+ return next()
91
+ }
92
+
93
+ // Handle console-pipe POST endpoint (from client)
94
+ if (req.url?.includes('__tsd/console-pipe') && req.method === 'POST') {
95
+ if (options.onConsolePipe) {
96
+ let body = ''
97
+ req.on('data', (chunk: Buffer) => {
98
+ body += chunk.toString()
99
+ })
100
+ req.on('end', () => {
101
+ try {
102
+ const { entries } = JSON.parse(body)
103
+ options.onConsolePipe!(entries)
104
+ res.statusCode = 200
105
+ res.end('OK')
106
+ } catch {
107
+ res.statusCode = 400
108
+ res.end('Bad Request')
109
+ }
110
+ })
111
+ return
112
+ }
113
+ return next()
114
+ }
115
+
41
116
  if (!req.url?.includes('__tsd')) {
42
117
  return next()
43
118
  }
@@ -50,7 +125,7 @@ export const handleDevToolsViteRequest = (
50
125
  const dataToParse = Buffer.concat(chunks)
51
126
  try {
52
127
  const parsedData = JSON.parse(dataToParse.toString())
53
- cb(parsedData)
128
+ options.onOpenSource?.(parsedData)
54
129
  } catch (e) {}
55
130
  res.write('OK')
56
131
  })
@@ -92,3 +167,80 @@ export const tryParseJson = <T extends any>(
92
167
 
93
168
  export const readPackageJson = async () =>
94
169
  tryParseJson<PackageJson>(await tryReadFile(process.cwd() + '/package.json'))
170
+
171
+ /**
172
+ * Extracts and formats the source location from enhanced client console logs.
173
+ * Instead of stripping the prefix entirely, we extract the file:line:column
174
+ * from the "Go to Source" URL and use that as a prefix.
175
+ *
176
+ * Enhanced logs format (two variants):
177
+ * 1. ['%cLOG%c %cGo to Source: http://...?source=%2Fsrc%2F...%c \n → ', 'color:...', 'color:...', 'color:...', 'color:...', 'message']
178
+ * 2. ['\x1b[...]%s\x1b[...]', '%cLOG%c %cGo to Source: ...%c \n → ', 'color:...', 'color:...', 'color:...', 'color:...', 'message']
179
+ *
180
+ * Output: ['src/components/Header.tsx:26:13', 'message']
181
+ */
182
+ export const stripEnhancedLogPrefix = (
183
+ args: Array<unknown>,
184
+ formatSourceLocation?: (location: string) => unknown,
185
+ ): Array<unknown> => {
186
+ if (args.length === 0) return args
187
+
188
+ // Find the arg that contains the Go to Source URL
189
+ let sourceArgIndex = -1
190
+ for (let i = 0; i < args.length; i++) {
191
+ const arg = args[i]
192
+ if (typeof arg === 'string' && arg.includes('__tsd/open-source?source=')) {
193
+ sourceArgIndex = i
194
+ break
195
+ }
196
+ }
197
+
198
+ // If no source URL found, return args as-is (not an enhanced log)
199
+ if (sourceArgIndex === -1) {
200
+ return args
201
+ }
202
+
203
+ const sourceArg = args[sourceArgIndex] as string
204
+
205
+ // Extract the source from the "Go to Source" URL
206
+ // URL format: http://localhost:3000/__tsd/open-source?source=%2Fsrc%2Ffile.tsx%3A26%3A13%c
207
+ // Note: The URL ends with %c which is a console format specifier, not URL encoding
208
+ let sourceLocation = ''
209
+ const sourceMatch = sourceArg.match(/source=([^&\s]+?)%c/)
210
+ if (sourceMatch?.[1]) {
211
+ try {
212
+ sourceLocation = decodeURIComponent(sourceMatch[1])
213
+ // Remove leading slash if present
214
+ if (sourceLocation.startsWith('/')) {
215
+ sourceLocation = sourceLocation.slice(1)
216
+ }
217
+ } catch {
218
+ // If decoding fails, leave it empty
219
+ }
220
+ }
221
+
222
+ // Count %c markers in the source arg to know how many style args follow it
223
+ const styleCount = (sourceArg.match(/%c/g) || []).length
224
+
225
+ // The actual user args start after the source arg and all its style args
226
+ const userArgsStart = sourceArgIndex + 1 + styleCount
227
+
228
+ // Build the result: source location prefix + remaining args (the actual user data)
229
+ const result: Array<unknown> = []
230
+
231
+ // Add source location as prefix if we found one
232
+ if (sourceLocation) {
233
+ result.push(
234
+ formatSourceLocation
235
+ ? formatSourceLocation(sourceLocation)
236
+ : sourceLocation,
237
+ )
238
+ }
239
+
240
+ // Add remaining args (the actual user data)
241
+ for (let i = userArgsStart; i < args.length; i++) {
242
+ result.push(args[i])
243
+ }
244
+
245
+ return result.length > 0 ? result : args
246
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { generateConsolePipeCode } from './virtual-console'
3
+
4
+ const TEST_VITE_URL = 'http://localhost:5173'
5
+
6
+ describe('virtual-console', () => {
7
+ test('generates inline code with specified levels', () => {
8
+ const code = generateConsolePipeCode(['log', 'error'], TEST_VITE_URL)
9
+
10
+ expect(code).toContain('["log","error"]')
11
+ expect(code).toContain('originalConsole')
12
+ expect(code).toContain('__TSD_CONSOLE_PIPE_INITIALIZED__')
13
+ })
14
+
15
+ test('uses fetch to send client logs', () => {
16
+ const code = generateConsolePipeCode(['log'], TEST_VITE_URL)
17
+
18
+ expect(code).toContain('/__tsd/console-pipe')
19
+ expect(code).toContain("method: 'POST'")
20
+ })
21
+
22
+ test('uses SSE to receive server logs', () => {
23
+ const code = generateConsolePipeCode(['log'], TEST_VITE_URL)
24
+
25
+ expect(code).toContain("new EventSource('/__tsd/console-pipe/sse')")
26
+ })
27
+
28
+ test('includes environment detection', () => {
29
+ const code = generateConsolePipeCode(['log'], TEST_VITE_URL)
30
+
31
+ expect(code).toContain("typeof window === 'undefined'")
32
+ expect(code).toContain('isServer')
33
+ })
34
+
35
+ test('includes batcher configuration', () => {
36
+ const code = generateConsolePipeCode(['log'], TEST_VITE_URL)
37
+
38
+ expect(code).toContain('BATCH_WAIT')
39
+ expect(code).toContain('BATCH_MAX_SIZE')
40
+ })
41
+
42
+ test('includes flush functionality', () => {
43
+ const code = generateConsolePipeCode(['log'], TEST_VITE_URL)
44
+
45
+ expect(code).toContain('flushBatch')
46
+ })
47
+
48
+ test('includes beforeunload listener for browser', () => {
49
+ const code = generateConsolePipeCode(['log'], TEST_VITE_URL)
50
+
51
+ expect(code).toContain('beforeunload')
52
+ })
53
+
54
+ test('wraps code in IIFE', () => {
55
+ const code = generateConsolePipeCode(['log'], TEST_VITE_URL)
56
+
57
+ expect(code).toContain('(function __tsdConsolePipe()')
58
+ expect(code).toContain('})();')
59
+ })
60
+
61
+ test('has no external imports', () => {
62
+ const code = generateConsolePipeCode(['log'], TEST_VITE_URL)
63
+
64
+ expect(code).not.toContain('import ')
65
+ })
66
+
67
+ test('includes vite server URL for server piping', () => {
68
+ const code = generateConsolePipeCode(['log'], TEST_VITE_URL)
69
+
70
+ expect(code).toContain(TEST_VITE_URL)
71
+ expect(code).toContain('/__tsd/console-pipe/server')
72
+ })
73
+ })
@@ -0,0 +1,202 @@
1
+ import type { ConsoleLevel } from './plugin'
2
+
3
+ // export const VIRTUAL_MODULE_ID = 'virtual:tanstack-devtools-console'
4
+ // export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID
5
+
6
+ /**
7
+ * Generates inline code to inject into entry files (both client and server).
8
+ * This code detects the environment at runtime and:
9
+ *
10
+ * CLIENT:
11
+ * 1. Store original console methods
12
+ * 2. Create batched wrappers that POST to server via fetch
13
+ * 3. Override global console with the wrapped methods
14
+ * 4. Listen for server console logs via SSE
15
+ *
16
+ * SERVER (Nitro/Vinxi runtime):
17
+ * 1. Store original console methods
18
+ * 2. Create batched wrappers that POST to Vite dev server
19
+ * 3. Override global console - original logging still happens, just also pipes to Vite
20
+ *
21
+ * Returns the inline code as a string - no imports needed since we use fetch.
22
+ */
23
+ export function generateConsolePipeCode(
24
+ levels: Array<ConsoleLevel>,
25
+ viteServerUrl: string,
26
+ ): string {
27
+ const levelsArray = JSON.stringify(levels)
28
+
29
+ return `
30
+ ;(function __tsdConsolePipe() {
31
+ // Detect environment
32
+ var isServer = typeof window === 'undefined';
33
+ var envKey = isServer ? '__TSD_SERVER_CONSOLE_PIPE_INITIALIZED__' : '__TSD_CONSOLE_PIPE_INITIALIZED__';
34
+ var globalObj = isServer ? globalThis : window;
35
+
36
+ // Only run once per environment
37
+ if (globalObj[envKey]) return;
38
+ globalObj[envKey] = true;
39
+
40
+ var CONSOLE_LEVELS = ${levelsArray};
41
+ var VITE_SERVER_URL = ${JSON.stringify(viteServerUrl)};
42
+
43
+ // Store original console methods before we override them
44
+ var originalConsole = {};
45
+ for (var i = 0; i < CONSOLE_LEVELS.length; i++) {
46
+ var level = CONSOLE_LEVELS[i];
47
+ originalConsole[level] = console[level].bind(console);
48
+ }
49
+
50
+ // Simple inline batcher implementation
51
+ var batchedEntries = [];
52
+ var batchTimeout = null;
53
+ var BATCH_WAIT = isServer ? 50 : 100;
54
+ var BATCH_MAX_SIZE = isServer ? 20 : 50;
55
+
56
+ function flushBatch() {
57
+ if (batchedEntries.length === 0) return;
58
+
59
+ var entries = batchedEntries;
60
+ batchedEntries = [];
61
+ batchTimeout = null;
62
+
63
+ // Determine endpoint based on environment
64
+ var endpoint = isServer
65
+ ? VITE_SERVER_URL + '/__tsd/console-pipe/server'
66
+ : '/__tsd/console-pipe';
67
+
68
+ // Send to Vite server via fetch
69
+ fetch(endpoint, {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({ entries: entries }),
73
+ }).catch(function( ) {
74
+ // Swallow errors
75
+ });
76
+ }
77
+
78
+ function addToBatch(entry) {
79
+ batchedEntries.push(entry);
80
+
81
+ if (batchedEntries.length >= BATCH_MAX_SIZE) {
82
+ if (batchTimeout) {
83
+ clearTimeout(batchTimeout);
84
+ batchTimeout = null;
85
+ }
86
+ flushBatch();
87
+ } else if (!batchTimeout) {
88
+ batchTimeout = setTimeout(flushBatch, BATCH_WAIT);
89
+ }
90
+ }
91
+
92
+ // Override global console methods
93
+ for (var j = 0; j < CONSOLE_LEVELS.length; j++) {
94
+ (function(level) {
95
+ var original = originalConsole[level];
96
+ console[level] = function() {
97
+ var args = Array.prototype.slice.call(arguments);
98
+
99
+ // Always call original first so logs appear normally
100
+ original.apply(console, args);
101
+
102
+ // Skip our own TSD Console Pipe logs to avoid recursion/noise
103
+ if (args.length > 0 && typeof args[0] === 'string' &&
104
+ (args[0].indexOf('[TSD Console Pipe]') !== -1 ||
105
+ args[0].indexOf('[@tanstack/devtools') !== -1)) {
106
+ return;
107
+ }
108
+
109
+ // Serialize args safely
110
+ var safeArgs = args.map(function(arg) {
111
+ if (arg === undefined) return 'undefined';
112
+ if (arg === null) return null;
113
+ if (typeof arg === 'function') return '[Function]';
114
+ if (typeof arg === 'symbol') return arg.toString();
115
+ try {
116
+ JSON.stringify(arg);
117
+ return arg;
118
+ } catch (e) {
119
+ return String(arg);
120
+ }
121
+ });
122
+
123
+ var entry = {
124
+ level: level,
125
+ args: safeArgs,
126
+ source: isServer ? 'server' : 'client',
127
+ timestamp: Date.now(),
128
+ };
129
+
130
+ addToBatch(entry);
131
+ };
132
+ })(CONSOLE_LEVELS[j]);
133
+ }
134
+
135
+ // CLIENT ONLY: Listen for server console logs via SSE
136
+ if (!isServer) {
137
+ // Transform server log args - strip ANSI codes and convert source paths to clickable URLs
138
+ function transformServerLogArgs(args) {
139
+ var escChar = String.fromCharCode(27);
140
+ var transformed = [];
141
+
142
+ for (var k = 0; k < args.length; k++) {
143
+ var arg = args[k];
144
+ if (typeof arg === 'string') {
145
+ // Strip ANSI escape sequences (ESC[...m patterns)
146
+ var cleaned = arg;
147
+ // Remove ESC character followed by [...m] - need to build regex dynamically
148
+ while (cleaned.indexOf(escChar) !== -1) {
149
+ cleaned = cleaned.split(escChar).join('');
150
+ }
151
+ // Also remove any leftover bracket codes like [35m
152
+ cleaned = cleaned.replace(/\\[[0-9;]*m/g, '');
153
+
154
+ // Transform source paths to clickable URLs
155
+ // Match patterns like /src/components/Header.tsx:17:3
156
+ var sourceRegex = /(\\/[^\\s]+:\\d+:\\d+)/g;
157
+ cleaned = cleaned.replace(sourceRegex, function(match) {
158
+ return window.location.origin + '/__tsd/open-source?source=' + encodeURIComponent(match);
159
+ });
160
+
161
+ if (cleaned.trim()) {
162
+ transformed.push(cleaned);
163
+ }
164
+ } else {
165
+ transformed.push(arg);
166
+ }
167
+ }
168
+
169
+ return transformed;
170
+ }
171
+
172
+ var eventSource = new EventSource('/__tsd/console-pipe/sse');
173
+
174
+ eventSource.onmessage = function(event) {
175
+ try {
176
+ var data = JSON.parse(event.data);
177
+ if (data.entries) {
178
+ for (var m = 0; m < data.entries.length; m++) {
179
+ var entry = data.entries[m];
180
+ var transformedArgs = transformServerLogArgs(entry.args);
181
+ var prefix = '%c[Server]%c';
182
+ var prefixStyle = 'color: #9333ea; font-weight: bold;';
183
+ var resetStyle = 'color: inherit;';
184
+ var logMethod = originalConsole[entry.level] || originalConsole.log;
185
+ logMethod.apply(console, [prefix, prefixStyle, resetStyle].concat(transformedArgs));
186
+ }
187
+ }
188
+ } catch (err) {
189
+ // Swallow errors
190
+ }
191
+ };
192
+
193
+ eventSource.onerror = function() {
194
+ // Swallow errors
195
+ };
196
+
197
+ // Flush on page unload
198
+ window.addEventListener('beforeunload', flushBatch);
199
+ }
200
+ })();
201
+ `
202
+ }