Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f6d19c0
docs: add documentation redesign design document
AlemTuzlak Mar 4, 2026
218fd0e
docs: add documentation implementation plan
AlemTuzlak Mar 4, 2026
1efff36
docs: restructure navigation with Concepts and Guides sections, add Vue
AlemTuzlak Mar 4, 2026
f39ea72
docs: expand overview with package map and architecture diagram
AlemTuzlak Mar 4, 2026
1b012ed
docs: add all framework examples to quick start
AlemTuzlak Mar 4, 2026
08fda04
docs: add Vue and Preact installation instructions
AlemTuzlak Mar 4, 2026
6af0696
docs: add architecture overview page
AlemTuzlak Mar 4, 2026
6b465dc
docs: add event system concepts page
AlemTuzlak Mar 4, 2026
f68d56f
docs: add plugin lifecycle concepts page
AlemTuzlak Mar 4, 2026
6053a2e
docs: add source inspector concepts page
AlemTuzlak Mar 4, 2026
8467fc3
docs: add framework-agnostic custom plugin building guide
AlemTuzlak Mar 4, 2026
5d27737
docs: add devtools-utils guide for plugin factory helpers
AlemTuzlak Mar 4, 2026
cea62b0
docs: add bidirectional communication guide
AlemTuzlak Mar 4, 2026
fc23c87
docs: add Vue basic setup and adapter pages
AlemTuzlak Mar 4, 2026
e59d87d
docs: add Vue and Solid custom plugins guides
AlemTuzlak Mar 4, 2026
78818b8
docs: replace TODO in React adapter with actual content
AlemTuzlak Mar 4, 2026
becb4a4
docs: expand Vite plugin features documentation
AlemTuzlak Mar 4, 2026
4cdd529
docs: fix event map key convention, typos, and internal link extensions
AlemTuzlak Mar 4, 2026
df6f01d
docs: remove internal plan files
AlemTuzlak Mar 4, 2026
30c3d2b
docs: replace ASCII diagrams with mermaid in architecture overview
AlemTuzlak Mar 4, 2026
90cc506
docs: add mermaid diagrams across all documentation pages
AlemTuzlak Mar 4, 2026
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
236 changes: 236 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
---
title: Architecture Overview
id: architecture
---

TanStack Devtools is a modular system of packages organized into three layers: **Framework Adapters**, **Core Shell**, and **Event Transport**. This architecture lets you use pre-built devtools panels or build your own custom ones, regardless of which frontend framework you use.

```mermaid
block-beta
columns 1
block:framework["Framework Layer"]
columns 4
React Vue Solid Preact
end
block:core["Core Layer"]
columns 3
Shell["Devtools Shell"] UI["UI Components"] Client["Event Client"]
end
block:transport["Transport Layer"]
columns 3
ClientBus["Client Event Bus"] ServerBus["Server Event Bus"] Vite["Vite Plugin"]
end

framework --> core
core --> transport
```

## Package Dependency Graph

```mermaid
graph TD
subgraph Framework["Framework Adapters"]
react["@tanstack/react-devtools"]
vue["@tanstack/vue-devtools"]
solid["@tanstack/solid-devtools"]
preact["@tanstack/preact-devtools"]
end

subgraph Core["Core Layer"]
shell["@tanstack/devtools<br/><i>Core shell (Solid.js)</i>"]
client["@tanstack/devtools-client<br/><i>Core devtools events</i>"]
eventClient["@tanstack/devtools-event-client<br/><i>Generic EventClient</i>"]
ui["@tanstack/devtools-ui<br/><i>Shared UI components</i>"]
clientBus["@tanstack/devtools-event-bus/client<br/><i>ClientEventBus</i>"]
end

subgraph Build["Build Layer"]
vite["@tanstack/devtools-vite"]
serverBus["@tanstack/devtools-event-bus/server<br/><i>ServerEventBus</i>"]
end

subgraph Utilities
utils["@tanstack/devtools-utils"]
end

react --> shell
vue --> shell
solid --> shell
preact --> shell

shell --> client
shell --> ui
shell --> clientBus
client --> eventClient

vite --> client
vite --> serverBus

utils --> ui
```

Each framework adapter depends only on `@tanstack/devtools`. The core shell pulls in everything it needs, so end users install just two packages: their framework adapter and the Vite plugin.

## Transport Layer

The transport layer handles event delivery between plugins, the devtools UI, and (optionally) a dev server. It is composed of three pieces.

### ServerEventBus (`@tanstack/devtools-event-bus/server`)

Runs inside the Vite dev server process (Node.js). It creates an HTTP server (or piggybacks on Vite's existing server when HTTPS is enabled) that accepts both **WebSocket** and **SSE** connections. When a message arrives from any client, the server broadcasts it to every other connected client and dispatches it on a server-side `EventTarget` so server-side listeners (like the Vite plugin's package-manager helpers) can react to it.

Key details:
- Default port is `4206`, auto-increments if the port is in use.
- Handles `/__devtools/ws` for WebSocket upgrades, `/__devtools/sse` for SSE streams, and `/__devtools/send` for SSE POST fallback.
- Sets `globalThis.__TANSTACK_EVENT_TARGET__` so that `EventClient` instances running on the server can dispatch events onto the same target.

### ClientEventBus (`@tanstack/devtools-event-bus/client`)

Runs in the browser. Started automatically when the core shell mounts via `TanStackDevtoolsCore.mount()`. Its responsibilities:

1. **Local dispatch** -- Listens for `tanstack-dispatch-event` CustomEvents on `window`, re-dispatches them as both a type-specific CustomEvent (e.g. `my-plugin:state-update`) and a global `tanstack-devtools-global` event so listeners can subscribe to individual event types or to all events.
2. **Server forwarding** -- If connected to a server bus, forwards every dispatched event over WebSocket (preferred) or SSE POST fallback.
3. **Cross-tab sync** -- Uses `BroadcastChannel('tanstack-devtools')` to replicate events across browser tabs without round-tripping through the server.
4. **Connection handshake** -- Responds to `tanstack-connect` events with `tanstack-connect-success`, allowing `EventClient` instances to discover the bus.

### EventClient (`@tanstack/devtools-event-client`)

The high-level, typed API that plugins use to send and receive events. Each `EventClient` is created with a `pluginId` and a type map that defines the events it can emit and listen to.

```ts
import { EventClient } from '@tanstack/devtools-event-client'

type MyEvents = {
'state-update': { count: number }
'reset': void
}

const client = new EventClient<MyEvents>({ pluginId: 'my-plugin' })
```

When you call `client.emit('state-update', { count: 42 })`, the EventClient:

1. Dispatches a CustomEvent on its internal `EventTarget` (for same-page listeners using the `withEventTarget` option).
2. Dispatches a `tanstack-dispatch-event` CustomEvent on the global target (typically `window`), with a payload of `{ type: 'my-plugin:state-update', payload: { count: 42 }, pluginId: 'my-plugin' }`.
3. The `ClientEventBus` picks up that `tanstack-dispatch-event`, re-dispatches it as a `my-plugin:state-update` CustomEvent on `window`, and forwards it to the server bus via WebSocket.

When you call `client.on('state-update', callback)`, the EventClient registers a listener on the global target for `my-plugin:state-update` events, so it receives events regardless of whether they came from a local emit or from the server bus.

> [!NOTE]
> The server bus is optional. Without the Vite plugin, `EventClient` still works for same-page communication via CustomEvent dispatch on `window`. Events simply won't cross tab or process boundaries.

### Event Flow Summary

```mermaid
flowchart LR
emit["EventClient.emit()"] --> dispatch["CustomEvent<br/><b>tanstack-dispatch-event</b><br/>on window"]
dispatch --> bus["ClientEventBus"]
bus --> local["Re-dispatch as<br/><b>my-plugin:state-update</b><br/>on window"]
local --> onLocal["EventClient.on()<br/>callbacks fire"]
bus --> bc["BroadcastChannel"]
bc --> otherTabs["Other tabs<br/>receive event"]
bus --> ws["WebSocket"]
ws --> server["ServerEventBus"]
server --> broadcast["Broadcast to all<br/>connected clients"]
broadcast --> remote["EventClient.on()<br/>callbacks fire<br/>in other clients"]
```

## Core Layer

### @tanstack/devtools -- The Shell

The devtools shell is a Solid.js application that renders the entire devtools UI. It exposes the `TanStackDevtoolsCore` class with three methods:

- **`mount(el)`** -- Renders the Solid.js devtools application into the given DOM element. Starts a `ClientEventBus` and lazy-loads the UI. Wraps everything in a `DevtoolsProvider` (reactive store for plugins, settings, state) and a `PiPProvider` (Picture-in-Picture support).
- **`unmount()`** -- Tears down the Solid.js app and stops the event bus.
- **`setConfig(config)`** -- Updates configuration and plugins at runtime. Plugins are reactive: adding or removing them updates the tab bar immediately.

The shell renders:
- A **trigger button** (the floating devtools toggle, customizable or replaceable)
- A **resizable panel** (docked to the bottom of the viewport, resizable via drag)
- **Tab navigation** for switching between plugins, settings, SEO inspector, and the plugin marketplace
- A **settings panel** for theme, hotkeys, position, and other preferences
- **Plugin containers** -- DOM elements where each plugin's UI is mounted

Settings and UI state (panel size, position, active tab, theme) are persisted in `localStorage` so they survive page reloads.

### @tanstack/devtools-ui -- Component Library

A shared Solid.js component library used by the core shell and available for use in Solid.js plugins. Provides buttons, inputs, checkboxes, a JSON tree viewer, section layouts, and other UI primitives. The `@tanstack/devtools-utils` package also depends on it to provide framework-specific plugin helpers.

### @tanstack/devtools-client -- Core Event Client

A specialized `EventClient` pre-configured with `pluginId: 'tanstack-devtools-core'` and a fixed event map for devtools-internal operations:

- `mounted` -- Fired when the devtools UI has mounted, triggers the server to send current package.json and outdated dependency data.
- `package-json-read` / `outdated-deps-read` -- Carries project metadata from the Vite server to the devtools UI.
- `install-devtools` / `devtools-installed` -- Request/response cycle for installing a plugin package from the marketplace.
- `add-plugin-to-devtools` / `plugin-added` -- Request/response cycle for injecting a plugin into the user's source code.
- `trigger-toggled` -- Synchronizes the open/closed state of the devtools panel.

This client is a singleton (`devtoolsEventClient`) used by both the core shell and the Vite plugin to coordinate.

## Framework Layer

Each framework adapter is a thin wrapper that bridges its framework's component model to the core Solid.js shell. The pattern is the same across all adapters:

1. **Creates a `TanStackDevtoolsCore` instance** with the user's plugins and config.
2. **Mounts it to a DOM element** using the framework's lifecycle hooks (`useEffect` in React, `onMounted` in Vue, `onMount` in Solid).
3. **Converts framework-specific plugin definitions** into the core's DOM-based `render(el, theme)` interface. Each adapter defines its own plugin type (e.g. `TanStackDevtoolsReactPlugin`) that accepts framework-native components, then wraps them in a `render` callback that the core calls with a target DOM element and the current theme.
4. **Uses the framework's portal/teleport mechanism** to render plugin components into the core's DOM containers:
- **React** -- `createPortal()` from `react-dom`
- **Vue** -- `<Teleport :to="'#' + plugin.id" />`
- **Solid** -- `<Portal mount={el} />`
- **Preact** -- Same portal pattern as React

The key insight: the core shell is always Solid.js, but your plugins run in **your** framework. A React plugin is a real React component rendered by React's `createPortal` into a DOM element that the Solid.js shell created. A Vue plugin is a real Vue component rendered by Vue's `<Teleport>`. The adapters bridge this gap so you never need to think about Solid.js unless you want to.

### What an adapter does NOT do

Adapters do not re-implement the devtools UI, manage settings, handle events, or communicate with the server. All of that lives in the core shell. Adapters are intentionally minimal -- typically a single file under 300 lines.

## Build Layer

`@tanstack/devtools-vite` is a collection of Vite plugins that enhance the development experience and clean up production builds. It returns an array of Vite plugins, each handling a specific concern:

### Source injection (`@tanstack/devtools:inject-source`)
Uses Babel to parse JSX/TSX files and injects `data-tsd-source` attributes on every JSX element. These attributes encode the file path, line number, and column number of each element in source code, which the source inspector feature uses to implement click-to-open-in-editor.

### Server event bus (`@tanstack/devtools:custom-server`)
Starts a `ServerEventBus` on the Vite dev server. Also sets up middleware for the go-to-source editor integration and bidirectional console piping (client logs appear in the terminal, server logs appear in the browser).

### Production stripping (`@tanstack/devtools:remove-devtools-on-build`)
On production builds, transforms any file that imports from `@tanstack/*-devtools` to remove the devtools imports and JSX usage entirely. This means devtools add zero bytes to your production bundle.

### Console piping (`@tanstack/devtools:console-pipe-transform`)
Injects a small runtime into your application's entry file that intercepts `console.log/warn/error/info/debug` calls and forwards them to the Vite dev server via HTTP POST. The server then broadcasts them to connected SSE clients, enabling server-to-browser log forwarding.

### Enhanced logging (`@tanstack/devtools:better-console-logs`)
Transforms `console.*` calls to prepend source location information (file, line, column), making it possible to click a console log and jump directly to the source.

### Plugin marketplace support (`@tanstack/devtools:event-client-setup`)
Listens for `install-devtools` events from the devtools UI, runs the package manager to install the requested package, and then uses AST manipulation to inject the plugin import and configuration into the user's source code.

### Connection injection (`@tanstack/devtools:connection-injection`)
Replaces compile-time placeholders (`__TANSTACK_DEVTOOLS_PORT__`, `__TANSTACK_DEVTOOLS_HOST__`, `__TANSTACK_DEVTOOLS_PROTOCOL__`) in the event bus client code with the actual values from the running dev server, so the client automatically connects to the correct server.

## Data Flow

To tie everything together, here is what happens when a plugin emits an event end-to-end:

1. **Your library code** calls `eventClient.emit('state-update', data)`.

2. **EventClient** constructs a payload `{ type: 'my-plugin:state-update', payload: data, pluginId: 'my-plugin' }` and dispatches it as a `tanstack-dispatch-event` CustomEvent on `window`.

3. **ClientEventBus** receives the `tanstack-dispatch-event`. It does three things:
- Dispatches a CustomEvent named `my-plugin:state-update` on `window` so that any `eventClient.on('state-update', callback)` listeners on this page fire immediately.
- Dispatches a `tanstack-devtools-global` CustomEvent on `window` so that `onAll()` and `onAllPluginEvents()` listeners fire.
- Posts the event to the `BroadcastChannel` so other tabs receive it.

4. **If connected to the server bus**, ClientEventBus also sends the event over WebSocket to `ServerEventBus`.

5. **ServerEventBus** receives the WebSocket message and broadcasts it to all other connected clients (WebSocket and SSE). It also dispatches the event on its server-side `EventTarget` so server-side listeners (e.g., the Vite plugin) can react.

6. **In other browser tabs/windows**, the event arrives via WebSocket from the server (or via BroadcastChannel from step 3). The local `ClientEventBus` dispatches it as a `my-plugin:state-update` CustomEvent, and any `eventClient.on('state-update', callback)` listeners fire with the data.

Without the Vite plugin and server bus, steps 4-6 are skipped, but steps 1-3 still work. This means plugins can communicate within a single page without any server infrastructure -- the server bus just adds cross-tab and cross-process capabilities.
162 changes: 162 additions & 0 deletions docs/bidirectional-communication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
---
title: Bidirectional Communication
id: bidirectional-communication
---

Most devtools plugins observe state in one direction: app to devtools. But `EventClient` supports two-way communication. Your devtools panel can also send commands back to the app. This enables features like state editing, action replay, and time-travel debugging.

```mermaid
graph LR
subgraph app["Application"]
state["State / Library"]
end
subgraph ec["EventClient"]
direction TB
bus["Event Bus"]
end
subgraph dt["Devtools Panel"]
ui["Panel UI"]
end

state -- "emit('state-update')" --> bus
bus -- "on('state-update')" --> ui
ui -- "emit('reset')" --> bus
bus -- "on('reset')" --> state
```

## Pattern: App to Devtools (Observation)

The standard one-way pattern. Your app emits events, the devtools panel listens.

```ts
// In your app/library
eventClient.emit('state-update', { count: 42 })

// In your devtools panel
eventClient.on('state-update', (e) => {
setState(e.payload)
})
```

## Pattern: Devtools to App (Commands)

The panel emits command events, the app listens and reacts.

```ts
// In your devtools panel (e.g., a "Reset" button click handler)
eventClient.emit('reset', undefined)

// In your app/library
eventClient.on('reset', () => {
store.reset()
})
```

You need to define both directions in your event map:

```ts
type MyEvents = {
// App → Devtools
'state-update': { count: number }
// Devtools → App
'reset': void
'set-state': { count: number }
}
```

## Pattern: Time Travel

The most powerful bidirectional pattern. Combine observation with command-based state restoration.

```mermaid
sequenceDiagram
participant App as Application
participant EC as EventClient
participant Panel as Time Travel Panel

App->>EC: emit('snapshot', { state, timestamp })
EC->>Panel: on('snapshot') → collect snapshots
Note over Panel: User drags slider to past state
Panel->>EC: emit('revert', { state })
EC->>App: on('revert') → restore state
App->>EC: emit('snapshot', { state, timestamp })
EC->>Panel: on('snapshot') → update timeline
```

### Event Map

```ts
type TimeTravelEvents = {
'snapshot': { state: unknown; timestamp: number; label: string }
'revert': { state: unknown }
}
```

### App Side

Emit snapshots on every state change:

```ts
function applyAction(action) {
state = reducer(state, action)

timeTravelClient.emit('snapshot', {
state: structuredClone(state),
timestamp: Date.now(),
label: action.type,
})
}

// Listen for revert commands from devtools
timeTravelClient.on('revert', (e) => {
state = e.payload.state
rerender()
})
```

### Panel Side

Collect snapshots and provide a slider:

```tsx
function TimeTravelPanel() {
const [snapshots, setSnapshots] = useState([])
const [index, setIndex] = useState(0)

useEffect(() => {
return timeTravelClient.on('snapshot', (e) => {
setSnapshots((prev) => [...prev, e.payload])
setIndex((prev) => prev + 1)
})
}, [])

const handleSliderChange = (newIndex) => {
setIndex(newIndex)
timeTravelClient.emit('revert', { state: snapshots[newIndex].state })
}

return (
<div>
<input
type="range"
min={0}
max={snapshots.length - 1}
value={index}
onChange={(e) => handleSliderChange(Number(e.target.value))}
/>
<p>
State at: {snapshots[index]?.label} (
{new Date(snapshots[index]?.timestamp).toLocaleTimeString()})
</p>
<pre>{JSON.stringify(snapshots[index]?.state, null, 2)}</pre>
</div>
)
}
```

## Best Practices

- **Keep payloads serializable.** No functions, DOM nodes, or circular references.
- **Use `structuredClone()` for snapshots** to avoid reference mutations.
- **Debounce frequent emissions** if needed (e.g., rapid state changes).
- **Use distinct event suffixes** for observation vs commands (e.g., `state-update` for observation, `set-state` for commands).
Loading