Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/breezy-schools-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/devtools-vite': minor
---

Console piping functionality
5 changes: 5 additions & 0 deletions .changeset/shaggy-taxes-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/devtools': patch
---

Fix a UI bug
12 changes: 11 additions & 1 deletion examples/react/start/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import {
StickyNote,
X,
} from 'lucide-react'
import { createServerFn } from '@tanstack/react-start'

const logger = createServerFn({ method: 'POST' }).handler(async ({ data }) => {
console.log('triggered logger server function')
console.log(data)
})

export default function Header() {
const [isOpen, setIsOpen] = useState(false)
Expand All @@ -22,7 +28,11 @@ export default function Header() {
<>
<header className="p-4 flex items-center bg-gray-800 text-white shadow-lg">
<button
onClick={() => setIsOpen(true)}
onClick={() => {
console.log('Opening menu')
logger()
setIsOpen(true)
}}
className="p-2 hover:bg-gray-700 rounded-lg transition-colors"
aria-label="Open menu"
>
Expand Down
4 changes: 4 additions & 0 deletions examples/react/start/src/routes/demo/start.api-request.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ function getNames() {

export const Route = createFileRoute('/demo/start/api-request')({
component: Home,
beforeLoad: () => {
console.log('Before loading Start API Request Demo route')
},
loader: () => {
console.log('Navigated to Start API Request Demo')
emitRouteNavigation('API Request', '/demo/start/api-request')
},
})
Expand Down
16 changes: 7 additions & 9 deletions examples/react/start/src/routes/demo/start.server-funcs.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import fs from 'node:fs'
import { useCallback, useState } from 'react'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { createMiddleware, createServerFn } from '@tanstack/react-start'
import { emitRouteNavigation } from '../../devtools'

/*
const loggingMiddleware = createMiddleware().server(
async ({ next, request }) => {
console.log("Request:", request.url);
return next();
}
);
const loggedServerFunction = createServerFn({ method: "GET" }).middleware([
console.log('Request:', request.url)
return next()
},
)
const loggedServerFunction = createServerFn({ method: 'GET' }).middleware([
loggingMiddleware,
]);
*/
])

const TODOS_FILE = 'todos.json'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const Route = createFileRoute('/demo/start/ssr/data-only')({
ssr: 'data-only',
component: RouteComponent,
loader: async () => {
console.log('Navigated to Data Only SSR Demo')
emitRouteNavigation('Data Only SSR', '/demo/start/ssr/data-only')
return await getPunkSongs()
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { emitRouteNavigation } from '../../devtools'
export const Route = createFileRoute('/demo/start/ssr/full-ssr')({
component: RouteComponent,
loader: async () => {
console.log('Navigated to Full SSR Demo')
emitRouteNavigation('Full SSR', '/demo/start/ssr/full-ssr')
return await getPunkSongs()
},
Expand Down
4 changes: 3 additions & 1 deletion examples/react/start/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { nitro } from 'nitro/vite'

const config = defineConfig({
plugins: [
devtools(),
devtools({
consolePiping: {},
}),
nitro(),
// this is the plugin that enables path aliases
viteTsConfigPaths({
Expand Down
1 change: 1 addition & 0 deletions examples/solid/basic/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Router, { router } from './setup'
const queryClient = new QueryClient()

function App() {
console.log('Rendering App Component')
return (
<QueryClientProvider client={queryClient}>
<h1>TanStack Devtools Solid Basic Example</h1>
Expand Down
2 changes: 1 addition & 1 deletion examples/solid/start/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import './app.css'

export default function App() {
const [count, setCount] = createSignal(0)

console.log('App component rendered with count:', count())
return (
<main>
<TanStackDevtools plugins={[]} />
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools-vite/src/enhance-logs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const removeEmptySpace = (str: string) => {
return str.replace(/\s/g, '').trim()
}

describe('remove-devtools', () => {
describe('enhance-logs', () => {
test('it adds enhanced console.logs to console.log()', () => {
const output = removeEmptySpace(
enhanceConsoleLog(
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools-vite/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { devtools, defineDevtoolsConfig } from './plugin'
export type { TanStackDevtoolsViteConfig } from './plugin'
export type { TanStackDevtoolsViteConfig, ConsoleLevel } from './plugin'
161 changes: 151 additions & 10 deletions packages/devtools-vite/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { devtoolsEventClient } from '@tanstack/devtools-client'
import { ServerEventBus } from '@tanstack/devtools-event-bus/server'
import { normalizePath } from 'vite'
import chalk from 'chalk'
import { handleDevToolsViteRequest, readPackageJson } from './utils'
import {
handleDevToolsViteRequest,
readPackageJson,
stripEnhancedLogPrefix,
} from './utils'
import { DEFAULT_EDITOR_CONFIG, handleOpenSource } from './editor'
import { removeDevtools } from './remove-devtools'
import { addSourceToJsx } from './inject-source'
Expand All @@ -13,10 +17,14 @@ import {
emitOutdatedDeps,
installPackage,
} from './package-manager'
import { generateConsolePipeCode } from './virtual-console'
import type { ServerResponse } from 'node:http'
import type { Plugin } from 'vite'
import type { EditorConfig } from './editor'
import type { ServerEventBusConfig } from '@tanstack/devtools-event-bus/server'

export type ConsoleLevel = 'log' | 'warn' | 'error' | 'info' | 'debug'

export type TanStackDevtoolsViteConfig = {
/**
* Configuration for the editor integration. Defaults to opening in VS code
Expand Down Expand Up @@ -70,6 +78,23 @@ export type TanStackDevtoolsViteConfig = {
components?: Array<string | RegExp>
}
}
/**
* Configuration for console piping between client and server.
* When enabled, console logs from the client will appear in the terminal,
* and server logs will appear in the browser console.
*/
consolePiping?: {
/**
* Whether to enable console piping.
* @default true
*/
enabled?: boolean
/**
* Which console methods to pipe.
* @default ['log', 'warn', 'error', 'info', 'debug']
*/
levels?: Array<ConsoleLevel>
}
}

export const defineDevtoolsConfig = (config: TanStackDevtoolsViteConfig) =>
Expand All @@ -82,6 +107,9 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
const injectSourceConfig = args?.injectSource ?? { enabled: true }
const removeDevtoolsOnBuild = args?.removeDevtoolsOnBuild ?? true
const serverBusEnabled = args?.eventBusConfig?.enabled ?? true
const consolePipingConfig = args?.consolePiping ?? { enabled: true }
const consolePipingLevels: Array<ConsoleLevel> =
consolePipingConfig.levels ?? ['log', 'warn', 'error', 'info', 'debug']

let devtoolsFileId: string | null = null
let devtoolsPort: number | null = null
Expand Down Expand Up @@ -175,16 +203,77 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
}
await editor.open(path, lineNum, columnNum)
}

// SSE clients for broadcasting server logs to browser
const sseClients: Array<{
res: ServerResponse
id: number
}> = []
let sseClientId = 0
const consolePipingEnabled = consolePipingConfig.enabled ?? true

server.middlewares.use((req, res, next) =>
handleDevToolsViteRequest(req, res, next, (parsedData) => {
const { data, routine } = parsedData
if (routine === 'open-source') {
return handleOpenSource({
data: { type: data.type, data },
openInEditor,
})
}
return
handleDevToolsViteRequest(req, res, next, {
onOpenSource: (parsedData) => {
const { data, routine } = parsedData
if (routine === 'open-source') {
return handleOpenSource({
data: { type: data.type, data },
openInEditor,
})
}
return
},
...(consolePipingEnabled
? {
onConsolePipe: (entries) => {
for (const entry of entries) {
const prefix = chalk.cyan('[Client]')
const logMethod = console[entry.level as ConsoleLevel]
const cleanedArgs = stripEnhancedLogPrefix(
entry.args,
(loc) => chalk.gray(loc),
)
logMethod(prefix, ...cleanedArgs)
}
},
onConsolePipeSSE: (res, req) => {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
res.setHeader('Access-Control-Allow-Origin', '*')
res.flushHeaders()

const clientId = ++sseClientId
sseClients.push({ res, id: clientId })

req.on('close', () => {
const index = sseClients.findIndex(
(c) => c.id === clientId,
)
if (index !== -1) {
sseClients.splice(index, 1)
}
})
},
onServerConsolePipe: (entries) => {
try {
const data = JSON.stringify({
entries: entries.map((e) => ({
level: e.level,
args: e.args,
source: 'server',
timestamp: e.timestamp || Date.now(),
})),
})

for (const client of sseClients) {
client.res.write(`data: ${data}\n\n`)
}
} catch {}
},
}
: {}),
}),
)
},
Expand Down Expand Up @@ -417,6 +506,8 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
packageJson,
})
})

// Console piping is now handled via HTTP endpoints in the custom-server plugin
},
async handleHotUpdate({ file }) {
if (file.endsWith('package.json')) {
Expand All @@ -428,6 +519,56 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
}
},
},
// Inject console piping code into entry files (both client and server)
{
name: '@tanstack/devtools:console-pipe-transform',
enforce: 'pre',
apply(config, { command }) {
return (
config.mode === 'development' &&
command === 'serve' &&
(consolePipingConfig.enabled ?? true)
)
},
transform(code, id) {
// Inject the console pipe code into entry files
if (
id.includes('node_modules') ||
id.includes('dist') ||
id.includes('?') ||
!id.match(/\.(tsx?|jsx?)$/)
) {
return
}

// Only inject once - check if already injected
if (code.includes('__tsdConsolePipe')) {
return
}

// Check if this is a root entry file (with <html> JSX or client entry points)
// In SSR frameworks, this file runs on BOTH server (SSR) and client (hydration)
// so our runtime check (typeof window === 'undefined') handles both environments
const isRootEntry =
/<html[\s>]/i.test(code) ||
code.includes('StartClient') ||
code.includes('hydrateRoot') ||
code.includes('createRoot') ||
(code.includes('solid-js/web') && code.includes('render('))

if (isRootEntry) {
const viteServerUrl = `http://localhost:${port}`
const inlineCode = generateConsolePipeCode(
consolePipingLevels,
viteServerUrl,
)

return `${inlineCode}\n${code}`
}

return undefined
},
},
{
name: '@tanstack/devtools:better-console-logs',
enforce: 'pre',
Expand Down
Loading