diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..c9065d87 --- /dev/null +++ b/docs/architecture.md @@ -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
Core shell (Solid.js)"] + client["@tanstack/devtools-client
Core devtools events"] + eventClient["@tanstack/devtools-event-client
Generic EventClient"] + ui["@tanstack/devtools-ui
Shared UI components"] + clientBus["@tanstack/devtools-event-bus/client
ClientEventBus"] + end + + subgraph Build["Build Layer"] + vite["@tanstack/devtools-vite"] + serverBus["@tanstack/devtools-event-bus/server
ServerEventBus"] + 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({ 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
tanstack-dispatch-event
on window"] + dispatch --> bus["ClientEventBus"] + bus --> local["Re-dispatch as
my-plugin:state-update
on window"] + local --> onLocal["EventClient.on()
callbacks fire"] + bus --> bc["BroadcastChannel"] + bc --> otherTabs["Other tabs
receive event"] + bus --> ws["WebSocket"] + ws --> server["ServerEventBus"] + server --> broadcast["Broadcast to all
connected clients"] + broadcast --> remote["EventClient.on()
callbacks fire
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** -- `` + - **Solid** -- `` + - **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 ``. 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. diff --git a/docs/bidirectional-communication.md b/docs/bidirectional-communication.md new file mode 100644 index 00000000..762308a0 --- /dev/null +++ b/docs/bidirectional-communication.md @@ -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 ( +
+ handleSliderChange(Number(e.target.value))} + /> +

+ State at: {snapshots[index]?.label} ( + {new Date(snapshots[index]?.timestamp).toLocaleTimeString()}) +

+
{JSON.stringify(snapshots[index]?.state, null, 2)}
+
+ ) +} +``` + +## 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). diff --git a/docs/building-custom-plugins.md b/docs/building-custom-plugins.md new file mode 100644 index 00000000..2385ad0b --- /dev/null +++ b/docs/building-custom-plugins.md @@ -0,0 +1,211 @@ +--- +title: Building Custom Plugins +id: building-custom-plugins +--- + +You can build custom devtools plugins for any state management library, API client, or internal tool. A plugin consists of two parts: + +1. **An EventClient** — sends and receives data between your application code and the devtools panel. +2. **A panel component** — displays the data inside the devtools UI. + +```mermaid +graph LR + subgraph app["Your Application"] + lib["Library / State"] + end + subgraph ec["EventClient"] + emit["emit()"] + on["on()"] + end + subgraph dt["Devtools Panel"] + panel["Panel Component"] + end + + lib -- "State changes" --> emit + emit -- "Events" --> panel + panel -- "Commands" --> on + on -- "Actions" --> lib +``` + +The EventClient is framework-agnostic. It works the same in React, Vue, Solid, Preact, or vanilla JavaScript. The panel component can be written in any framework supported by an adapter. + +This guide walks through building a custom plugin from scratch using a "store inspector" as a running example. + +## Step 1: Define Your Event Map + +Start by creating a TypeScript type that maps event names to their payload types: + +```ts +type StoreEvents = { + 'state-changed': { storeName: string; state: unknown; timestamp: number } + 'action-dispatched': { storeName: string; action: string; payload: unknown } + 'reset': void +} +``` + +Each key in the event map is just the event name (the suffix). Do **not** include the `pluginId` in the key — the `EventClient` prepends the `pluginId` automatically when emitting and listening. For example, if `pluginId` is `'store-inspector'` and the key is `'state-changed'`, the fully qualified event dispatched on the bus will be `'store-inspector:state-changed'`. + +The value of each key is the payload type. Use `void` for events that carry no data. + +## Step 2: Create an EventClient + +Extend the base `EventClient` class with your event map: + +```ts +import { EventClient } from '@tanstack/devtools-event-client' + +class StoreInspectorClient extends EventClient { + constructor() { + super({ pluginId: 'store-inspector' }) + } +} + +export const storeInspector = new StoreInspectorClient() +``` + +Install the event client package if you haven't already: + +```bash +npm i @tanstack/devtools-event-client +``` + +The constructor accepts additional options beyond `pluginId`: + +| Option | Type | Default | Description | +| ------------------ | --------- | ------- | ------------------------------------------------ | +| `pluginId` | `string` | — | Required. Identifies this plugin in the event system. | +| `debug` | `boolean` | `false` | Enable verbose console logging. | +| `enabled` | `boolean` | `true` | Whether the client connects to the event bus. | +| `reconnectEveryMs` | `number` | `300` | Interval (ms) between connection retry attempts. | + +See the [Event System](./event-system) page for the full connection lifecycle details. + +## Step 3: Emit Events From Your Code + +Call `emit()` from your library code whenever something interesting happens. You pass only the **suffix** part of the event name — the `pluginId` is prepended automatically. + +```ts +function dispatch(action, payload) { + // Your library logic + state = reducer(state, action, payload) + + // Emit to devtools + storeInspector.emit('state-changed', { + storeName: 'main', + state, + timestamp: Date.now(), + }) + storeInspector.emit('action-dispatched', { + storeName: 'main', + action, + payload, + }) +} +``` + +Common patterns for where to call `emit()`: + +- **In state mutations** — emit the new state after every update (as shown above). +- **In observers or subscriptions** — if your library uses a subscriber/observer pattern, emit from the notification callback. +- **In middleware** — if your library supports middleware or interceptors, emit from a middleware layer so it works automatically for all operations. + +If the devtools are not yet mounted when you emit, events are queued and flushed once the connection succeeds. If the connection never succeeds (e.g., devtools are not present), events are silently dropped after 5 retries. This means you can leave `emit()` calls in your library code without worrying about whether the devtools are active. + +## Step 4: Build the Panel Component + +Create a component that listens for events via `on()` and renders the data. Here is a React example: + +```tsx +import { useState, useEffect } from 'react' +import { storeInspector } from './store-inspector-client' + +function StoreInspectorPanel() { + const [state, setState] = useState>({}) + const [actions, setActions] = useState>([]) + + useEffect(() => { + const cleanupState = storeInspector.on('state-changed', (e) => { + setState(prev => ({ ...prev, [e.payload.storeName]: e.payload.state })) + }) + const cleanupActions = storeInspector.on('action-dispatched', (e) => { + setActions(prev => [...prev, { action: e.payload.action, payload: e.payload.payload }]) + }) + return () => { cleanupState(); cleanupActions() } + }, []) + + return ( +
+

Current State

+
{JSON.stringify(state, null, 2)}
+

Action Log

+
    + {actions.map((a, i) =>
  • {a.action}: {JSON.stringify(a.payload)}
  • )} +
+
+ ) +} +``` + +Like `emit()`, the `on()` method takes only the suffix. The callback receives the full event object with a typed `payload` property. Each `on()` call returns a cleanup function that removes the listener. + +> [!NOTE] +> When using plugin factories from `@tanstack/devtools-utils` (covered below), your panel component receives a `theme` prop (`'light' | 'dark'`) so you can adapt your UI to the current devtools theme. + +## Step 5: Register the Plugin + +Pass your plugin to the devtools component's `plugins` array: + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools' +import { StoreInspectorPanel } from './StoreInspectorPanel' + +function App() { + return ( + <> + {/* Your app */} + , + }, + ]} + /> + + ) +} +``` + +The `name` is displayed as the tab title in the devtools sidebar. The `render` field accepts a JSX element (React, Preact) or a component reference (Vue, Solid), depending on your adapter. + +You can also pass optional fields: + +- **`id`** — A stable identifier for the plugin. If omitted, one is generated from the name. +- **`defaultOpen`** — Set to `true` to open the plugin's panel automatically on first load. + +See the [Plugin Lifecycle](./plugin-lifecycle) page for the full plugin interface and mount sequence. + +## Advanced: Bidirectional Communication + +Plugins are not limited to one-way data display. You can also send commands from the devtools panel back to your application — for example, "reset state", "replay action", or "toggle feature flag". The same `EventClient` instance handles both directions: your app emits events that the panel listens to, and the panel emits events that your app listens to. + +For a detailed walkthrough with examples, see the [Bidirectional Communication](./bidirectional-communication) guide. + +## Advanced: Plugin Factories + +The `@tanstack/devtools-utils` package provides factory functions that simplify plugin creation for each framework: + +- `createReactPlugin()` — for React plugins +- `createSolidPlugin()` — for Solid plugins +- `createVuePlugin()` — for Vue plugins +- `createPreactPlugin()` — for Preact plugins + +These factories handle the wiring between your component and the devtools container, pass the `theme` prop automatically, and return a `[Plugin, NoOpPlugin]` tuple so you can tree-shake the devtools out of production builds. + +For usage details, see the [Using devtools-utils](./devtools-utils) guide. + +## Publishing to the Marketplace + +Once your plugin is working, you can share it with the community by publishing it to npm and submitting it to the TanStack Devtools Marketplace. The marketplace is a registry of third-party plugins that users can discover and install directly from the devtools UI. + +For submission instructions and the registry format, see [Third-party Plugins](./third-party-plugins). diff --git a/docs/config.json b/docs/config.json index e03fc3c7..b255291d 100644 --- a/docs/config.json +++ b/docs/config.json @@ -9,143 +9,117 @@ { "label": "Getting Started", "children": [ - { - "label": "Overview", - "to": "overview" - }, - { - "label": "Quick Start", - "to": "quick-start" - }, - { - "label": "Configuration", - "to": "configuration" - }, - { - "label": "Plugin Configuration", - "to": "plugin-configuration" - }, - { - "label": "Installation", - "to": "installation" - }, - { - "label": "Vite plugin", - "to": "vite-plugin" - }, - { - "label": "Third party plugins", - "to": "third-party-plugins" - }, - { - "label": "Production", - "to": "production" - } + { "label": "Overview", "to": "overview" }, + { "label": "Quick Start", "to": "quick-start" }, + { "label": "Installation", "to": "installation" }, + { "label": "Configuration", "to": "configuration" }, + { "label": "Plugin Configuration", "to": "plugin-configuration" }, + { "label": "Vite Plugin", "to": "vite-plugin" }, + { "label": "Production", "to": "production" } ], "frameworks": [ { "label": "react", "children": [ - { - "label": "Basic setup", - "to": "framework/react/basic-setup" - }, - { - "label": "React adapter", - "to": "framework/react/adapter" - } + { "label": "Basic Setup", "to": "framework/react/basic-setup" }, + { "label": "React Adapter", "to": "framework/react/adapter" } ] }, { "label": "preact", "children": [ - { - "label": "Basic setup", - "to": "framework/preact/basic-setup" - }, - { - "label": "Preact adapter", - "to": "framework/preact/adapter" - } + { "label": "Basic Setup", "to": "framework/preact/basic-setup" }, + { "label": "Preact Adapter", "to": "framework/preact/adapter" } ] }, { "label": "solid", "children": [ - { - "label": "Basic setup", - "to": "framework/solid/basic-setup" - }, - { - "label": "Solid Adapter", - "to": "framework/solid/adapter" - } + { "label": "Basic Setup", "to": "framework/solid/basic-setup" }, + { "label": "Solid Adapter", "to": "framework/solid/adapter" } + ] + }, + { + "label": "vue", + "children": [ + { "label": "Basic Setup", "to": "framework/vue/basic-setup" }, + { "label": "Vue Adapter", "to": "framework/vue/adapter" } ] } ] }, + { + "label": "Concepts", + "children": [ + { "label": "Architecture Overview", "to": "architecture" }, + { "label": "Event System", "to": "event-system" }, + { "label": "Plugin Lifecycle", "to": "plugin-lifecycle" }, + { "label": "Source Inspector", "to": "source-inspector" } + ] + }, { "label": "Guides", - "children": [], + "children": [ + { "label": "Building Custom Plugins", "to": "building-custom-plugins" }, + { "label": "Using devtools-utils", "to": "devtools-utils" }, + { "label": "Bidirectional Communication", "to": "bidirectional-communication" }, + { "label": "Third-party Plugins", "to": "third-party-plugins" } + ], "frameworks": [ { "label": "react", "children": [ - { - "label": "Custom plugins", - "to": "framework/react/guides/custom-plugins" - } + { "label": "Custom Plugins", "to": "framework/react/guides/custom-plugins" } ] }, { "label": "preact", "children": [ - { - "label": "Custom plugins", - "to": "framework/preact/guides/custom-plugins" - } + { "label": "Custom Plugins", "to": "framework/preact/guides/custom-plugins" } ] }, { "label": "solid", - "children": [] + "children": [ + { "label": "Custom Plugins", "to": "framework/solid/guides/custom-plugins" } + ] + }, + { + "label": "vue", + "children": [ + { "label": "Custom Plugins", "to": "framework/vue/guides/custom-plugins" } + ] } ] }, { "label": "API Reference", "children": [ - { - "label": "Core API Reference", - "to": "reference/index" - } + { "label": "Core API Reference", "to": "reference/index" } ], "frameworks": [ { "label": "react", "children": [ - { - "label": "React Hooks", - "to": "framework/react/reference/index" - } + { "label": "React Reference", "to": "framework/react/reference/index" } ] }, { "label": "preact", "children": [ - { - "label": "Preact Hooks", - "to": "framework/preact/reference/index" - } + { "label": "Preact Reference", "to": "framework/preact/reference/index" } ] }, { "label": "solid", "children": [ - { - "label": "Solid Hooks", - "to": "framework/solid/reference/index" - } + { "label": "Solid Reference", "to": "framework/solid/reference/index" } + ] + }, + { + "label": "vue", + "children": [ + { "label": "Vue Reference", "to": "framework/vue/reference/index" } ] } ] @@ -157,43 +131,31 @@ { "label": "react", "children": [ - { - "label": "Basic", - "to": "framework/react/examples/basic" - }, - { - "label": "TanStack Start", - "to": "framework/react/examples/start" - }, - { - "label": "Custom devtools", - "to": "framework/react/examples/custom-devtools" - } + { "label": "Basic", "to": "framework/react/examples/basic" }, + { "label": "TanStack Start", "to": "framework/react/examples/start" }, + { "label": "Custom Devtools", "to": "framework/react/examples/custom-devtools" } ] }, { "label": "preact", "children": [ - { - "label": "Basic", - "to": "framework/preact/examples/basic" - }, - { - "label": "Custom devtools", - "to": "framework/preact/examples/custom-devtools" - } + { "label": "Basic", "to": "framework/preact/examples/basic" }, + { "label": "Custom Devtools", "to": "framework/preact/examples/custom-devtools" } ] }, { "label": "solid", "children": [ - { - "label": "Basic", - "to": "framework/solid/examples/basic" - } + { "label": "Basic", "to": "framework/solid/examples/basic" } + ] + }, + { + "label": "vue", + "children": [ + { "label": "Basic", "to": "framework/vue/examples/basic" } ] } ] } ] -} \ No newline at end of file +} diff --git a/docs/configuration.md b/docs/configuration.md index a49d5db3..1e742efe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -150,7 +150,7 @@ Put together the `EventClient` configuration looks like: import { EventClient } from '@tanstack/devtools-event-client' type EventMap = { - 'custom-devtools:custom-state': { state: string } + 'custom-state': { state: string } } class customEventClient extends EventClient { diff --git a/docs/devtools-utils.md b/docs/devtools-utils.md new file mode 100644 index 00000000..5f581c20 --- /dev/null +++ b/docs/devtools-utils.md @@ -0,0 +1,273 @@ +--- +title: Using devtools-utils +id: devtools-utils +--- + +`@tanstack/devtools-utils` provides factory functions that simplify creating devtools plugins for each framework. Instead of manually wiring up render functions and no-op variants, these helpers produce correctly-typed plugin objects (and their production-safe no-op counterparts) from your components. Each framework has its own subpath export with an API tailored to that framework's conventions. + +## Installation + +```bash +npm install @tanstack/devtools-utils +``` + +## DevtoolsPanelProps + +Every panel component receives a `theme` prop so the panel can match the devtools shell appearance. The interface is defined per-framework in each subpath: + +```ts +interface DevtoolsPanelProps { + theme?: 'light' | 'dark' +} +``` + +> The Vue variant additionally accepts `'system'` as a theme value. + +Import it from the framework-specific subpath: + +```ts +// React +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/react' + +// Solid +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid' + +// Preact +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/preact' + +// Vue +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/vue' +``` + +## React + +### createReactPlugin + +Creates a `[Plugin, NoOpPlugin]` tuple from a React component and plugin metadata. + +**Signature:** + +```ts +function createReactPlugin(options: { + name: string + id?: string + defaultOpen?: boolean + Component: (props: DevtoolsPanelProps) => JSX.Element +}): readonly [Plugin, NoOpPlugin] +``` + +**Usage:** + +```tsx +import { createReactPlugin } from '@tanstack/devtools-utils/react' + +const [MyPlugin, NoOpPlugin] = createReactPlugin({ + name: 'My Store', + id: 'my-store', + defaultOpen: false, + Component: ({ theme }) => , +}) +``` + +The returned tuple contains two factory functions: + +- **`Plugin()`** -- returns a plugin object with `name`, `id`, `defaultOpen`, and a `render` function that renders your `Component` with the current theme. +- **`NoOpPlugin()`** -- returns a plugin object with the same metadata but a `render` function that renders an empty fragment. Use this for production builds where you want to strip devtools out. + +A common pattern for tree-shaking: + +```tsx +const [MyPlugin, NoOpPlugin] = createReactPlugin({ /* ... */ }) + +const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +``` + +### createReactPanel + +For library authors shipping a class-based devtools core that exposes `mount(el, theme)` and `unmount()` methods. This factory wraps that class in a React component that handles mounting into a `div`, passing the theme, and cleaning up on unmount. + +**Signature:** + +```ts +function createReactPanel< + TComponentProps extends DevtoolsPanelProps | undefined, + TCoreDevtoolsClass extends { + mount: (el: HTMLElement, theme: 'light' | 'dark') => void + unmount: () => void + }, +>(CoreClass: new () => TCoreDevtoolsClass): readonly [Panel, NoOpPanel] +``` + +**Usage:** + +```tsx +import { createReactPanel } from '@tanstack/devtools-utils/react' + +class MyDevtoolsCore { + mount(el: HTMLElement, theme: 'light' | 'dark') { + // render your devtools UI into el + } + unmount() { + // cleanup + } +} + +const [MyPanel, NoOpPanel] = createReactPanel(MyDevtoolsCore) + +// Then use the panel component inside createReactPlugin: +const [MyPlugin, NoOpPlugin] = createReactPlugin({ + name: 'My Store', + Component: MyPanel, +}) +``` + +The returned `Panel` component: +- Creates a `div` with `height: 100%` and stores a ref to it. +- Instantiates `CoreClass` on mount and calls `core.mount(el, theme)`. +- Calls `core.unmount()` on cleanup. +- Re-mounts when the `theme` prop changes. + +`NoOpPanel` renders an empty fragment and does nothing. + +## Preact + +### createPreactPlugin + +Identical API to `createReactPlugin`, using Preact's JSX types. Import from `@tanstack/devtools-utils/preact`. + +**Signature:** + +```ts +function createPreactPlugin(options: { + name: string + id?: string + defaultOpen?: boolean + Component: (props: DevtoolsPanelProps) => JSX.Element +}): readonly [Plugin, NoOpPlugin] +``` + +**Usage:** + +```tsx +import { createPreactPlugin } from '@tanstack/devtools-utils/preact' + +const [MyPlugin, NoOpPlugin] = createPreactPlugin({ + name: 'My Store', + id: 'my-store', + Component: ({ theme }) => , +}) +``` + +The return value and behavior are the same as `createReactPlugin` -- a `[Plugin, NoOpPlugin]` tuple where `Plugin` renders your component and `NoOpPlugin` renders nothing. + +### createPreactPanel + +Also available for Preact with the same class-based API as `createReactPanel`: + +```ts +import { createPreactPanel } from '@tanstack/devtools-utils/preact' + +const [MyPanel, NoOpPanel] = createPreactPanel(MyDevtoolsCore) +``` + +## Solid + +### createSolidPlugin + +Same option-object API as React and Preact, using Solid's JSX types. Import from `@tanstack/devtools-utils/solid`. + +**Signature:** + +```ts +function createSolidPlugin(options: { + name: string + id?: string + defaultOpen?: boolean + Component: (props: DevtoolsPanelProps) => JSX.Element +}): readonly [Plugin, NoOpPlugin] +``` + +**Usage:** + +```tsx +import { createSolidPlugin } from '@tanstack/devtools-utils/solid' + +const [MyPlugin, NoOpPlugin] = createSolidPlugin({ + name: 'My Store', + id: 'my-store', + Component: (props) => , +}) +``` + +### createSolidPanel + +Solid also provides a class-based panel factory. It uses `createSignal` and `onMount`/`onCleanup` instead of React hooks: + +```ts +import { createSolidPanel } from '@tanstack/devtools-utils/solid' + +const [MyPanel, NoOpPanel] = createSolidPanel(MyDevtoolsCore) +``` + +## Vue + +### createVuePlugin + +The Vue factory has a different API from the JSX-based frameworks. It takes a `name` string and a Vue `DefineComponent` as separate arguments rather than an options object. + +**Signature:** + +```ts +function createVuePlugin>( + name: string, + component: DefineComponent, +): readonly [Plugin, NoOpPlugin] +``` + +**Usage:** + +```ts +import { createVuePlugin } from '@tanstack/devtools-utils/vue' +import MyStorePanel from './MyStorePanel.vue' + +const [MyPlugin, NoOpPlugin] = createVuePlugin('My Store', MyStorePanel) +``` + +The returned functions differ from the JSX-based variants: + +- **`Plugin(props)`** -- returns `{ name, component, props }` where `component` is your Vue component. +- **`NoOpPlugin(props)`** -- returns `{ name, component: Fragment, props }` where the component is Vue's built-in `Fragment` (renders nothing visible). + +Both accept props that get forwarded to the component. + +### createVuePanel + +For class-based devtools cores, Vue provides `createVuePanel`. It creates a Vue `defineComponent` that handles mounting and unmounting the core class: + +```ts +import { createVuePanel } from '@tanstack/devtools-utils/vue' + +const [MyPanel, NoOpPanel] = createVuePanel(MyDevtoolsCore) +``` + +The panel component accepts `theme` and `devtoolsProps` as props. It mounts the core instance into a `div` element on `onMounted` and calls `unmount()` on `onUnmounted`. + +## When to Use Factories vs Manual Plugin Objects + +**Use the factories** when you are building a reusable library plugin that will be published as a package. The factories ensure: + +- Consistent plugin object shape across frameworks. +- A matching `NoOpPlugin` variant for production tree-shaking. +- Correct typing without manual type annotations. + +**Use manual plugin objects** when you are building a one-off internal devtools panel for your application. In that case, passing `name` and `render` directly to the devtools configuration is simpler and avoids the extra abstraction: + +```tsx +// Manual approach -- fine for one-off panels +{ + name: 'App State', + render: (el, theme) => , +} +``` + +The factory approach becomes more valuable as you add `id`, `defaultOpen`, and need both a development and production variant of the same plugin. diff --git a/docs/event-system.md b/docs/event-system.md new file mode 100644 index 00000000..50041cb4 --- /dev/null +++ b/docs/event-system.md @@ -0,0 +1,171 @@ +--- +title: Event System +id: event-system +--- + +The event system is how plugins communicate with the devtools UI and with the application. It is built on `EventClient`, a type-safe event emitter/listener from `@tanstack/devtools-event-client`. It is completely framework-agnostic. + +## EventClient Basics + +Create a typed `EventClient` by extending the base class with your event map: + +```ts +import { EventClient } from '@tanstack/devtools-event-client' + +type MyEvents = { + 'state-update': { count: number } + 'action': { type: string } +} + +class MyEventClient extends EventClient { + constructor() { + super({ pluginId: 'my-plugin' }) + } +} + +export const myEventClient = new MyEventClient() +``` + +The constructor accepts the following options: + +| Option | Type | Default | Description | +| ------------------ | --------- | ------- | ---------------------------------------------------- | +| `pluginId` | `string` | — | Required. Identifies this plugin in the event system. | +| `debug` | `boolean` | `false` | Enable debug logging to the console. | +| `enabled` | `boolean` | `true` | Whether the client connects to the bus at all. | +| `reconnectEveryMs` | `number` | `300` | Interval (ms) between connection retry attempts. | + +## Event Maps and Type Safety + +The generic `EventMap` type maps event names to payload types. Keys are event suffixes only — the `pluginId` is prepended automatically by `EventClient` when emitting and listening: + +```ts +type MyEvents = { + 'state-update': { count: number } + 'action': { type: string } +} +``` + +TypeScript enforces correct event names and payload shapes at compile time. You get autocomplete on event names and type errors if the payload does not match the declared shape. + +## Emitting Events + +Call `eventClient.emit(eventSuffix, payload)` to dispatch an event. You pass only the **suffix** (the part after the colon). The `pluginId` is prepended automatically: + +```ts +// If pluginId is 'my-plugin' and event map has 'state-update' +myEventClient.emit('state-update', { count: 42 }) +// Dispatches event named 'my-plugin:state-update' +``` + +If the client is not yet connected to the bus, the event is queued and flushed once the connection succeeds (see [Connection Lifecycle](#connection-lifecycle) below). + +## Listening to Events + +There are three methods for subscribing to events. Each returns a cleanup function you call to unsubscribe. + +### `on(eventSuffix, callback)` + +Listen to a specific event from this plugin. Like `emit`, you pass only the suffix: + +```ts +const cleanup = myEventClient.on('state-update', (event) => { + console.log(event.payload.count) // typed as { count: number } +}) + +// Later: stop listening +cleanup() +``` + +The callback receives the full event object: + +```ts +{ + type: 'my-plugin:state-update', // fully qualified event name + payload: { count: number }, // typed payload + pluginId: 'my-plugin' // originating plugin +} +``` + +### `onAll(callback)` + +Listen to **all** events from **all** plugins. Useful for logging, debugging, or building cross-plugin features: + +```ts +const cleanup = myEventClient.onAll((event) => { + console.log(event.type, event.payload) +}) +``` + +### `onAllPluginEvents(callback)` + +Listen to all events from **this** plugin only (filtered by `pluginId`): + +```ts +const cleanup = myEventClient.onAllPluginEvents((event) => { + // Only fires for events where event.pluginId === 'my-plugin' + console.log(event.type, event.payload) +}) +``` + +## Connection Lifecycle + +The `EventClient` manages its connection to the event bus automatically: + +```mermaid +stateDiagram-v2 + [*] --> Queueing: First emit() call + Queueing --> Connecting: Dispatches tanstack-connect + Connecting --> Retrying: No response + Retrying --> Connecting: Every 300ms (up to 5×) + Connecting --> Connected: tanstack-connect-success + Connected --> Connected: Emit events directly + Queueing --> Connected: Flush queued events + Retrying --> Failed: 5 retries exhausted + Failed --> [*]: Subsequent emits dropped +``` + +1. **Queueing** — When you call `emit()` before the client is connected, events are queued in memory. +2. **Connection** — On the first `emit()`, the client dispatches a `tanstack-connect` event and starts a retry loop. +3. **Retries** — The client retries every `reconnectEveryMs` (default: 300ms) up to a maximum of 5 attempts. +4. **Flush** — Once a `tanstack-connect-success` event is received, all queued events are flushed to the bus in order. +5. **Failure** — If all 5 retries are exhausted without a successful connection, the client stops retrying. Subsequent `emit()` calls are silently dropped (they will not be queued). + +### The `enabled` Option + +When `enabled` is set to `false`, the EventClient is effectively inert — `emit()` is a no-op and `on()` returns a no-op cleanup function. This is useful for conditionally disabling devtools instrumentation (e.g., in production). + +## Server Event Bus + +When `connectToServerBus: true` is set in the component's `eventBusConfig` prop, the `ClientEventBus` connects to the `ServerEventBus` started by the Vite plugin (default port 4206). This enables server-side features like console piping and the plugin marketplace. + +```tsx + +``` + +Without the Vite plugin running, the `EventClient` still works for same-page communication between your application code and the devtools panel. The server bus is only needed for features that bridge the browser and the dev server. + +## Debugging + +Set `debug: true` in the `EventClient` constructor or in `eventBusConfig` to enable verbose console logging. Debug logs are prefixed with `[tanstack-devtools:{pluginId}]` for plugin events and `[tanstack-devtools:client-bus]` for the client bus. + +```ts +const myEventClient = new MyEventClient() +// In the constructor: super({ pluginId: 'my-plugin', debug: true }) +``` + +Example output: + +``` +🌴 [tanstack-devtools:client-bus] Initializing client event bus +🌴 [tanstack-devtools:my-plugin] Registered event to bus my-plugin:state-update +🌴 [tanstack-devtools:my-plugin] Emitting event my-plugin:state-update +``` + +This is helpful when diagnosing issues with event delivery, connection timing, or verifying that your event map is wired up correctly. diff --git a/docs/framework/preact/basic-setup.md b/docs/framework/preact/basic-setup.md index efb501b6..11ec85b8 100644 --- a/docs/framework/preact/basic-setup.md +++ b/docs/framework/preact/basic-setup.md @@ -64,6 +64,6 @@ render( ) ``` -Finally add any additional configuration you desire to the `TanStackDevtools` component, more information can be found under the [TanStack Devtools Configuration](../../configuration.md) section. +Finally add any additional configuration you desire to the `TanStackDevtools` component, more information can be found under the [TanStack Devtools Configuration](../../configuration) section. A complete working example can be found in our [basic example](https://tanstack.com/devtools/latest/docs/framework/preact/examples/basic). diff --git a/docs/framework/preact/guides/custom-plugins.md b/docs/framework/preact/guides/custom-plugins.md index c3c3ac1b..ce2d3f0c 100644 --- a/docs/framework/preact/guides/custom-plugins.md +++ b/docs/framework/preact/guides/custom-plugins.md @@ -47,15 +47,15 @@ import { EventClient } from '@tanstack/devtools-event-client' type EventMap = { - // The key of the event map is a combination of {pluginId}:{eventSuffix} + // The key is the event suffix only — the pluginId is prepended automatically by EventClient // The value is the expected type of the event payload - 'custom-devtools:counter-state': { count: number, history: number[] } + 'counter-state': { count: number, history: number[] } } class CustomEventClient extends EventClient { constructor() { super({ - // The pluginId must match that of the event map key + // The pluginId is prepended to event map keys when emitting/listening pluginId: 'custom-devtools', }) } @@ -138,7 +138,7 @@ export function DevtoolPanel() { ## Application Integration -This step follows what's shown in [basic-setup](../basic-setup.md) for a more documented guide go check it out. As well as the complete [custom-devtools example](https://tanstack.com/devtools/latest/docs/framework/preact/examples/custom-devtools) in our examples section. +This step follows what's shown in [basic-setup](../basic-setup) for a more documented guide go check it out. As well as the complete [custom-devtools example](https://tanstack.com/devtools/latest/docs/framework/preact/examples/custom-devtools) in our examples section. Main.tsx ```tsx diff --git a/docs/framework/react/adapter.md b/docs/framework/react/adapter.md index 408446b2..bb5426bd 100644 --- a/docs/framework/react/adapter.md +++ b/docs/framework/react/adapter.md @@ -11,6 +11,72 @@ If you are using TanStack Devtools in a React application, we recommend using th npm install @tanstack/react-devtools ``` -## React Hooks +## Component: TanStackDevtools -TODO +The main React component for rendering devtools. It accepts a single props object of type `TanStackDevtoolsReactInit`: + +| Prop | Type | Description | +| --- | --- | --- | +| `plugins` | `TanStackDevtoolsReactPlugin[]` | Array of plugins to display in the devtools panel | +| `config` | `TanStackDevtoolsReactConfig` | Devtools UI configuration (position, hotkeys, theme, custom trigger, etc.) | +| `eventBusConfig` | `ClientEventBusConfig` | Event bus connection settings for communicating with the server bus | + +## Plugin Type: TanStackDevtoolsReactPlugin + +Each plugin describes a tab in the devtools panel: + +```ts +type PluginRender = + | JSX.Element + | ((el: HTMLElement, theme: 'dark' | 'light') => JSX.Element) + +type TanStackDevtoolsReactPlugin = { + id?: string + name: string | PluginRender + render: PluginRender + defaultOpen?: boolean +} +``` + +- **`render`** can be a JSX element (simplest -- just pass ``) or a function that receives the container element and current theme. The function form is useful when you need to access the raw DOM element or respond to theme changes. +- **`name`** works the same way -- use a string for plain text, or JSX / a function for custom tab titles. +- **`id`** is an optional unique identifier. If omitted, it is generated from the name. +- **`defaultOpen`** marks the plugin as initially active when no other plugins are open. + +## Usage Example + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools' +import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' + +function App() { + return ( + <> + + , + defaultOpen: true, + }, + ]} + /> + + ) +} +``` + +## RSC Compatibility + +The adapter includes a `'use client'` directive at the top of its entry file, making it compatible with React Server Components. You can import it directly in your client components without needing to add your own `'use client'` boundary. + +## Exports + +The `@tanstack/react-devtools` package exports: + +- **`TanStackDevtools`** -- the main component +- **`TanStackDevtoolsReactPlugin`** -- type for plugin objects +- **`TanStackDevtoolsReactInit`** -- type for the component's props diff --git a/docs/framework/react/basic-setup.md b/docs/framework/react/basic-setup.md index aa24ec9d..dfd39c43 100644 --- a/docs/framework/react/basic-setup.md +++ b/docs/framework/react/basic-setup.md @@ -74,6 +74,6 @@ createRoot(document.getElementById('root')!).render( ) ``` -Finally add any additional configuration you desire to the `TanStackDevtools` component, more information can be found under the [TanStack Devtools Configuration](../../configuration.md) section. +Finally add any additional configuration you desire to the `TanStackDevtools` component, more information can be found under the [TanStack Devtools Configuration](../../configuration) section. A complete working example can be found in our [basic example](https://tanstack.com/devtools/latest/docs/framework/react/examples/basic). diff --git a/docs/framework/react/guides/custom-plugins.md b/docs/framework/react/guides/custom-plugins.md index 4a700220..66ac8051 100644 --- a/docs/framework/react/guides/custom-plugins.md +++ b/docs/framework/react/guides/custom-plugins.md @@ -47,15 +47,15 @@ import { EventClient } from '@tanstack/devtools-event-client' type EventMap = { - // The key of the event map is a combination of {pluginId}:{eventSuffix} + // The key is the event suffix only — the pluginId is prepended automatically by EventClient // The value is the expected type of the event payload - 'custom-devtools:counter-state': { count: number, history: number[] } + 'counter-state': { count: number, history: number[] } } class CustomEventClient extends EventClient { constructor() { super({ - // The pluginId must match that of the event map key + // The pluginId is prepended to event map keys when emitting/listening pluginId: 'custom-devtools', }) } @@ -137,7 +137,7 @@ export function DevtoolPanel() { ## Application Integration -This step follows what's shown in [basic-setup](../basic-setup.md) for a more documented guide go check it out. As well as the complete [custom-devtools example](https://tanstack.com/devtools/latest/docs/framework/react/examples/custom-devtools) in our examples section. +This step follows what's shown in [basic-setup](../basic-setup) for a more documented guide go check it out. As well as the complete [custom-devtools example](https://tanstack.com/devtools/latest/docs/framework/react/examples/custom-devtools) in our examples section. Main.tsx ```tsx diff --git a/docs/framework/solid/basic-setup.md b/docs/framework/solid/basic-setup.md index 238dfedf..a7601b30 100644 --- a/docs/framework/solid/basic-setup.md +++ b/docs/framework/solid/basic-setup.md @@ -68,6 +68,6 @@ render(() => ( ), document.getElementById('root')!) ``` -Finally add any additional configuration you desire to the `TanStackDevtools` component, more information can be found under the [TanStack Devtools Configuration](../../configuration.md) section. +Finally add any additional configuration you desire to the `TanStackDevtools` component, more information can be found under the [TanStack Devtools Configuration](../../configuration) section. A complete working example can be found in our [examples section](https://tanstack.com/devtools/latest/docs/framework/solid/examples). diff --git a/docs/framework/solid/guides/custom-plugins.md b/docs/framework/solid/guides/custom-plugins.md new file mode 100644 index 00000000..eb5efd8f --- /dev/null +++ b/docs/framework/solid/guides/custom-plugins.md @@ -0,0 +1,197 @@ +--- +title: Custom plugins +id: custom-plugins +--- + +TanStack devtools allows you to create your own custom plugins by emitting and listening to our event bus. + +## Prerequisite + +This guide will walk you through a simple example where our library is a counter with a count history. A working example can be found in our [custom-plugin example](https://tanstack.com/devtools/latest/docs/framework/react/examples/custom-devtools). + +This is our library code: + +counter.ts +```tsx +export function createCounter() { + let count = 0 + const history = [] + + return { + getCount: () => count, + increment: () => { + count++ + history.push(count) + }, + decrement: () => { + count-- + history.push(count) + }, + }; +} +``` + +## Event Client Setup + +Install the [TanStack Devtools Event Client](https://www.npmjs.com/package/@tanstack/devtools-event-client) utils. + +```bash +npm i @tanstack/devtools-event-client +``` + +First you will need to setup the `EventClient`. + +eventClient.ts +```tsx +import { EventClient } from '@tanstack/devtools-event-client' + + +type EventMap = { + // The key is the event suffix only — the pluginId is prepended automatically by EventClient + // The value is the expected type of the event payload + 'counter-state': { count: number, history: number[] } +} + +class CustomEventClient extends EventClient { + constructor() { + super({ + // The pluginId is prepended to event map keys when emitting/listening + pluginId: 'custom-devtools', + }) + } +} + +// This is where the magic happens, it'll be used throughout your application. +export const DevtoolsEventClient = new CustomEventClient() +``` + +## Event Client Integration + +Now we need to hook our `EventClient` into the application code. This can be done in many way's, a useEffect that emits the current state, or a subscription to an observer, all that matters is that when you want to emit the current state you do the following. + +Our new library code will looks as follows: + +counter.ts +```tsx +import { DevtoolsEventClient } from './eventClient.ts' + +export function createCounter() { + let count = 0 + const history: Array = [] + + return { + getCount: () => count, + increment: () => { + count++ + history.push(count) + + // The emit eventSuffix must match that of the EventMap defined in eventClient.ts + DevtoolsEventClient.emit('counter-state', { + count, + history, + }) + }, + decrement: () => { + count-- + history.push(count) + + DevtoolsEventClient.emit('counter-state', { + count, + history, + }) + }, + } +} +``` + +> [!IMPORTANT] +> `EventClient` is framework agnostic so this process will be the same regardless of framework or even in vanilla JavaScript. + +## Consuming The Event Client + +Now we need to create our devtools panel, for a simple approach write the devtools in the framework that the adapter is, be aware that this will make the plugin framework specific. + +> Because TanStack is framework agnostic we have taken a more complicated approach that will be explained in coming docs (if framework agnosticism is not a concern to you, you can ignore this). + +DevtoolPanel.tsx +```tsx +import { createSignal, onCleanup } from 'solid-js' +import { DevtoolsEventClient } from './eventClient' + +export function DevtoolPanel() { + const [state, setState] = createSignal<{ count: number; history: number[] }>() + + const cleanup = DevtoolsEventClient.on('counter-state', (e) => setState(e.payload)) + onCleanup(cleanup) + + return ( +
+
{state()?.count}
+
{JSON.stringify(state()?.history)}
+
+ ) +} +``` + +## Application Integration + +This step follows what's shown in [basic-setup](../basic-setup) for a more documented guide go check it out. + +index.tsx +```tsx +import { render } from 'solid-js/web' +import { TanStackDevtools } from '@tanstack/solid-devtools' +import { DevtoolPanel } from './DevtoolPanel' + +render(() => ( + <> + + , + }, + ]} + /> + +), document.getElementById('root')!) +``` + +## Debugging + +Both the `TanStackDevtools` component and the TanStack `EventClient` come with built in debug mode which will log to the console the emitted event as well as the EventClient status. + +TanStackDevtool's debugging mode can be activated like so: +```tsx + , + }, + ]} +/> +``` + +Where as the EventClient's debug mode can be activated by: +```tsx +class CustomEventClient extends EventClient { + constructor() { + super({ + pluginId: 'custom-devtools', + debug: true, + }) + } +} +``` + +Activating the debug mode will log to the console the current events that emitter has emitted or listened to. The EventClient will have appended `[tanstack-devtools:${pluginId}]` and the client will have appended `[tanstack-devtools:client-bus]`. + +Heres an example of both: +``` +[tanstack-devtools:client-bus] Initializing client event bus + +[tanstack-devtools:custom-devtools-plugin] Registered event to bus custom-devtools:counter-state +``` diff --git a/docs/framework/vue/adapter.md b/docs/framework/vue/adapter.md new file mode 100644 index 00000000..bbf6156a --- /dev/null +++ b/docs/framework/vue/adapter.md @@ -0,0 +1,76 @@ +--- +title: TanStack Devtools Vue Adapter +id: adapter +--- + +The Vue adapter wraps `TanStackDevtoolsCore` in a Vue 3 component, using Vue's `` to render plugins and their tab titles into the correct DOM containers managed by the devtools shell. + +## Installation + +```sh +npm install @tanstack/vue-devtools +``` + +## Component Props + +The `TanStackDevtools` component accepts the following props, defined by the `TanStackDevtoolsVueInit` interface: + +| Prop | Type | Description | +| --- | --- | --- | +| `plugins` | `TanStackDevtoolsVuePlugin[]` | Array of plugins to render inside the devtools panel. | +| `config` | `Partial` | Configuration for the devtools shell. Sets the initial state on first load; afterwards settings are persisted in local storage. | +| `eventBusConfig` | `ClientEventBusConfig` | Configuration for the TanStack Devtools client event bus. | + +## Plugin Type + +Each plugin in the `plugins` array must conform to the `TanStackDevtoolsVuePlugin` type: + +```ts +type TanStackDevtoolsVuePlugin = { + id?: string + component: Component + name: string | Component + props?: Record +} +``` + +| Field | Type | Description | +| --- | --- | --- | +| `id` | `string` (optional) | Unique identifier for the plugin. | +| `component` | `Component` | The Vue component to render as the plugin panel content. | +| `name` | `string \| Component` | Display name for the tab title. Can be a plain string or a Vue component for custom rendering. | +| `props` | `Record` (optional) | Additional props passed to the plugin component via `v-bind`. | + +## Key Difference from React + +The Vue adapter uses `component` (a Vue component reference) instead of `render` (a JSX element) in plugin definitions. Props are provided through the `props` field and bound to the component with `v-bind`, rather than being embedded directly in a JSX expression. + +```vue + + + + +``` + +## Exports + +The `@tanstack/vue-devtools` package exports: + +- **`TanStackDevtools`** -- The main Vue component that renders the devtools panel. +- **`TanStackDevtoolsVuePlugin`** (type) -- The type for plugin definitions. +- **`TanStackDevtoolsVueInit`** (type) -- The props interface for the `TanStackDevtools` component. + +The package depends on `@tanstack/devtools` (the core package), which provides `TanStackDevtoolsCore`, `TanStackDevtoolsConfig`, `ClientEventBusConfig`, and other core utilities. diff --git a/docs/framework/vue/basic-setup.md b/docs/framework/vue/basic-setup.md new file mode 100644 index 00000000..3e27b964 --- /dev/null +++ b/docs/framework/vue/basic-setup.md @@ -0,0 +1,62 @@ +--- +title: Basic setup +id: basic-setup +--- + +TanStack Devtools provides you with an easy-to-use and modular client that allows you to compose multiple devtools into one easy-to-use panel. + +## Setup + +Install the [TanStack Devtools](https://www.npmjs.com/package/@tanstack/vue-devtools) library. This will install the devtools core as well as provide you with the Vue-specific adapter. + +```bash +npm i @tanstack/vue-devtools +``` + +Next, in the root of your application, import the `TanStackDevtools` component from `@tanstack/vue-devtools` and add it to your template. + +```vue + + + +``` + +Import the desired devtools and provide them to the `TanStackDevtools` component via the `plugins` prop along with a label for the menu. + +Currently TanStack offers: + +- `VueQueryDevtoolsPanel` + +```vue + + + +``` + +> Note: The Vue adapter uses `component` instead of `render` in plugin definitions. In Vue, components are passed as component references rather than JSX elements, and any additional props can be provided via the `props` field. + +Finally, add any additional configuration you desire to the `TanStackDevtools` component. More information can be found under the [TanStack Devtools Configuration](../../configuration) section. + +A complete working example can be found in our [basic example](https://tanstack.com/devtools/latest/docs/framework/vue/examples/basic). diff --git a/docs/framework/vue/guides/custom-plugins.md b/docs/framework/vue/guides/custom-plugins.md new file mode 100644 index 00000000..51942454 --- /dev/null +++ b/docs/framework/vue/guides/custom-plugins.md @@ -0,0 +1,204 @@ +--- +title: Custom plugins +id: custom-plugins +--- + +TanStack devtools allows you to create your own custom plugins by emitting and listening to our event bus. + +## Prerequisite + +This guide will walk you through a simple example where our library is a counter with a count history. A working example can be found in our [custom-plugin example](https://tanstack.com/devtools/latest/docs/framework/react/examples/custom-devtools). + +This is our library code: + +counter.ts +```tsx +export function createCounter() { + let count = 0 + const history = [] + + return { + getCount: () => count, + increment: () => { + count++ + history.push(count) + }, + decrement: () => { + count-- + history.push(count) + }, + }; +} +``` + +## Event Client Setup + +Install the [TanStack Devtools Event Client](https://www.npmjs.com/package/@tanstack/devtools-event-client) utils. + +```bash +npm i @tanstack/devtools-event-client +``` + +First you will need to setup the `EventClient`. + +eventClient.ts +```tsx +import { EventClient } from '@tanstack/devtools-event-client' + + +type EventMap = { + // The key is the event suffix only — the pluginId is prepended automatically by EventClient + // The value is the expected type of the event payload + 'counter-state': { count: number, history: number[] } +} + +class CustomEventClient extends EventClient { + constructor() { + super({ + // The pluginId is prepended to event map keys when emitting/listening + pluginId: 'custom-devtools', + }) + } +} + +// This is where the magic happens, it'll be used throughout your application. +export const DevtoolsEventClient = new CustomEventClient() +``` + +## Event Client Integration + +Now we need to hook our `EventClient` into the application code. This can be done in many way's, a useEffect that emits the current state, or a subscription to an observer, all that matters is that when you want to emit the current state you do the following. + +Our new library code will looks as follows: + +counter.ts +```tsx +import { DevtoolsEventClient } from './eventClient.ts' + +export function createCounter() { + let count = 0 + const history: Array = [] + + return { + getCount: () => count, + increment: () => { + count++ + history.push(count) + + // The emit eventSuffix must match that of the EventMap defined in eventClient.ts + DevtoolsEventClient.emit('counter-state', { + count, + history, + }) + }, + decrement: () => { + count-- + history.push(count) + + DevtoolsEventClient.emit('counter-state', { + count, + history, + }) + }, + } +} +``` + +> [!IMPORTANT] +> `EventClient` is framework agnostic so this process will be the same regardless of framework or even in vanilla JavaScript. + +## Consuming The Event Client + +Now we need to create our devtools panel, for a simple approach write the devtools in the framework that the adapter is, be aware that this will make the plugin framework specific. + +> Because TanStack is framework agnostic we have taken a more complicated approach that will be explained in coming docs (if framework agnosticism is not a concern to you, you can ignore this). + +DevtoolPanel.vue +```vue + + + +``` + +## Application Integration + +This step follows what's shown in [basic-setup](../basic-setup) for a more documented guide go check it out. + +App.vue +```vue + + + +``` + +## Debugging + +Both the `TanStackDevtools` component and the TanStack `EventClient` come with built in debug mode which will log to the console the emitted event as well as the EventClient status. + +TanStackDevtool's debugging mode can be activated like so: +```vue + +``` + +Where as the EventClient's debug mode can be activated by: +```tsx +class CustomEventClient extends EventClient { + constructor() { + super({ + pluginId: 'custom-devtools', + debug: true, + }) + } +} +``` + +Activating the debug mode will log to the console the current events that emitter has emitted or listened to. The EventClient will have appended `[tanstack-devtools:${pluginId}]` and the client will have appended `[tanstack-devtools:client-bus]`. + +Heres an example of both: +``` +[tanstack-devtools:client-bus] Initializing client event bus + +[tanstack-devtools:custom-devtools-plugin] Registered event to bus custom-devtools:counter-state +``` diff --git a/docs/installation.md b/docs/installation.md index f1535529..f1bb2766 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -16,6 +16,15 @@ npm install -D @tanstack/devtools-vite TanStack Devtools is compatible with React v16.8+ +## Preact + +```sh +npm install -D @tanstack/preact-devtools +npm install -D @tanstack/devtools-vite +``` + +TanStack Devtools is compatible with Preact v10+ + ## Solid ```sh @@ -25,13 +34,27 @@ npm install -D @tanstack/devtools-vite TanStack Devtools is compatible with Solid v1.9.5+ +## Vue + +```sh +npm install -D @tanstack/vue-devtools +``` + +The Vite plugin (`@tanstack/devtools-vite`) is optional for Vue — it enables additional features like source inspection and console piping but isn't required for basic usage. + +```sh +npm install -D @tanstack/devtools-vite +``` + +TanStack Devtools is compatible with Vue 3+ + ## Vanilla JS ```sh npm install -D @tanstack/devtools ``` -Install the the core `@tanstack/devtools` package to use with any framework or without a framework. Each framework package up above will also re-export everything from this core package. +Install the core `@tanstack/devtools` package to use with any framework or without a framework. Each framework package up above will also re-export everything from this core package. ## Production Builds @@ -44,4 +67,4 @@ npm install -D @tanstack/devtools-vite ``` -Read more about using the devtools in production in our [Production docs](./production.md). \ No newline at end of file +Read more about using the devtools in production in our [Production docs](./production). \ No newline at end of file diff --git a/docs/overview.md b/docs/overview.md index 0e932af9..15d2716c 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -15,7 +15,7 @@ to add your own custom devtools and merge multiple instances of TanStack Devtool ## Origin -Have you ever wanted to build your own devtools? Then you start work on them, and you realize how complex and time-consuming it can be. First of all, you have to deal with the dreaded z-indexes! +Have you ever wanted to build your own devtools? Then you start work on them, and you realize how complex and time-consuming it can be. First of all, you have to deal with the dreaded z-indexes! Create the trigger, make sure you can move it, style the panel, handle the interactions, make sure it's not getting in the way... The list keeps going on, and you're just trying to build your next feature in half the time! @@ -24,9 +24,75 @@ next feature in half the time! Well, that's where TanStack Devtools comes in. We provide a solid foundation for building your own devtools, so you can focus on what matters most: your application. We provide everything you need out of the box and all you have to do is plug the simple custom devtool panel into your app! +## What's in the Box + +TanStack Devtools is composed of several packages organized into layers. You only need to install the ones relevant to your use case. + +### Framework Adapters + +- `@tanstack/react-devtools` +- `@tanstack/vue-devtools` +- `@tanstack/solid-devtools` +- `@tanstack/preact-devtools` + +Thin wrappers that integrate the devtools into your framework of choice. Pick the one that matches your app and you're good to go. + +### Core + +- `@tanstack/devtools` — The devtools shell UI built in Solid.js. Provides the plugin system, tab navigation, settings panel, and trigger button. + +### Event System + +- `@tanstack/devtools-event-client` — Type-safe event client for building custom plugins. +- `@tanstack/devtools-event-bus` — WebSocket/SSE transport layer connecting client and server. + +### Build Tools + +- `@tanstack/devtools-vite` — Vite plugin providing source inspection, console piping, enhanced logging, and production build stripping. + +### Utilities + +- `@tanstack/devtools-utils` — Plugin factory helpers for each framework. +- `@tanstack/devtools-ui` — Shared Solid.js UI component library. +- `@tanstack/devtools-client` — Internal typed event client for core devtools operations. + +## Architecture + +The diagram below shows how the layers connect at a high level: + +```mermaid +graph TD + subgraph App["Your Application"] + adapter["Framework Adapter
React / Vue / Solid / Preact"] + subgraph shell["@tanstack/devtools — Core Shell"] + plugins["Plugin System & Tab Navigation"] + settings["Settings & State Persistence"] + ui["Trigger, Panel, Source Inspector"] + end + subgraph events["Event System"] + ec["EventClient
Typed events for plugins"] + eb["EventBus
WebSocket / SSE transport"] + end + end + + adapter --> shell + shell --> events +``` + +Your application loads a **Framework Adapter**, which mounts the **Core Shell**. The shell manages the plugin lifecycle, renders tabs, and surfaces settings. Plugins communicate through the **Event System**, which provides typed events locally and can bridge to a server over WebSocket or SSE when needed. + ## Key Features -- **Framework Agnostic**: Works with any framework or library. -- **Customizable**: Easily add your own devtools and plugins. -- **Lightweight**: Minimal impact on your application's performance. -- **Easy to Use**: Plug and play with your existing setup. +- **Framework Agnostic**: Works with React, Vue, Solid, and Preact out of the box. +- **Plugin System & Marketplace**: Build, share, and install devtools plugins with a simple API. +- **Type-Safe Event System**: Communicate between plugins and the shell using fully typed events. +- **Source Inspector**: Click any element in your app to jump straight to its source code (go-to-source). +- **Console Piping**: Route devtools output to your browser console for a familiar debugging workflow. +- **Picture-in-Picture Mode**: Pop the devtools panel out into its own window so it never covers your app. +- **Customizable Hotkeys**: Rebind keyboard shortcuts to match your workflow. + +## Next Steps + +- [Quick Start](./quick-start) — Get running in 2 minutes +- [Architecture Overview](./architecture) — Understand how the pieces fit together +- [Building Custom Plugins](./building-custom-plugins) — Create your own devtools diff --git a/docs/plugin-lifecycle.md b/docs/plugin-lifecycle.md new file mode 100644 index 00000000..3dd8416b --- /dev/null +++ b/docs/plugin-lifecycle.md @@ -0,0 +1,216 @@ +--- +title: Plugin Lifecycle +id: plugin-lifecycle +--- + +Every TanStack Devtools plugin follows a well-defined lifecycle: it is registered, mounted into a DOM container, rendered when activated, and cleaned up when the devtools unmount. This page walks through each stage in detail. + +## Plugin Interface + +All plugins implement the `TanStackDevtoolsPlugin` interface, which is the low-level contract between a plugin and the devtools core. Framework adapters (React, Vue, Solid, Preact) wrap this interface so you can work with familiar components, but under the hood every plugin is reduced to these fields: + +```ts +interface TanStackDevtoolsPlugin { + id?: string + name: string | ((el: HTMLHeadingElement, theme: 'dark' | 'light') => void) + render: (el: HTMLDivElement, theme: 'dark' | 'light') => void + destroy?: (pluginId: string) => void + defaultOpen?: boolean +} +``` + +### `id` (optional) + +A unique identifier for the plugin. If you omit it, the core generates one from the `name` string (lowercased, spaces replaced with dashes, suffixed with the plugin's index). Providing an explicit `id` is useful when you need a stable identifier across page reloads - for example, to persist which plugins the user had open. + +### `name` (required) + +Displayed as the tab title in the sidebar. This can be: + +- **A plain string** - rendered as text inside an `

` element. +- **A function** - receives the heading element (`HTMLHeadingElement`) and the current theme (`'dark' | 'light'`). You can render anything into the heading, such as an icon next to the name or a fully custom title. + +```ts +// Simple string name +{ name: 'My Plugin', render: (el) => { /* ... */ } } + +// Custom title via function +{ + name: (el, theme) => { + el.innerHTML = `My Plugin` + }, + render: (el) => { /* ... */ } +} +``` + +### `render` (required) + +The main rendering function. It receives a `
` container element and the current theme. Your job is to render your plugin UI into this container using whatever approach you prefer - raw DOM manipulation, a framework portal, or anything else. + +```ts +render: (el, theme) => { + el.innerHTML = `
Hello from my plugin!
` +} +``` + +The `render` function is called: + +1. When the plugin's tab is first activated (clicked or opened by default). +2. When the theme changes, so your UI can adapt. + +### `destroy` (optional) + +Called when the plugin is removed from the active set (e.g., the user deactivates the tab) or when the devtools unmount entirely. It receives the `pluginId` as its argument. Use this for cleanup - cancelling timers, closing WebSocket connections, removing event listeners, etc. + +Most plugins don't need to implement `destroy` because framework adapters handle cleanup automatically. + +### `defaultOpen` (optional) + +When set to `true`, the plugin's panel will open automatically when the devtools first load and no user preferences exist in localStorage. At most 3 plugins can be open simultaneously, so if more than 3 specify `defaultOpen: true`, only the first 3 are opened. + +This setting does **not** override saved user preferences. Once a user has interacted with the devtools and their active-plugin selection is persisted, `defaultOpen` has no effect. + +If only a single plugin is registered, it opens automatically regardless of `defaultOpen`. + +## Mount Sequence + +Here is what happens when you provide plugins to the devtools: + +1. **Initialization** - `TanStackDevtoolsCore` is instantiated with a `plugins` array. Each plugin is assigned an `id` if one is not already provided. + +2. **DOM containers are created** - The core's Solid-based UI creates two DOM elements per plugin: + - A content container: `
` where the plugin renders its UI. + - A title container: `

` where the plugin's name is rendered. + +3. **Title rendering** - For each plugin, the core checks if `name` is a string or function. If it's a string, the text is set directly on the heading element. If it's a function, the function is called with the heading element and current theme. + +4. **Plugin activation** - When the user clicks a plugin's tab (or the plugin is auto-opened via `defaultOpen`), the plugin is added to the `activePlugins` list. The core then calls `plugin.render(container, theme)` with the content `
` and the current theme. + +5. **Rendering** - The container is a regular `
` element. Your plugin can render anything into it - DOM nodes, a framework component tree via portals, a canvas, an iframe, etc. + +6. **Theme changes** - When the user toggles the theme in settings, `render` is called again with the new theme value. Your plugin should update its appearance accordingly. + +```mermaid +flowchart TD + A["User opens devtools"] --> B["TanStackDevtoolsCore created
with plugins array"] + B --> C["Solid UI renders sidebar
tabs and containers"] + C --> D["<h3 id='plugin-title-container-{id}'>
created per plugin"] + C --> E["<div id='plugin-container-{id}'>
created per active plugin"] + D --> F["plugin.name
string set or function called"] + E --> G["plugin.render(div, theme)
called with container + theme"] +``` + +## Framework Adapter Pattern + +You rarely interact with the raw `TanStackDevtoolsPlugin` interface directly. Instead, each framework adapter converts your familiar component model into the DOM-based plugin contract. + +### React / Preact + +The React adapter takes your JSX element and uses `createPortal` to render it into the plugin's container element: + +```tsx +// What you write: +, + }]} +/> + +// What happens internally: +// The adapter's render function calls: +// createPortal(, containerElement) +``` + +Your React component runs in its normal React tree with full access to hooks, context, state management, etc. It just renders into a different DOM location via the portal. + +### Solid + +The Solid adapter uses Solid's `` component to mount your JSX into the container: + +```tsx +// What you write: +, + }]} +/> + +// What happens internally: +// The adapter wraps your component in: +// {yourComponent} +``` + +Since the devtools core is itself built in Solid, this is the most native integration. Your component runs inside the same Solid reactive system as the devtools shell. + +### Vue + +The Vue adapter uses `` to render your Vue component into the container: + +```vue + + + + + + + + +``` + +Your Vue component receives the `theme` as a prop along with any other props you pass. It runs within the Vue app's reactivity system with full access to composition API, inject/provide, etc. + +### The Key Insight + +```mermaid +graph LR + subgraph fw["Your Framework Runtime"] + comp["Your Plugin Component
Full reactivity, hooks, signals"] + end + subgraph core["Devtools Core (Solid.js)"] + container["Plugin Container
<div id='plugin-container-{id}'>"] + end + comp -- "Portal / Teleport" --> container +``` + +Regardless of framework, your plugin component runs in its **normal framework context** with full reactivity, hooks, signals, lifecycle methods, and dependency injection. It just renders into a different DOM location via portals or teleports. This means: + +- React plugins can use `useState`, `useEffect`, `useContext`, and any React library. +- Solid plugins can use signals, stores, `createEffect`, and the full Solid API. +- Vue plugins can use `ref`, `computed`, `watch`, `inject`, and any Vue composable. + +The framework adapter handles all the wiring between your component and the devtools container. + +## Plugin State + +The devtools persist the active/visible plugin selection in `localStorage` under the key `tanstack_devtools_state`. This means that when a user opens specific plugin tabs, their selection survives page reloads. + +Key behaviors: + +- **Maximum 3 visible plugins** - At most 3 plugin panels can be displayed simultaneously. Activating a 4th plugin deactivates the earliest one. +- **`defaultOpen` vs. saved state** - If `defaultOpen: true` is set on a plugin and no saved state exists in localStorage, the plugin opens on first load. Once the user changes the selection, their preference takes precedence. +- **Single-plugin auto-open** - If only one plugin is registered, it opens automatically regardless of `defaultOpen` or saved state. +- **Stale plugin cleanup** - When the devtools load, any plugin IDs in the saved state that no longer match a registered plugin are automatically removed. + +## Cleanup + +When `TanStackDevtoolsCore.unmount()` is called - either explicitly or because the framework component unmounts - the following happens: + +1. The Solid reactive system disposes of the entire devtools component tree. +2. For each active plugin that provides a `destroy` function, `destroy(pluginId)` is called. +3. All DOM containers created by the core are removed. + +Framework adapters handle their own cleanup automatically: + +- **React** unmounts the portal, which triggers the normal React cleanup cycle (`useEffect` cleanup functions, etc.). +- **Vue** destroys the Teleport, which runs `onUnmounted` hooks in your component. +- **Solid** disposes of the Portal's reactive scope, running any `onCleanup` callbacks. + +You typically do **not** need to implement `destroy` unless your plugin has manual subscriptions, timers, WebSocket connections, or other resources that aren't tied to your framework's lifecycle. If all your cleanup is handled by framework hooks (like `useEffect` cleanup in React or `onCleanup` in Solid), the adapter takes care of it for you. diff --git a/docs/quick-start.md b/docs/quick-start.md index 2f41aa9a..016c430b 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -3,81 +3,226 @@ title: Quick Start id: quick-start --- -TanStack Devtools is a framework-agnostic devtool for managing and debugging *devtool devtools* +TanStack Devtools is a framework-agnostic devtool for managing and debugging devtools plugins across React, Preact, Solid, and Vue. Pick your framework below to get started. -To get up and running install the correct adapter for your framework: +## React -- **React**: `npm install @tanstack/react-devtools @tanstack/devtools-vite` -- **Solid**: `npm install @tanstack/solid-devtools @tanstack/devtools-vite` +Install the devtools and the Vite plugin: -Then import the devtools into the root of your application: +```bash +npm install @tanstack/react-devtools @tanstack/devtools-vite +``` + +Add the `TanStackDevtools` component to the root of your application: -```javascript +```tsx +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' import { TanStackDevtools } from '@tanstack/react-devtools' -function App() { - return ( - <> - - - - ) -} +import App from './App' + +createRoot(document.getElementById('root')!).render( + + + + , +) ``` -And plug the vite plugin as the first plugin in your plugin array in `vite.config.ts`: +To add plugins, pass them via the `plugins` prop. Each plugin needs a `name` and a `render` element: -```javascript -import { devtools } from '@tanstack/devtools-vite' +```tsx +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { TanStackDevtools } from '@tanstack/react-devtools' +import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' +import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' -export default { - plugins: [ - devtools(), - // ... rest of your plugins here - ], -} +import App from './App' + +createRoot(document.getElementById('root')!).render( + + + , + }, + { + name: 'TanStack Router', + render: , + }, + ]} + /> + , +) ``` -And you're done! If you want to add custom plugins, you can do so by using the `plugins` prop: +## Preact -```javascript -import { TanStackDevtools } from '@tanstack/react-devtools' +Install the devtools and the Vite plugin: -function App() { - return ( - <> - - - - ) -} +```bash +npm install @tanstack/preact-devtools @tanstack/devtools-vite ``` -For example, if you want to add TanStack query & router you could do so in the following way: -```javascript -import { TanStackDevtools } from '@tanstack/react-devtools' -import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' -import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' +Add the `TanStackDevtools` component using Preact's `render()` function: + +```tsx +import { render } from 'preact' +import { TanStackDevtools } from '@tanstack/preact-devtools' + +import App from './App' + +render( + <> + + + , + document.getElementById('root')!, +) +``` + +To add plugins, pass them via the `plugins` prop: + +```tsx +import { render } from 'preact' +import { TanStackDevtools } from '@tanstack/preact-devtools' + +import App from './App' + +render( + <> + + , + }, + ]} + /> + , + document.getElementById('root')!, +) +``` + +## Solid + +Install the devtools and the Vite plugin: -function App() { - return ( - - - ...)` pattern: + +```tsx +import { render } from 'solid-js/web' +import { TanStackDevtools } from '@tanstack/solid-devtools' + +import App from './App' + +render(() => ( + <> + + + +), document.getElementById('root')!) +``` + +To add plugins, pass them via the `plugins` prop: + +```tsx +import { render } from 'solid-js/web' +import { TanStackDevtools } from '@tanstack/solid-devtools' +import { SolidQueryDevtoolsPanel } from '@tanstack/solid-query-devtools' +import { TanStackRouterDevtoolsPanel } from '@tanstack/solid-router-devtools' + +import App from './App' + +render(() => ( + <> + + , - defaultOpen: true + render: , }, { name: 'TanStack Router', render: , - defaultOpen: false }, - ]} /> - - ) + ]} + /> + +), document.getElementById('root')!) +``` + +## Vue + +Install the Vue devtools adapter: + +```bash +npm install @tanstack/vue-devtools +``` + +> The Vite plugin (`@tanstack/devtools-vite`) is optional for Vue but recommended if you want features like enhanced console logs and go-to-source. + +Add the `TanStackDevtools` component in your root template: + +```vue + + + +``` + +To add plugins, define them as an array and pass them via the `:plugins` binding. Vue uses `component` instead of `render` in plugin definitions: + +```vue + + + +``` + +## Vite Plugin + +All frameworks benefit from the optional Vite plugin, which provides enhanced console logs, go-to-source, and a server event bus. Install it as a dev dependency: + +```bash +npm install -D @tanstack/devtools-vite +``` + +Add it as the **first** plugin in your Vite config: + +```ts +import { devtools } from '@tanstack/devtools-vite' + +export default { + plugins: [ + devtools(), + // ... rest of your plugins here + ], } -``` \ No newline at end of file +``` + +For the full list of Vite plugin options, see the [Vite Plugin](./vite-plugin) documentation. diff --git a/docs/source-inspector.md b/docs/source-inspector.md new file mode 100644 index 00000000..e49535db --- /dev/null +++ b/docs/source-inspector.md @@ -0,0 +1,86 @@ +--- +title: Source Inspector +id: source-inspector +--- + +The source inspector lets you click any element in your app to open its source file in your editor. When activated, the devtools overlay highlights elements as you hover over them and shows their source file location. Click to open the file at the exact line. + +## Requirements + +Two things are needed for the source inspector to work: + +- The `@tanstack/devtools-vite` plugin must be installed and running (dev server only) +- Source injection must be enabled: `injectSource.enabled: true` (this is the default) + +The feature only works in development. In production builds, source attributes are not injected. + +## How It Works + +```mermaid +flowchart LR + A["Your JSX/TSX files"] -- "Babel transform" --> B["data-tsd-source
attributes injected"] + B -- "Hold inspect hotkey
+ click element" --> C["Devtools reads
data-tsd-source"] + C -- "HTTP request" --> D["Vite dev server"] + D -- "launch-editor" --> E["Opens file in editor
at exact line"] +``` + +The Vite plugin uses Babel to parse your JSX/TSX files during development. It adds a `data-tsd-source="filepath:line:column"` attribute to every JSX element. When you activate the source inspector and click an element, the devtools reads this attribute and sends a request to the Vite dev server. The server then launches your editor at the specified file and line using `launch-editor`. + +## Activating the Inspector + +There are two ways to activate the source inspector: + +- **Hotkey**: Hold Shift+Alt+Ctrl (or Shift+Alt+Meta on Mac) — this is the default `inspectHotkey`. While held, the inspector overlay appears. +- **Settings panel**: The inspect hotkey can be customized in the devtools Settings tab. + +The hotkey can also be configured programmatically: + +```ts + +``` + +## Ignoring Files and Components + +Not all elements need source attributes. Use the `ignore` config to exclude files or components: + +```ts +import { devtools } from '@tanstack/devtools-vite' + +export default { + plugins: [ + devtools({ + injectSource: { + enabled: true, + ignore: { + files: ['node_modules', /.*\.test\.(js|ts|jsx|tsx)$/], + components: ['InternalComponent', /.*Provider$/], + }, + }, + }), + ], +} +``` + +Both `files` and `components` accept arrays of strings (exact match) or RegExp patterns. + +## Editor Configuration + +Most popular editors work out of the box via the `launch-editor` package. Supported editors include VS Code, WebStorm, Sublime Text, Atom, and more ([full list](https://github.com/yyx990803/launch-editor?tab=readme-ov-file#supported-editors)). + +For unsupported editors, use the `editor` config: + +```ts +devtools({ + editor: { + name: 'My Editor', + open: async (path, lineNumber, columnNumber) => { + const { exec } = await import('node:child_process') + exec(`myeditor --goto "${path}:${lineNumber}:${columnNumber}"`) + }, + }, +}) +``` diff --git a/docs/vite-plugin.md b/docs/vite-plugin.md index cde63117..fa9a4ecb 100644 --- a/docs/vite-plugin.md +++ b/docs/vite-plugin.md @@ -204,13 +204,47 @@ export default { ## Features -### Go to source +The Vite plugin is composed of several sub-plugins, each handling a specific concern: + +```mermaid +graph TD + vite["@tanstack/devtools-vite"] + vite --> source["Source Injection
Babel → data-tsd-source attrs"] + vite --> server["Server Event Bus
WebSocket + SSE transport"] + vite --> strip["Production Stripping
Remove devtools on build"] + vite --> pipe["Console Piping
Client ↔ Server logs"] + vite --> logs["Enhanced Logs
Source location in console"] + vite --> market["Marketplace
Install plugins via UI"] + vite --> connect["Connection Injection
Port/host placeholders"] +``` + +### Go to Source + +The "Go to Source" feature lets you click on any element in your browser and open its source file in your editor at the exact line where it's defined. It works by injecting `data-tsd-source` attributes into your components via a Babel transformation during development. These attributes encode the file path and line number of each element. + +To use it, activate the source inspector by holding the inspect hotkey (default: Shift+Alt+Ctrl/Meta). An overlay will highlight elements under your cursor and display their source location. Clicking on a highlighted element opens the corresponding file in your editor at the exact line, powered by `launch-editor` under the hood. + +For a complete guide on configuration and usage, see the [Source Inspector](./source-inspector) docs. + +### Console Piping + +When enabled (default), `console.log()` and other console methods in the browser are piped to your terminal, and server-side console output appears in the browser console. This is particularly useful when debugging SSR or API routes — you see all logs in one place without switching between terminal and browser. Configure which log levels are piped via `consolePiping.levels`. + +### Enhanced Logs + +Console logs are enhanced with clickable source locations. In the browser console, each log shows the file and line number where it originated. Click to open the source file in your editor. Enable/disable via the `enhancedLogs` config option. + +### Production Build Stripping -Allows you to open the source location on anything in your browser by clicking on it. +By default (`removeDevtoolsOnBuild: true`), the Vite plugin replaces all devtools imports with empty modules in production builds. This includes: +- `@tanstack/react-devtools` +- `@tanstack/vue-devtools` +- `@tanstack/solid-devtools` +- `@tanstack/preact-devtools` +- `@tanstack/devtools` -To trigger this behavior you need the Devtools Vite plugin installed and configured and -the Panel available on the page. Simply click on any element while holding down the Shift and Ctrl (or Meta) keys. +This ensures zero devtools code reaches production. Set `removeDevtoolsOnBuild: false` to keep devtools in production (see [Production](./production) docs). -### Advanced console logs +### Plugin Marketplace -Allows you to go directly to the console log location directly from the browser/terminal +The Vite plugin enables the in-devtools plugin marketplace. When you browse available plugins in the devtools Settings tab and click "Install", the Vite plugin handles the npm/pnpm/yarn installation and automatically injects the plugin import into your devtools setup file. This only works during development with the Vite dev server running.