@tanstack/devtools 0.10.13 → 0.10.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,429 @@
1
+ ---
2
+ name: devtools-plugin-panel
3
+ description: >
4
+ Build devtools panel components that display emitted event data. Listen via
5
+ EventClient.on(), handle theme (light/dark), use @tanstack/devtools-ui
6
+ components. Plugin registration (name, render, id, defaultOpen), lifecycle
7
+ (mount, activate, destroy), max 3 active plugins. Two paths: Solid.js core
8
+ with devtools-ui for multi-framework support, or framework-specific panels.
9
+ type: core
10
+ library: tanstack-devtools
11
+ library_version: '0.10.12'
12
+ requires:
13
+ - devtools-event-client
14
+ sources:
15
+ - 'TanStack/devtools:docs/building-custom-plugins.md'
16
+ - 'TanStack/devtools:docs/plugin-lifecycle.md'
17
+ - 'TanStack/devtools:docs/plugin-configuration.md'
18
+ - 'TanStack/devtools:packages/devtools/src/context/devtools-context.tsx'
19
+ ---
20
+
21
+ ## TanStackDevtoolsPlugin Interface
22
+
23
+ The low-level contract every plugin implements. Framework adapters wrap this automatically.
24
+
25
+ ```ts
26
+ // Source: packages/devtools/src/context/devtools-context.tsx
27
+ interface TanStackDevtoolsPlugin {
28
+ id?: string
29
+ name: string | ((el: HTMLHeadingElement, theme: 'dark' | 'light') => void)
30
+ render: (el: HTMLDivElement, theme: 'dark' | 'light') => void
31
+ destroy?: (pluginId: string) => void
32
+ defaultOpen?: boolean
33
+ }
34
+ ```
35
+
36
+ - **`name`** (required) -- String tab title, or function receiving `(el, theme)` for custom rendering.
37
+ - **`render`** (required) -- Called on activation with a `<div>` container and theme. Called again on theme change.
38
+ - **`id`** (optional) -- Stable identifier. If omitted: `name.toLowerCase().replace(' ', '-')-{index}`. Explicit ids persist selection across reloads.
39
+ - **`defaultOpen`** (optional) -- Opens panel on first load when no saved state. Max 3 open. Does not override saved preferences.
40
+ - **`destroy`** (optional) -- Called on deactivation or unmount. Framework adapters handle cleanup automatically.
41
+
42
+ ---
43
+
44
+ ## Two Development Paths
45
+
46
+ ### Path 1: Solid.js Core + Framework Adapters (Multi-Framework)
47
+
48
+ Build the panel in Solid.js using `@tanstack/devtools-ui` components. Use `constructCoreClass` for lazy loading, then `createReactPanel`/`createSolidPanel` to wrap for each framework. The devtools core is Solid, so Solid panels run natively.
49
+
50
+ ### Path 2: Framework-Specific Panel (Single Framework)
51
+
52
+ Build directly in your framework and use `createReactPlugin`/`createVuePlugin`/`createSolidPlugin`/`createPreactPlugin` from `@tanstack/devtools-utils`.
53
+
54
+ ---
55
+
56
+ ## Path 1: Solid.js Core Panel
57
+
58
+ ### Step 1: Define Event Map and Create EventClient
59
+
60
+ ```ts
61
+ // src/event-client.ts
62
+ import { EventClient } from '@tanstack/devtools-event-client'
63
+
64
+ type StoreEvents = {
65
+ 'state-changed': { storeName: string; state: unknown; timestamp: number }
66
+ 'action-dispatched': { storeName: string; action: string; payload: unknown }
67
+ reset: void
68
+ }
69
+
70
+ class StoreInspectorClient extends EventClient<StoreEvents> {
71
+ constructor() {
72
+ super({ pluginId: 'store-inspector' })
73
+ }
74
+ }
75
+
76
+ export const storeInspector = new StoreInspectorClient()
77
+ ```
78
+
79
+ Event names are suffixes only. The `pluginId` is prepended automatically: `'store-inspector:state-changed'`.
80
+
81
+ ### Step 2: Build the Solid.js Panel Component
82
+
83
+ ```tsx
84
+ /** @jsxImportSource solid-js */
85
+ import { createSignal, onCleanup, For } from 'solid-js'
86
+ import {
87
+ MainPanel,
88
+ Header,
89
+ HeaderLogo,
90
+ Section,
91
+ SectionTitle,
92
+ JsonTree,
93
+ Button,
94
+ Tag,
95
+ useTheme,
96
+ } from '@tanstack/devtools-ui'
97
+ import { storeInspector } from './event-client'
98
+
99
+ export default function StoreInspectorPanel() {
100
+ const { theme } = useTheme()
101
+ const [state, setState] = createSignal<Record<string, unknown>>({})
102
+ const [actions, setActions] = createSignal<
103
+ Array<{ action: string; payload: unknown }>
104
+ >([])
105
+
106
+ const cleanupState = storeInspector.on('state-changed', (e) => {
107
+ setState((prev) => ({ ...prev, [e.payload.storeName]: e.payload.state }))
108
+ })
109
+ const cleanupActions = storeInspector.on('action-dispatched', (e) => {
110
+ setActions((prev) => [
111
+ ...prev,
112
+ { action: e.payload.action, payload: e.payload.payload },
113
+ ])
114
+ })
115
+
116
+ onCleanup(() => {
117
+ cleanupState()
118
+ cleanupActions()
119
+ })
120
+
121
+ return (
122
+ <MainPanel>
123
+ <Header>
124
+ <HeaderLogo flavor={{ light: '#1a1a2e', dark: '#e0e0e0' }}>
125
+ Store Inspector
126
+ </HeaderLogo>
127
+ </Header>
128
+ <Section>
129
+ <SectionTitle>Current State</SectionTitle>
130
+ <JsonTree value={state()} copyable defaultExpansionDepth={2} />
131
+ </Section>
132
+ <Section>
133
+ <SectionTitle>
134
+ Action Log
135
+ <Tag color="purple" label="Actions" count={actions().length} />
136
+ </SectionTitle>
137
+ <For each={actions()}>
138
+ {(a) => (
139
+ <div>
140
+ <strong>{a.action}</strong>
141
+ <JsonTree value={a.payload} copyable defaultExpansionDepth={1} />
142
+ </div>
143
+ )}
144
+ </For>
145
+ <Button variant="danger" onClick={() => setActions([])}>
146
+ Clear Log
147
+ </Button>
148
+ </Section>
149
+ </MainPanel>
150
+ )
151
+ }
152
+ ```
153
+
154
+ ### Step 3: Create Core Class and Framework Adapters
155
+
156
+ ```ts
157
+ // src/core.ts
158
+ import { constructCoreClass } from '@tanstack/devtools-utils/solid/class'
159
+
160
+ export const [StoreInspectorCore, NoOpStoreInspectorCore] = constructCoreClass(
161
+ () => import('./panel'),
162
+ )
163
+ ```
164
+
165
+ ```tsx
166
+ // src/react.tsx
167
+ import { createReactPanel } from '@tanstack/devtools-utils/react'
168
+ import { StoreInspectorCore } from './core'
169
+
170
+ export const [StoreInspectorPanel, NoOpStoreInspectorPanel] =
171
+ createReactPanel(StoreInspectorCore)
172
+ ```
173
+
174
+ ```tsx
175
+ // src/react-plugin.tsx
176
+ import { createReactPlugin } from '@tanstack/devtools-utils/react'
177
+ import { StoreInspectorPanel } from './react'
178
+
179
+ export const [StoreInspectorPlugin, NoOpStoreInspectorPlugin] =
180
+ createReactPlugin({
181
+ name: 'Store Inspector',
182
+ id: 'store-inspector',
183
+ defaultOpen: true,
184
+ Component: StoreInspectorPanel,
185
+ })
186
+ ```
187
+
188
+ ### Step 4: Register
189
+
190
+ ```tsx
191
+ import { TanStackDevtools } from '@tanstack/react-devtools'
192
+ import { StoreInspectorPlugin } from 'your-package/react-plugin'
193
+
194
+ function App() {
195
+ return (
196
+ <>
197
+ <YourApp />
198
+ <TanStackDevtools plugins={[StoreInspectorPlugin()]} />
199
+ </>
200
+ )
201
+ }
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Path 2: Framework-Specific Panel (React Example)
207
+
208
+ ```tsx
209
+ import { useState, useEffect } from 'react'
210
+ import { EventClient } from '@tanstack/devtools-event-client'
211
+ import { createReactPlugin } from '@tanstack/devtools-utils/react'
212
+
213
+ type MyEvents = {
214
+ 'data-update': { items: Array<{ id: string; value: number }> }
215
+ }
216
+
217
+ class MyPluginClient extends EventClient<MyEvents> {
218
+ constructor() {
219
+ super({ pluginId: 'my-plugin' })
220
+ }
221
+ }
222
+
223
+ export const myPlugin = new MyPluginClient()
224
+
225
+ function MyPluginPanel({ theme }: { theme?: 'light' | 'dark' }) {
226
+ const [items, setItems] = useState<Array<{ id: string; value: number }>>([])
227
+
228
+ useEffect(() => {
229
+ const cleanup = myPlugin.on('data-update', (e) => {
230
+ setItems(e.payload.items)
231
+ })
232
+ return cleanup
233
+ }, [])
234
+
235
+ return (
236
+ <div style={{ color: theme === 'dark' ? '#fff' : '#000' }}>
237
+ <h3>My Plugin</h3>
238
+ <ul>
239
+ {items.map((item) => (
240
+ <li key={item.id}>
241
+ {item.id}: {item.value}
242
+ </li>
243
+ ))}
244
+ </ul>
245
+ </div>
246
+ )
247
+ }
248
+
249
+ export const [MyPlugin, NoOpMyPlugin] = createReactPlugin({
250
+ name: 'My Plugin',
251
+ id: 'my-plugin',
252
+ defaultOpen: false,
253
+ Component: MyPluginPanel,
254
+ })
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Plugin Lifecycle Sequence
260
+
261
+ 1. **Initialization** -- `TanStackDevtoolsCore` receives `plugins` array. Each plugin gets an `id` (explicit or generated).
262
+ 2. **DOM containers created** -- Core creates `<div id="plugin-container-{id}">` and `<h3 id="plugin-title-container-{id}">` per plugin.
263
+ 3. **Activation** -- On tab click or `defaultOpen`, `plugin.render(container, theme)` called.
264
+ 4. **Framework portaling** -- React uses `createPortal`, Solid uses `<Portal>`, Vue uses `<Teleport>`.
265
+ 5. **Theme change** -- `render` called again with new theme value.
266
+ 6. **Deactivation/Unmount** -- `destroy(pluginId)` called if provided. Framework adapters handle cleanup.
267
+
268
+ Active plugin selection persisted in `localStorage` under key `tanstack_devtools_state`.
269
+
270
+ ---
271
+
272
+ ## Common Mistakes
273
+
274
+ ### CRITICAL: Not Cleaning Up Event Listeners
275
+
276
+ Each `on()` returns a cleanup function. Forgetting it causes memory leaks and duplicate handlers.
277
+
278
+ Wrong:
279
+
280
+ ```ts
281
+ useEffect(() => {
282
+ client.on('state', cb)
283
+ }, [])
284
+ ```
285
+
286
+ Correct:
287
+
288
+ ```ts
289
+ useEffect(() => {
290
+ const cleanup = client.on('state', cb)
291
+ return cleanup
292
+ }, [])
293
+ ```
294
+
295
+ In Solid, use `onCleanup()`:
296
+
297
+ ```ts
298
+ const cleanup = storeInspector.on('state-changed', handler)
299
+ onCleanup(cleanup)
300
+ ```
301
+
302
+ Source: docs/building-custom-plugins.md
303
+
304
+ ### HIGH: Oversubscribing to Events in Multiple Components
305
+
306
+ Do not call `on()` in multiple components for the same event. Subscribe once in a shared store/hook.
307
+
308
+ Wrong:
309
+
310
+ ```ts
311
+ function ComponentA() {
312
+ useEffect(() => {
313
+ const c = client.on('state', cb1)
314
+ return c
315
+ }, [])
316
+ }
317
+ function ComponentB() {
318
+ useEffect(() => {
319
+ const c = client.on('state', cb2)
320
+ return c
321
+ }, [])
322
+ }
323
+ ```
324
+
325
+ Correct:
326
+
327
+ ```ts
328
+ function useStoreState() {
329
+ const [state, setState] = useState(null)
330
+ useEffect(() => {
331
+ const cleanup = client.on('state', (e) => setState(e.payload))
332
+ return cleanup
333
+ }, [])
334
+ return state
335
+ }
336
+ ```
337
+
338
+ Source: maintainer interview
339
+
340
+ ### MEDIUM: Hardcoding Repeated Event Payload Fields
341
+
342
+ When emitting events that share common fields, create a shared base object.
343
+
344
+ Wrong:
345
+
346
+ ```ts
347
+ client.emit('state-changed', { storeName: 'main', version: '1.0', state })
348
+ client.emit('action-dispatched', { storeName: 'main', version: '1.0', action })
349
+ ```
350
+
351
+ Correct:
352
+
353
+ ```ts
354
+ const base = { storeName: 'main', version: '1.0' }
355
+ client.emit('state-changed', { ...base, state })
356
+ client.emit('action-dispatched', { ...base, action })
357
+ ```
358
+
359
+ Source: maintainer interview
360
+
361
+ ### MEDIUM: Ignoring Theme Prop in Panel Component
362
+
363
+ Panels must adapt styling to theme. Factory-created plugins receive `props.theme`.
364
+
365
+ Wrong:
366
+
367
+ ```tsx
368
+ function MyPanel() {
369
+ return <div style={{ color: 'white' }}>Always white text</div>
370
+ }
371
+ ```
372
+
373
+ Correct:
374
+
375
+ ```tsx
376
+ function MyPanel({ theme }: { theme?: 'light' | 'dark' }) {
377
+ return (
378
+ <div style={{ color: theme === 'dark' ? '#e0e0e0' : '#1a1a1a' }}>
379
+ Theme-aware text
380
+ </div>
381
+ )
382
+ }
383
+ ```
384
+
385
+ In Solid panels using devtools-ui, use `useTheme()` instead of prop drilling.
386
+
387
+ Source: docs/plugin-lifecycle.md
388
+
389
+ ### MEDIUM: Not Knowing Max 3 Active Plugins Limit
390
+
391
+ `MAX_ACTIVE_PLUGINS = 3` (in `packages/devtools/src/utils/constants.ts`). If more than 3 set `defaultOpen: true`, only the first 3 open. Activating a 4th deactivates the earliest. Single-plugin exception: if only 1 plugin is registered, it opens automatically.
392
+
393
+ Source: packages/devtools/src/utils/get-default-active-plugins.ts
394
+
395
+ ### MEDIUM: Using Raw DOM Manipulation Instead of Framework Portals
396
+
397
+ Framework adapters handle portaling. Do not manually manipulate DOM.
398
+
399
+ Wrong:
400
+
401
+ ```ts
402
+ render: (el) => {
403
+ const div = document.createElement('div')
404
+ div.textContent = 'Hello'
405
+ el.appendChild(div)
406
+ }
407
+ ```
408
+
409
+ Correct:
410
+
411
+ ```tsx
412
+ import { createReactPlugin } from '@tanstack/devtools-utils/react'
413
+ const [Plugin, NoOpPlugin] = createReactPlugin({
414
+ name: 'My Plugin',
415
+ Component: ({ theme }) => <div>Hello</div>,
416
+ })
417
+ ```
418
+
419
+ Source: docs/plugin-lifecycle.md
420
+
421
+ ### MEDIUM: Not Keeping Devtools Packages at Latest Versions
422
+
423
+ All `@tanstack/devtools-*` packages should be on compatible versions. For external plugins, pin to compatible ranges.
424
+
425
+ Source: maintainer interview
426
+
427
+ ## References
428
+
429
+ - [devtools-ui components and API](references/panel-api.md)
@@ -0,0 +1,136 @@
1
+ # Plugin Panel API Reference
2
+
3
+ ## Plugin Factory Functions
4
+
5
+ All factories return `[Plugin, NoOpPlugin]` tuples for production tree-shaking.
6
+
7
+ | Factory | Import Path | Framework |
8
+ | -------------------- | -------------------------------------- | ------------------------ |
9
+ | `createReactPlugin` | `@tanstack/devtools-utils/react` | React |
10
+ | `createSolidPlugin` | `@tanstack/devtools-utils/solid` | Solid.js |
11
+ | `createVuePlugin` | `@tanstack/devtools-utils/vue` | Vue 3 |
12
+ | `createPreactPlugin` | `@tanstack/devtools-utils/preact` | Preact |
13
+ | `createReactPanel` | `@tanstack/devtools-utils/react` | React (wraps Solid core) |
14
+ | `createSolidPanel` | `@tanstack/devtools-utils/solid` | Solid (wraps Solid core) |
15
+ | `constructCoreClass` | `@tanstack/devtools-utils/solid/class` | Core class construction |
16
+
17
+ ### createReactPlugin / createSolidPlugin / createPreactPlugin
18
+
19
+ ```ts
20
+ function createReactPlugin(config: {
21
+ name: string
22
+ id?: string
23
+ defaultOpen?: boolean
24
+ Component: (props: { theme?: 'light' | 'dark' }) => JSX.Element
25
+ }): readonly [() => PluginConfig, () => PluginConfig]
26
+ ```
27
+
28
+ ### createVuePlugin
29
+
30
+ ```ts
31
+ function createVuePlugin<TComponentProps extends Record<string, any>>(
32
+ name: string,
33
+ component: DefineComponent<TComponentProps, {}, unknown>,
34
+ ): readonly [
35
+ (props: TComponentProps) => {
36
+ name: string
37
+ component: DefineComponent
38
+ props: TComponentProps
39
+ },
40
+ (props: TComponentProps) => {
41
+ name: string
42
+ component: Fragment
43
+ props: TComponentProps
44
+ },
45
+ ]
46
+ ```
47
+
48
+ Vue uses positional `(name, component)` args, not an options object.
49
+
50
+ ---
51
+
52
+ ## devtools-ui Components
53
+
54
+ All components are Solid.js. Use in Path 1 (Solid core) panels only.
55
+
56
+ | Component | Purpose |
57
+ | ---------------------- | -------------------------------------------------------------------------------------------- |
58
+ | `MainPanel` | Root container with optional padding |
59
+ | `Header` | Top header bar |
60
+ | `HeaderLogo` | Logo section; accepts `flavor` colors |
61
+ | `Section` | Content section wrapper |
62
+ | `SectionTitle` | `<h3>` section heading |
63
+ | `SectionDescription` | `<p>` description text |
64
+ | `SectionIcon` | Icon wrapper in sections |
65
+ | `JsonTree` | Expandable JSON tree viewer with copy support |
66
+ | `Button` | Variants: primary, secondary, danger, success, info, warning; supports `outline` and `ghost` |
67
+ | `Tag` | Colored label tag with optional count badge |
68
+ | `Select` | Dropdown select with label and description |
69
+ | `Input` | Text input |
70
+ | `Checkbox` | Checkbox input |
71
+ | `TanStackLogo` | TanStack logo SVG |
72
+ | `ThemeContextProvider` | Wraps children with theme context |
73
+ | `useTheme` | Returns `{ theme: Accessor<Theme>, setTheme }` -- must be inside ThemeContextProvider |
74
+
75
+ ### JsonTree Props
76
+
77
+ ```ts
78
+ function JsonTree<TData>(props: {
79
+ value: TData
80
+ copyable?: boolean
81
+ defaultExpansionDepth?: number // default: 1
82
+ collapsePaths?: Array<string>
83
+ config?: { dateFormat?: string }
84
+ }): JSX.Element
85
+ ```
86
+
87
+ ---
88
+
89
+ ## EventClient API (Quick Reference)
90
+
91
+ ```ts
92
+ class EventClient<TEventMap extends Record<string, any>> {
93
+ constructor(config: {
94
+ pluginId: string
95
+ debug?: boolean // default: false
96
+ enabled?: boolean // default: true
97
+ reconnectEveryMs?: number // default: 300
98
+ })
99
+
100
+ emit<TEvent extends keyof TEventMap & string>(
101
+ eventSuffix: TEvent,
102
+ payload: TEventMap[TEvent],
103
+ ): void
104
+
105
+ on<TEvent extends keyof TEventMap & string>(
106
+ eventSuffix: TEvent,
107
+ cb: (event: {
108
+ type: TEvent
109
+ payload: TEventMap[TEvent]
110
+ pluginId?: string
111
+ }) => void,
112
+ options?: { withEventTarget?: boolean },
113
+ ): () => void
114
+
115
+ onAll(cb: (event: { type: string; payload: any }) => void): () => void
116
+ onAllPluginEvents(
117
+ cb: (event: AllDevtoolsEvents<TEventMap>) => void,
118
+ ): () => void
119
+ getPluginId(): string
120
+ }
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Key Source Files
126
+
127
+ | File | Purpose |
128
+ | ----------------------------------------------------------- | -------------------------------------------------------- |
129
+ | `packages/devtools/src/context/devtools-context.tsx` | `TanStackDevtoolsPlugin` interface, plugin ID generation |
130
+ | `packages/devtools/src/core.ts` | `TanStackDevtoolsCore` class |
131
+ | `packages/devtools/src/utils/constants.ts` | `MAX_ACTIVE_PLUGINS = 3` |
132
+ | `packages/devtools/src/utils/get-default-active-plugins.ts` | defaultOpen resolution logic |
133
+ | `packages/event-bus-client/src/plugin.ts` | `EventClient` class |
134
+ | `packages/devtools-utils/src/solid/class.ts` | `constructCoreClass` |
135
+ | `packages/devtools-ui/src/index.ts` | All UI component exports |
136
+ | `packages/devtools-ui/src/components/theme.tsx` | `ThemeContextProvider`, `useTheme` |