From 87af6d88d148f590714192a0c8ae1cce4c496189 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Wed, 11 Mar 2026 11:51:06 +0100 Subject: [PATCH 1/5] feat: add @tanstack/intent agent skills for all packages Scaffold agent-readable SKILL.md files via @tanstack/intent for AI coding agents (Claude Code, Cursor, Copilot, etc.) to generate better code when working with TanStack Devtools. Skills added (9 total across 4 packages): - devtools: app-setup, plugin-panel, production, marketplace - event-bus-client: event-client, instrumentation, bidirectional - devtools-vite: vite-plugin - devtools-utils: framework-adapters (React, Vue, Solid, Preact) Also includes: - CI workflows for skill validation, staleness checks, and intent notifications - Domain map, skill spec, and skill tree artifacts - Intent CLI bin shim and package.json wiring - README note for AI agent users --- .github/workflows/check-skills.yml | 143 +++ .github/workflows/notify-playbooks.yml | 52 + .github/workflows/validate-skills.yml | 52 + README.md | 8 + _artifacts/domain_map.yaml | 1003 +++++++++++++++++ _artifacts/skill_spec.md | 171 +++ _artifacts/skill_tree.yaml | 205 ++++ bin/intent.js | 20 + package.json | 187 +-- .../devtools-framework-adapters/SKILL.md | 251 +++++ .../references/preact.md | 254 +++++ .../references/react.md | 233 ++++ .../references/solid.md | 268 +++++ .../references/vue.md | 256 +++++ .../skills/devtools-vite-plugin/SKILL.md | 306 +++++ .../references/vite-options.md | 404 +++++++ .../skills/devtools-app-setup/SKILL.md | 359 ++++++ .../skills/devtools-marketplace/SKILL.md | 389 +++++++ .../skills/devtools-plugin-panel/SKILL.md | 406 +++++++ .../references/panel-api.md | 122 ++ .../skills/devtools-production/SKILL.md | 427 +++++++ .../skills/devtools-bidirectional/SKILL.md | 471 ++++++++ .../skills/devtools-event-client/SKILL.md | 285 +++++ .../skills/devtools-instrumentation/SKILL.md | 351 ++++++ pnpm-lock.yaml | 103 +- 25 files changed, 6591 insertions(+), 135 deletions(-) create mode 100644 .github/workflows/check-skills.yml create mode 100644 .github/workflows/notify-playbooks.yml create mode 100644 .github/workflows/validate-skills.yml create mode 100644 _artifacts/domain_map.yaml create mode 100644 _artifacts/skill_spec.md create mode 100644 _artifacts/skill_tree.yaml create mode 100644 bin/intent.js create mode 100644 packages/devtools-utils/skills/devtools-framework-adapters/SKILL.md create mode 100644 packages/devtools-utils/skills/devtools-framework-adapters/references/preact.md create mode 100644 packages/devtools-utils/skills/devtools-framework-adapters/references/react.md create mode 100644 packages/devtools-utils/skills/devtools-framework-adapters/references/solid.md create mode 100644 packages/devtools-utils/skills/devtools-framework-adapters/references/vue.md create mode 100644 packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md create mode 100644 packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md create mode 100644 packages/devtools/skills/devtools-app-setup/SKILL.md create mode 100644 packages/devtools/skills/devtools-marketplace/SKILL.md create mode 100644 packages/devtools/skills/devtools-plugin-panel/SKILL.md create mode 100644 packages/devtools/skills/devtools-plugin-panel/references/panel-api.md create mode 100644 packages/devtools/skills/devtools-production/SKILL.md create mode 100644 packages/event-bus-client/skills/devtools-bidirectional/SKILL.md create mode 100644 packages/event-bus-client/skills/devtools-event-client/SKILL.md create mode 100644 packages/event-bus-client/skills/devtools-instrumentation/SKILL.md diff --git a/.github/workflows/check-skills.yml b/.github/workflows/check-skills.yml new file mode 100644 index 00000000..bcf3615e --- /dev/null +++ b/.github/workflows/check-skills.yml @@ -0,0 +1,143 @@ +# check-skills.yml — Drop this into your library repo's .github/workflows/ +# +# Checks for stale intent skills after a release and opens a review PR +# if any skills need attention. The PR body includes a prompt you can +# paste into Claude Code, Cursor, or any coding agent to update them. +# +# Triggers: new release published, or manual workflow_dispatch. +# +# Template variables (replaced by `intent setup`): +# @tanstack/devtools — e.g. @tanstack/query + +name: Check Skills + +on: + release: + types: [published] + workflow_dispatch: {} + +permissions: + contents: write + pull-requests: write + +jobs: + check: + name: Check for stale skills + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install intent + run: npm install -g @tanstack/intent + + - name: Check staleness + id: stale + run: | + OUTPUT=$(npx @tanstack/intent stale --json 2>&1) || true + echo "$OUTPUT" + + # Check if any skills need review + NEEDS_REVIEW=$(echo "$OUTPUT" | node -e " + const input = require('fs').readFileSync('/dev/stdin','utf8'); + try { + const reports = JSON.parse(input); + const stale = reports.flatMap(r => + r.skills.filter(s => s.needsReview).map(s => ({ library: r.library, skill: s.name, reasons: s.reasons })) + ); + if (stale.length > 0) { + console.log(JSON.stringify(stale)); + } + } catch {} + ") + + if [ -z "$NEEDS_REVIEW" ]; then + echo "has_stale=false" >> "$GITHUB_OUTPUT" + else + echo "has_stale=true" >> "$GITHUB_OUTPUT" + # Escape for multiline GH output + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + echo "stale_json<<$EOF" >> "$GITHUB_OUTPUT" + echo "$NEEDS_REVIEW" >> "$GITHUB_OUTPUT" + echo "$EOF" >> "$GITHUB_OUTPUT" + fi + + - name: Build summary + if: steps.stale.outputs.has_stale == 'true' + id: summary + run: | + node -e " + const stale = JSON.parse(process.env.STALE_JSON); + const lines = stale.map(s => + '- **' + s.skill + '** (' + s.library + '): ' + s.reasons.join(', ') + ); + const summary = lines.join('\n'); + + const prompt = [ + 'Review and update the following stale intent skills for @tanstack/devtools:', + '', + ...stale.map(s => '- ' + s.skill + ': ' + s.reasons.join(', ')), + '', + 'For each stale skill:', + '1. Read the current SKILL.md file', + '2. Check what changed in the library since the skill was last updated', + '3. Update the skill content to reflect current APIs and behavior', + '4. Run \`npx @tanstack/intent validate\` to verify the updated skill', + ].join('\n'); + + // Write outputs + const fs = require('fs'); + const env = fs.readFileSync(process.env.GITHUB_OUTPUT, 'utf8'); + const eof = require('crypto').randomBytes(15).toString('base64'); + fs.appendFileSync(process.env.GITHUB_OUTPUT, + 'summary<<' + eof + '\n' + summary + '\n' + eof + '\n' + + 'prompt<<' + eof + '\n' + prompt + '\n' + eof + '\n' + ); + " + env: + STALE_JSON: ${{ steps.stale.outputs.stale_json }} + + - name: Open review PR + if: steps.stale.outputs.has_stale == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ github.event.release.tag_name || 'manual' }}" + BRANCH="skills/review-${VERSION}" + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git commit --allow-empty -m "chore: review stale skills for ${VERSION}" + git push origin "$BRANCH" + + gh pr create \ + --title "Review stale skills (${VERSION})" \ + --body "$(cat <<'PREOF' + ## Stale Skills Detected + + The following skills may need updates after the latest release: + + ${{ steps.summary.outputs.summary }} + + --- + + ### Update Prompt + + Paste this into your coding agent (Claude Code, Cursor, etc.): + + ~~~ + ${{ steps.summary.outputs.prompt }} + ~~~ + + PREOF + )" \ + --head "$BRANCH" \ + --base main diff --git a/.github/workflows/notify-playbooks.yml b/.github/workflows/notify-playbooks.yml new file mode 100644 index 00000000..3b6e2cf3 --- /dev/null +++ b/.github/workflows/notify-playbooks.yml @@ -0,0 +1,52 @@ +# notify-intent.yml — Drop this into your library repo's .github/workflows/ +# +# Fires a repository_dispatch event to TanStack/intent whenever docs or +# source files change on merge to main. This triggers the skill staleness +# check workflow in the intent repo. +# +# Requirements: +# - A fine-grained PAT with contents:write on TanStack/intent stored +# as the INTENT_NOTIFY_TOKEN repository secret. +# +# Template variables (replaced by `intent setup`): +# @tanstack/devtools +# docs/** +# packages/*/src/** + +name: Notify Intent + +on: + push: + branches: [main] + paths: + - 'docs/**' + - 'packages/*/src/**' + +jobs: + notify: + name: Notify TanStack Intent + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Collect changed files + id: changes + run: | + FILES=$(git diff --name-only HEAD~1 HEAD | jq -R -s -c 'split("\n") | map(select(length > 0))') + echo "files=$FILES" >> "$GITHUB_OUTPUT" + + - name: Dispatch to intent repo + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.INTENT_NOTIFY_TOKEN }} + repository: TanStack/intent + event-type: skill-check + client-payload: | + { + "package": "@tanstack/devtools", + "sha": "${{ github.sha }}", + "changed_files": ${{ steps.changes.outputs.files }} + } diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml new file mode 100644 index 00000000..8f39716a --- /dev/null +++ b/.github/workflows/validate-skills.yml @@ -0,0 +1,52 @@ +# validate-skills.yml — Drop this into your library repo's .github/workflows/ +# +# Validates skill files on PRs that touch the skills/ directory. +# Ensures frontmatter is correct, names match paths, and files stay under +# the 500-line limit. + +name: Validate Skills + +on: + pull_request: + paths: + - 'skills/**' + - '**/skills/**' + +jobs: + validate: + name: Validate skill files + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install intent CLI + run: npm install -g @tanstack/intent + + - name: Find and validate skills + run: | + # Find all directories containing SKILL.md files + SKILLS_DIR="" + if [ -d "skills" ]; then + SKILLS_DIR="skills" + elif [ -d "packages" ]; then + # Monorepo — find skills/ under packages + for dir in packages/*/skills; do + if [ -d "$dir" ]; then + echo "Validating $dir..." + intent validate "$dir" + fi + done + exit 0 + fi + + if [ -n "$SKILLS_DIR" ]; then + intent validate "$SKILLS_DIR" + else + echo "No skills/ directory found — skipping validation." + fi diff --git a/README.md b/README.md index 66264b2f..8fdd31ce 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,14 @@ A debugging toolkit that provides a unified interface for inspecting, monitoring ### Read the docs → +## AI Agent Support + +If you use an AI coding agent (Claude Code, Cursor, Copilot, etc.), install the TanStack Devtools skills for better code generation: + +```sh +npx @tanstack/intent@latest install +``` + ## Get Involved - We welcome issues and pull requests! diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml new file mode 100644 index 00000000..c92dedf9 --- /dev/null +++ b/_artifacts/domain_map.yaml @@ -0,0 +1,1003 @@ +# domain_map.yaml +# Generated by skill-domain-discovery +# Library: @tanstack/devtools +# Version: 0.10.12 +# Date: 2026-03-11 +# Status: reviewed + +library: + name: '@tanstack/devtools' + version: '0.10.12' + repository: 'https://github.com/TanStack/devtools' + description: > + Framework-agnostic toolkit for building, composing, and debugging custom + devtools panels via a plugin system with typed event communication. + Supports React, Vue, Solid, and Preact. + primary_framework: 'framework-agnostic' + +domains: + - name: 'Setting up devtools' + slug: 'setup' + description: > + Installing, configuring, and mounting TanStack Devtools in an application. + Covers framework adapter selection, plugin registration, shell configuration, + and Vite plugin integration. + + - name: 'Building plugins' + slug: 'plugin-development' + description: > + Creating custom devtools plugins end-to-end: defining event clients, + building panel components, registering plugins, and publishing to + the marketplace. + + - name: 'Event communication' + slug: 'event-communication' + description: > + The typed event system that plugins use to send and receive data. + Covers EventClient creation, event maps, connection lifecycle, + bidirectional patterns, and cross-tab synchronization. + + - name: 'Framework adaptation' + slug: 'framework-adaptation' + description: > + Creating per-framework plugin adapters using devtools-utils factory + functions. Covers React, Vue, Solid, and Preact differences, + portal/teleport mechanisms, and NoOp variants for tree-shaking. + + - name: 'Build and production' + slug: 'build-production' + description: > + Vite plugin features and production build concerns. Covers source + injection, console piping, production stripping, enhanced logging, + and conditional devtools inclusion. + + - name: 'Library instrumentation' + slug: 'instrumentation' + description: > + Strategically adding event emissions to a library codebase at critical + architecture and debugging points without overdoing it. + +skills: + - name: 'App setup' + slug: 'app-setup' + domain: 'setup' + description: > + End-app developer installs TanStack Devtools, picks the right framework + adapter, registers plugins, and configures the shell. + type: 'core' + packages: + - '@tanstack/react-devtools' + - '@tanstack/vue-devtools' + - '@tanstack/solid-devtools' + - '@tanstack/preact-devtools' + - '@tanstack/devtools' + - '@tanstack/devtools-vite' + covers: + - 'TanStackDevtools component' + - 'plugins prop' + - 'config prop (position, hotkeys, theme, hideUntilHover, requireUrlFlag)' + - 'eventBusConfig prop' + - 'Framework-specific plugin types' + - 'defaultOpen behavior' + - 'localStorage persistence' + tasks: + - 'Install devtools for my React/Vue/Solid/Preact app' + - 'Add TanStack Query and Router devtools to my app' + - 'Configure devtools position, hotkeys, and theme' + - 'Set up devtools with Vite plugin for enhanced features' + - 'Hide devtools trigger until hover' + - 'Require URL flag to enable devtools' + failure_modes: + - mistake: 'Vite plugin not placed first in plugins array' + mechanism: > + Source injection and other transforms must run before framework + plugins. Placing devtools() after other plugins causes source + inspector to not work. + wrong_pattern: | + export default { + plugins: [ + react(), + devtools(), + ], + } + correct_pattern: | + export default { + plugins: [ + devtools(), + react(), + ], + } + source: 'docs/quick-start.md, docs/vite-plugin.md' + priority: 'HIGH' + status: 'active' + + - mistake: 'Vue plugin uses render instead of component' + mechanism: > + Vue adapter uses component reference + props pattern, not JSX render. + Using render field produces silent failure — no panel renders. + wrong_pattern: | + const plugins = [ + { name: 'My Plugin', render: } + ] + correct_pattern: | + const plugins: TanStackDevtoolsVuePlugin[] = [ + { name: 'My Plugin', component: MyComponent } + ] + source: 'docs/framework/vue/adapter.md, packages/vue-devtools/src/vue-devtools.vue' + priority: 'CRITICAL' + status: 'active' + skills: ['app-setup', 'framework-adapters'] + + - mistake: 'Installing as regular dependency for dev-only use' + mechanism: > + Devtools should be devDependencies when using Vite plugin stripping. + Installing as regular deps bloats production node_modules. + wrong_pattern: | + npm install @tanstack/react-devtools @tanstack/devtools-vite + correct_pattern: | + npm install -D @tanstack/react-devtools @tanstack/devtools-vite + source: 'docs/installation.md' + priority: 'MEDIUM' + status: 'active' + + - mistake: 'Mounting TanStackDevtools in SSR without client guard' + mechanism: > + The core shell requires DOM APIs. Mounting during SSR causes errors. + React adapter has use client directive, but custom setups need guards. + wrong_pattern: | + // In an SSR-rendered component without client guard + function App() { + return + } + correct_pattern: | + // React adapter already has 'use client' directive + // For custom setups, guard with typeof document check + import { TanStackDevtools } from '@tanstack/react-devtools' + function App() { + return + } + source: 'packages/devtools/src/core.ts, packages/react-devtools/src/devtools.tsx' + priority: 'HIGH' + status: 'active' + + - name: 'Vite plugin setup' + slug: 'vite-plugin' + domain: 'build-production' + description: > + Configure the Vite plugin for source inspection, console piping, + enhanced logging, server event bus, and production stripping. + type: 'core' + packages: + - '@tanstack/devtools-vite' + covers: + - 'devtools() function and options' + - 'Source injection (data-tsd-source attributes)' + - 'Source inspector activation and hotkeys' + - 'Console piping (client-to-server and server-to-client)' + - 'Enhanced console logs with source locations' + - 'Production build stripping' + - 'Server event bus configuration' + - 'Editor integration (launch-editor)' + - 'Plugin marketplace support' + - 'Connection injection placeholders' + tasks: + - 'Add Vite plugin to my project' + - 'Configure source inspector for click-to-open-in-editor' + - 'Set up console piping between browser and terminal' + - 'Configure custom editor for go-to-source' + - 'Disable specific Vite plugin features' + - 'Configure event bus port and host' + subsystems: + - name: 'Source injection' + package: '@tanstack/devtools-vite' + config_surface: 'injectSource.enabled, injectSource.ignore (files, components)' + - name: 'Console piping' + package: '@tanstack/devtools-vite' + config_surface: 'consolePiping.enabled, consolePiping.levels' + - name: 'Enhanced logging' + package: '@tanstack/devtools-vite' + config_surface: 'enhancedLogs.enabled' + - name: 'Production stripping' + package: '@tanstack/devtools-vite' + config_surface: 'removeDevtoolsOnBuild' + - name: 'Server event bus' + package: '@tanstack/devtools-vite' + config_surface: 'eventBusConfig.port, eventBusConfig.enabled, eventBusConfig.debug' + - name: 'Editor integration' + package: '@tanstack/devtools-vite' + config_surface: 'editor.name, editor.open' + failure_modes: + - mistake: 'Expecting Vite plugin features in production' + mechanism: > + Source injection, console piping, enhanced logging, and the server + event bus only work during development with Vite dev server. + Production builds strip all of this. + source: 'packages/devtools-vite/src/plugin.ts' + priority: 'MEDIUM' + status: 'active' + + - mistake: 'Not placing devtools() first in Vite plugins' + mechanism: > + All devtools sub-plugins use enforce:pre. They must transform code + before framework plugins (React, Vue, etc.) process it. + wrong_pattern: | + plugins: [react(), devtools()] + correct_pattern: | + plugins: [devtools(), react()] + source: 'docs/vite-plugin.md' + priority: 'HIGH' + status: 'active' + skills: ['vite-plugin', 'app-setup'] + + - mistake: 'Source injection on spread props elements' + mechanism: > + The Babel transform skips elements with {...props} spread to avoid + overwriting dynamic attributes. Agent might not realize source + inspector wont work on those elements. + source: 'packages/devtools-vite/src/inject-source.ts' + priority: 'MEDIUM' + status: 'active' + + - mistake: 'Using devtools-vite with non-Vite bundlers' + mechanism: > + The package is Vite-specific (peer dep vite ^6 || ^7). It will not + work with webpack, rspack, or other bundlers. EventClient still works + without it for same-page communication. + source: 'packages/devtools-vite/package.json' + priority: 'HIGH' + status: 'active' + + - mistake: 'Event bus port conflict in multi-project setups' + mechanism: > + Default port 4206 may conflict when running multiple Vite dev servers. + ServerEventBus falls back to OS-assigned port on EADDRINUSE, but + clients may not discover the new port without Vite injection. + source: 'packages/event-bus/src/server/server.ts lines 313-336' + priority: 'MEDIUM' + status: 'active' + + - name: 'Event client creation' + slug: 'event-client-creation' + domain: 'event-communication' + description: > + Create a typed EventClient for a library, define event maps, understand + the connection lifecycle, event namespacing, and queuing behavior. + type: 'core' + packages: + - '@tanstack/devtools-event-client' + covers: + - 'EventClient class and constructor options' + - 'Event map type definitions' + - 'pluginId namespacing (auto-prepend)' + - 'emit() and event queuing' + - 'on(), onAll(), onAllPluginEvents() listeners' + - 'Connection lifecycle (5 retries, 300ms interval)' + - 'enabled/disabled state' + - 'SSR/non-web environment fallbacks' + - 'Internal EventTarget (withEventTarget option)' + tasks: + - 'Create a typed EventClient for my library' + - 'Define an event map with typed payloads' + - 'Emit events from my library code' + - 'Listen for events in my devtools panel' + - 'Handle connection failures gracefully' + failure_modes: + - mistake: 'Including pluginId prefix in event names' + mechanism: > + EventClient auto-prepends pluginId to event names. Writing + emit('my-plugin:state-update') produces the event name + 'my-plugin:my-plugin:state-update' on the bus. + wrong_pattern: | + myClient.emit('my-plugin:state-update', data) + correct_pattern: | + myClient.emit('state-update', data) + source: 'packages/event-bus-client/src/plugin.ts lines 190-199' + priority: 'CRITICAL' + status: 'active' + + - mistake: 'Creating multiple EventClient instances per plugin' + mechanism: > + EventClient should be a singleton per plugin. Multiple instances + with the same pluginId create duplicate event handlers and + multiple connection attempts to the bus. + wrong_pattern: | + function MyPanel() { + const client = new MyEventClient() // new instance per render + client.on('state', cb) + } + correct_pattern: | + // Module-level singleton + export const myClient = new MyEventClient() + + function MyPanel() { + myClient.on('state', cb) + } + source: 'docs/building-custom-plugins.md, packages/event-bus-client/src/plugin.ts' + priority: 'CRITICAL' + status: 'active' + + - mistake: 'Not realizing events drop after 5 failed retries' + mechanism: > + After 5 connection retries (1.5s total), failedToConnect is set + permanently. All subsequent emit() calls are silently dropped. + No error is thrown. + source: 'packages/event-bus-client/src/plugin.ts lines 44-56' + priority: 'HIGH' + status: 'active' + + - mistake: 'Listening before emitting and expecting connection' + mechanism: > + Connection is lazily initiated on first emit(), not on construction + or on(). If you only call on() without ever emitting, the client + never connects to the bus. + source: 'packages/event-bus-client/src/plugin.ts' + priority: 'HIGH' + status: 'active' + + - mistake: 'Using non-serializable payloads in events' + mechanism: > + Events are serialized via JSON when crossing process/tab boundaries + (WebSocket, SSE, BroadcastChannel). Functions, DOM nodes, and + circular references will fail silently or produce empty payloads. + wrong_pattern: | + client.emit('update', { + callback: () => {}, // function - not serializable + element: document.body, // DOM node + }) + correct_pattern: | + client.emit('update', { + count: 42, + label: 'my-update', + timestamp: Date.now(), + }) + source: 'packages/event-bus/src/utils/json.ts, docs/bidirectional-communication.md' + priority: 'HIGH' + status: 'active' + skills: ['event-client-creation', 'bidirectional-communication'] + + - mistake: 'Non-unique pluginId causing event collisions' + mechanism: > + If two plugins use the same pluginId, their events collide on + the bus. One plugin receives events intended for the other, + causing incorrect state and hard-to-debug behavior. + wrong_pattern: | + // Plugin A + class PluginA extends EventClient { + constructor() { super({ pluginId: 'my-plugin' }) } + } + // Plugin B — same pluginId! + class PluginB extends EventClient { + constructor() { super({ pluginId: 'my-plugin' }) } + } + correct_pattern: | + // Plugin A + class PluginA extends EventClient { + constructor() { super({ pluginId: 'query-inspector' }) } + } + // Plugin B — unique pluginId + class PluginB extends EventClient { + constructor() { super({ pluginId: 'router-inspector' }) } + } + source: 'maintainer interview' + priority: 'CRITICAL' + status: 'active' + + - mistake: 'Not stripping EventClient emit calls for production' + mechanism: > + The Vite plugin strips framework adapter imports but NOT + @tanstack/devtools-event-client imports. Library authors must + decide whether to keep or guard emit() calls in production. + The enabled:false option or conditional guards are needed. + wrong_pattern: | + // Library code — emits in production with no guard + export function updateState(state) { + myClient.emit('state-changed', state) + } + correct_pattern: | + // Option 1: Use enabled flag + const myClient = new MyClient({ + pluginId: 'my-lib', + enabled: process.env.NODE_ENV === 'development', + }) + + // Option 2: Conditional guard + export function updateState(state) { + if (__DEV__) myClient.emit('state-changed', state) + } + source: 'maintainer interview' + priority: 'HIGH' + status: 'active' + + - name: 'Strategic instrumentation' + slug: 'strategic-instrumentation' + domain: 'instrumentation' + description: > + Analyze a library codebase to find critical architecture and debugging + points, then add strategic event emissions that provide maximum debugging + value with minimum noise. + type: 'core' + packages: + - '@tanstack/devtools-event-client' + covers: + - 'Identifying critical architecture points (middleware, state transitions, lifecycle hooks)' + - 'Choosing what to emit vs what to skip' + - 'Consolidating multiple events into single meaningful emissions' + - 'Performance-aware emission patterns' + - 'Middleware/interceptor integration patterns' + - 'Observer/subscription emission patterns' + tasks: + - 'Instrument my library with devtools events' + - 'Find the right places to emit events in my codebase' + - 'Emit one meaningful event instead of many noisy ones' + - 'Add devtools support to my state management library' + - 'Instrument middleware pipelines for debugging' + failure_modes: + - mistake: 'Emitting too many granular events' + mechanism: > + Emitting 15 events for what could be 1 consolidated event creates + noise in the devtools panel and performance overhead. Consolidate + related state changes into single emissions. + wrong_pattern: | + function processRequest(req) { + client.emit('request-start', { id: req.id }) + client.emit('request-parsed', { body: req.body }) + client.emit('request-validated', { valid: true }) + client.emit('middleware-1-start', {}) + client.emit('middleware-1-end', {}) + // ... 10 more events + } + correct_pattern: | + function processRequest(req) { + const result = runPipeline(req) + client.emit('request-processed', { + id: req.id, + duration: result.duration, + middlewareChain: result.middlewareNames, + status: result.status, + }) + } + source: 'maintainer interview' + priority: 'HIGH' + status: 'active' + + - mistake: 'Emitting in hot loops without debouncing' + mechanism: > + Emitting events inside tight loops or rapid state updates causes + performance degradation. Events should be debounced or batched + for high-frequency operations. + source: 'docs/bidirectional-communication.md' + priority: 'HIGH' + status: 'active' + + - mistake: 'Not emitting at architecture boundaries' + mechanism: > + The highest-value emission points are architecture boundaries: + before/after middleware, on state transitions, at lifecycle hooks. + Emitting inside implementation details misses the forest for trees. + source: 'maintainer interview' + priority: 'MEDIUM' + status: 'active' + + - name: 'Plugin panel development' + slug: 'plugin-panel-development' + domain: 'plugin-development' + description: > + Build the devtools panel component that displays data emitted by + your library. Use devtools-ui components, handle theming, and + manage event listener lifecycle. + type: 'core' + packages: + - '@tanstack/devtools-ui' + - '@tanstack/devtools-event-client' + covers: + - 'Panel component structure' + - 'Listening to events via on()' + - 'Theme handling (light/dark)' + - 'devtools-ui components (JSONTree, buttons, inputs, sections)' + - 'Plugin registration (name, render, id, defaultOpen)' + - 'Plugin lifecycle (mount, activate, destroy)' + - 'Max 3 concurrent active plugins' + - 'localStorage state persistence' + - 'Two paths: Solid.js core with devtools-ui for multi-framework, or framework-specific panels directly' + - 'Transparent server/client event bridging (emit on server, receive on client and vice versa)' + tasks: + - 'Build a panel that displays my library state' + - 'Use devtools-ui JSON viewer to show data' + - 'Handle dark/light theme in my panel' + - 'Register my panel as a devtools plugin' + - 'Set my plugin to open by default' + failure_modes: + - mistake: 'Not cleaning up event listeners' + mechanism: > + Each on() call returns a cleanup function. Forgetting to call it + in useEffect cleanup (React), onCleanup (Solid), or onUnmounted + (Vue) causes memory leaks and duplicate handlers on re-render. + wrong_pattern: | + useEffect(() => { + client.on('state', (e) => setState(e.payload)) + // Missing cleanup! + }, []) + correct_pattern: | + useEffect(() => { + const cleanup = client.on('state', (e) => setState(e.payload)) + return cleanup + }, []) + source: 'docs/building-custom-plugins.md, docs/event-system.md' + priority: 'CRITICAL' + status: 'active' + + - mistake: 'Ignoring theme prop in panel component' + mechanism: > + The render function receives theme as second argument. Panel + components should adapt their styling. Agents often hardcode + light or dark styles. + source: 'docs/plugin-lifecycle.md' + priority: 'MEDIUM' + status: 'active' + + - mistake: 'Not knowing max 3 active plugins limit' + mechanism: > + At most 3 plugin panels can be displayed simultaneously + (MAX_ACTIVE_PLUGINS constant). Setting more than 3 plugins + to defaultOpen:true only opens the first 3. + source: 'packages/devtools/src/utils/get-default-active-plugins.ts' + priority: 'MEDIUM' + status: 'active' + + - mistake: 'Using raw DOM manipulation instead of framework portals' + mechanism: > + Plugin render() receives a DOM element, but framework adapters + handle portaling automatically. Agents might try to manually + manipulate the DOM instead of using the adapter pattern. + source: 'docs/plugin-lifecycle.md' + priority: 'MEDIUM' + status: 'active' + + - mistake: 'Oversubscribing to events in multiple places' + mechanism: > + AI agents subscribe to the same event in 5 different components + instead of subscribing once and sharing state. This creates + duplicate handlers, wasted memory, and inconsistent state. + wrong_pattern: | + // Component A subscribes + client.on('state', (e) => setStateA(e.payload)) + // Component B also subscribes to same event + client.on('state', (e) => setStateB(e.payload)) + // Component C also subscribes... + client.on('state', (e) => setStateC(e.payload)) + correct_pattern: | + // Subscribe once in a shared store/context + const cleanup = client.on('state', (e) => { + sharedStore.setState(e.payload) + }) + // Components read from the shared store + source: 'maintainer interview' + priority: 'HIGH' + status: 'active' + + - mistake: 'Hardcoding repeated event payload fields' + mechanism: > + When multiple events share common fields (name, type, version), + agents hardcode each field separately instead of creating a shared + object and spreading it. Makes code harder to maintain. + wrong_pattern: | + client.emit('created', { name: lib.name, type: lib.type, version: lib.version, action: 'create' }) + client.emit('updated', { name: lib.name, type: lib.type, version: lib.version, action: 'update' }) + client.emit('deleted', { name: lib.name, type: lib.type, version: lib.version, action: 'delete' }) + correct_pattern: | + const baseInfo = { name: lib.name, type: lib.type, version: lib.version } + client.emit('created', { ...baseInfo, action: 'create' }) + client.emit('updated', { ...baseInfo, action: 'update' }) + client.emit('deleted', { ...baseInfo, action: 'delete' }) + source: 'maintainer interview' + priority: 'MEDIUM' + status: 'active' + skills: ['plugin-panel-development', 'strategic-instrumentation'] + + - mistake: 'Not keeping devtools packages at latest versions' + mechanism: > + Agents pin or use old versions of devtools packages. While exact + version sync is not required, using latest versions across all + devtools packages increases stability and compatibility. + source: 'maintainer interview' + priority: 'MEDIUM' + status: 'active' + skills: ['app-setup', 'plugin-panel-development'] + + - name: 'Framework adapters' + slug: 'framework-adapters' + domain: 'framework-adaptation' + description: > + Use devtools-utils factory functions to create per-framework plugin + adapters with NoOp variants for production tree-shaking. + type: 'framework' + packages: + - '@tanstack/devtools-utils' + covers: + - 'createReactPlugin() / createReactPanel()' + - 'createSolidPlugin() / createSolidPanel()' + - 'createVuePlugin() / createVuePanel()' + - 'createPreactPlugin() / createPreactPanel()' + - '[Plugin, NoOpPlugin] tuple pattern' + - 'DevtoolsPanelProps (theme prop)' + - 'Class-based panel factories' + - 'Framework-specific portal/teleport mechanisms' + tasks: + - 'Create React and Solid adapters for my plugin' + - 'Use factory functions to build framework adapters' + - 'Set up production tree-shaking with NoOp variants' + - 'Build a class-based devtools core with panel wrappers' + subsystems: + - name: 'React adapter' + package: '@tanstack/devtools-utils' + config_surface: 'createReactPlugin({name, id, defaultOpen, Component})' + - name: 'Solid adapter' + package: '@tanstack/devtools-utils' + config_surface: 'createSolidPlugin({name, id, defaultOpen, Component})' + - name: 'Vue adapter' + package: '@tanstack/devtools-utils' + config_surface: 'createVuePlugin(name, component)' + - name: 'Preact adapter' + package: '@tanstack/devtools-utils' + config_surface: 'createPreactPlugin({name, id, defaultOpen, Component})' + failure_modes: + - mistake: 'Using React JSX pattern in Vue adapter' + mechanism: > + Vue uses component reference + props, not JSX render elements. + createVuePlugin takes (name, component) not an options object. + wrong_pattern: | + const [Plugin] = createVuePlugin({ + name: 'My Plugin', + Component: MyPanel, + }) + correct_pattern: | + const [Plugin] = createVuePlugin('My Plugin', MyPanel) + source: 'packages/devtools-utils/src/vue/plugin.ts' + priority: 'CRITICAL' + status: 'active' + + - mistake: 'Solid render prop not wrapped in function' + mechanism: > + Solid requires render to be a function returning JSX, not raw JSX. + Passing raw JSX causes it to evaluate immediately and not react + to signal changes. + wrong_pattern: | + , + }]} + /> + correct_pattern: | + , + }]} + /> + source: 'docs/framework/solid/adapter.md' + priority: 'CRITICAL' + status: 'active' + + - mistake: 'Ignoring NoOp variant for production' + mechanism: > + Factory functions return [Plugin, NoOpPlugin] tuple. The NoOp + variant renders nothing and is meant for production builds. + Ignoring it means devtools code ships to production. + wrong_pattern: | + const [MyPlugin] = createReactPlugin({ ... }) + // Always uses MyPlugin, even in production + correct_pattern: | + const [MyPlugin, NoOpPlugin] = createReactPlugin({ ... }) + const ActivePlugin = process.env.NODE_ENV === 'development' + ? MyPlugin + : NoOpPlugin + source: 'docs/devtools-utils.md' + priority: 'HIGH' + status: 'active' + + - mistake: 'Not passing theme prop to panel component' + mechanism: > + Factory-created plugins receive DevtoolsPanelProps with theme. + Agent might destructure props but not use theme, resulting in + panel that doesnt match devtools dark/light mode. + source: 'packages/devtools-utils/src/react/plugin.tsx' + priority: 'MEDIUM' + status: 'active' + + - name: 'Bidirectional communication' + slug: 'bidirectional-communication' + domain: 'event-communication' + description: > + Implement two-way event patterns between the devtools panel and + application: commands, state editing, and time-travel debugging. + type: 'core' + packages: + - '@tanstack/devtools-event-client' + covers: + - 'App-to-devtools observation pattern' + - 'Devtools-to-app command pattern' + - 'Time-travel debugging with snapshots and revert' + - 'Bidirectional event map design' + - 'structuredClone for snapshot safety' + - 'Debouncing frequent emissions' + tasks: + - 'Add a reset button to my devtools panel' + - 'Implement state editing from devtools' + - 'Build time-travel debugging for my state library' + - 'Send commands from devtools panel back to app' + failure_modes: + - mistake: 'Not using structuredClone for snapshots' + mechanism: > + Passing state object references directly means future mutations + corrupt historical snapshots. Must deep-clone before emitting. + wrong_pattern: | + client.emit('snapshot', { state, timestamp: Date.now() }) + correct_pattern: | + client.emit('snapshot', { + state: structuredClone(state), + timestamp: Date.now(), + }) + source: 'docs/bidirectional-communication.md' + priority: 'HIGH' + status: 'active' + + - mistake: 'Not distinguishing observation from command events' + mechanism: > + Mixing observation and command events in the same namespace causes + confusion. Use distinct suffixes like state-update (observation) + vs set-state (command). + source: 'docs/bidirectional-communication.md' + priority: 'MEDIUM' + status: 'active' + + - mistake: 'Non-serializable payloads in cross-tab scenarios' + mechanism: > + Events crossing tab/process boundaries are JSON-serialized. + Functions, DOM nodes, and circular references silently fail. + source: 'packages/event-bus/src/utils/json.ts' + priority: 'HIGH' + status: 'active' + skills: ['event-client-creation', 'bidirectional-communication'] + + - name: 'Production setup' + slug: 'production-setup' + domain: 'build-production' + description: > + Handle devtools in production vs development: tree-shaking, + conditional imports, Vite stripping, and dev-only dependencies. + type: 'lifecycle' + packages: + - '@tanstack/devtools-vite' + - '@tanstack/devtools' + covers: + - 'removeDevtoolsOnBuild option' + - 'Dev dependency vs regular dependency' + - '/production sub-export' + - 'Conditional imports with environment variables' + - 'NoOp plugin variants for tree-shaking' + - 'Non-Vite production exclusion patterns' + tasks: + - 'Strip devtools from production build' + - 'Keep devtools in production intentionally' + - 'Set up conditional devtools loading' + - 'Handle production builds without Vite' + failure_modes: + - mistake: 'Keeping devtools in production without disabling stripping' + mechanism: > + Setting removeDevtoolsOnBuild defaults to true. If you want + devtools in production, you must explicitly set it to false AND + install as regular dependency. + wrong_pattern: | + // vite.config.ts - devtools stripped silently + devtools() + // package.json - devDependency won't be available + "devDependencies": { "@tanstack/react-devtools": "..." } + correct_pattern: | + // vite.config.ts + devtools({ removeDevtoolsOnBuild: false }) + // package.json - regular dependency + "dependencies": { "@tanstack/react-devtools": "..." } + source: 'docs/production.md' + priority: 'HIGH' + status: 'active' + + - mistake: 'Not using /production sub-export for production builds' + mechanism: > + Production builds should use the /production sub-export which + includes only production-safe code without dev-only features. + source: 'docs/production.md' + priority: 'MEDIUM' + status: 'active' + + - mistake: 'Non-Vite projects not excluding devtools manually' + mechanism: > + Without the Vite plugin, devtools are not automatically stripped. + Non-Vite projects must manually exclude via environment variables + or conditional imports. + wrong_pattern: | + // Always imports devtools regardless of environment + import { TanStackDevtools } from '@tanstack/react-devtools' + correct_pattern: | + const Devtools = process.env.NODE_ENV === 'development' + ? await import('./devtools-setup') + : () => null + source: 'docs/production.md' + priority: 'HIGH' + status: 'active' + + - name: 'Marketplace publishing' + slug: 'marketplace-publishing' + domain: 'plugin-development' + description: > + Publish a devtools plugin to npm and submit it to the TanStack + Devtools Marketplace registry for discovery and one-click install. + type: 'lifecycle' + packages: + - '@tanstack/devtools' + covers: + - 'PluginMetadata registry format' + - 'plugin-registry.ts submission' + - 'pluginImport configuration (importName, type)' + - 'requires field (packageName, minVersion, maxVersion)' + - 'Framework tagging' + - 'Multi-framework submissions' + - 'Featured plugins' + tasks: + - 'Submit my plugin to the TanStack Marketplace' + - 'Configure plugin metadata for auto-install' + - 'Submit plugins for multiple frameworks' + failure_modes: + - mistake: 'Missing pluginImport metadata for auto-install' + mechanism: > + Without pluginImport.importName and pluginImport.type, the + marketplace cant auto-inject the plugin into user code. + The install button will add the package but not wire it up. + source: 'docs/third-party-plugins.md, packages/devtools-vite/src/inject-plugin.ts' + priority: 'HIGH' + status: 'active' + + - mistake: 'Not specifying requires.minVersion' + mechanism: > + If your plugin depends on a minimum version of another library, + omitting minVersion means users with older versions will get + runtime errors. + source: 'docs/third-party-plugins.md' + priority: 'MEDIUM' + status: 'active' + + - mistake: 'Submitting without framework field' + mechanism: > + The framework field enables filtering in the marketplace UI. + Without it, users cant find framework-specific plugins. + source: 'docs/third-party-plugins.md' + priority: 'MEDIUM' + status: 'active' + +tensions: + - name: 'Instrumentation completeness vs performance' + skills: ['strategic-instrumentation', 'event-client-creation'] + description: > + Strategic instrumentation wants comprehensive debugging data, but + emitting too many events degrades application performance. Events + crossing WebSocket/SSE boundaries add latency. + implication: > + An agent optimizing for debugging coverage will emit events in hot + paths and tight loops. An agent optimizing for performance will skip + critical debugging points. The right balance is architecture-boundary + emissions with debouncing for high-frequency updates. + + - name: 'Development convenience vs production safety' + skills: ['app-setup', 'production-setup'] + description: > + Easy dev setup (devDependencies, auto-stripping) conflicts with + production usage where you need regular deps and explicit opt-in. + implication: > + An agent setting up devtools will install as devDependency and rely + on Vite stripping. If the user later wants production devtools, the + agent needs to undo those decisions — change to regular dep, disable + stripping, use production sub-export. + + - name: 'Framework-agnostic core vs framework ergonomics' + skills: ['framework-adapters', 'plugin-panel-development'] + description: > + The Solid.js core provides consistency but framework adapters have + different patterns. Vue uses component not render, Solid requires + function wrappers, React uses JSX directly. + implication: > + An agent trained on React patterns will use render/JSX everywhere. + Vue and Solid have fundamentally different plugin definitions that + agents consistently get wrong without framework-specific guidance. + +cross_references: + - from: 'app-setup' + to: 'vite-plugin' + reason: > + After basic setup, Vite plugin adds enhanced features. Agent should + know Vite plugin is optional but recommended. + + - from: 'event-client-creation' + to: 'strategic-instrumentation' + reason: > + After creating an EventClient, the next step is instrumenting code. + Understanding the event system informs where to emit. + + - from: 'event-client-creation' + to: 'plugin-panel-development' + reason: > + The EventClient emits events that the panel listens to. Both sides + must use the same event map and understand the pluginId namespacing. + + - from: 'plugin-panel-development' + to: 'framework-adapters' + reason: > + After building a panel, wrapping it in framework adapters makes it + usable across React, Vue, Solid, and Preact. + + - from: 'framework-adapters' + to: 'production-setup' + reason: > + NoOp variants from factory functions are the primary mechanism for + production tree-shaking. + + - from: 'bidirectional-communication' + to: 'event-client-creation' + reason: > + Bidirectional patterns extend the event client with command events. + Understanding the base event system is prerequisite. + + - from: 'vite-plugin' + to: 'production-setup' + reason: > + The Vite plugin handles production stripping. Understanding its + defaults informs production configuration decisions. + + - from: 'plugin-panel-development' + to: 'marketplace-publishing' + reason: > + After building a working plugin, publishing to the marketplace is + the distribution step. Plugin metadata must match the code. + + - from: 'strategic-instrumentation' + to: 'bidirectional-communication' + reason: > + Strategic emission points often benefit from bidirectional patterns — + not just showing state but allowing developers to interact with it. + +gaps: + - skill: 'strategic-instrumentation' + question: > + What are the specific telemetry/instrumentation patterns built into + TanStack Devtools that the maintainer mentioned are undocumented? + context: > + Maintainer said telemetry support details are missing from docs. + To be investigated from source code. + status: 'resolved' + + - skill: 'plugin-panel-development' + question: > + How does the Picture-in-Picture (PiP) window mode affect plugin + rendering? Do plugins need to handle PiP-specific concerns? + context: > + PiP opens a new browser window and renders devtools there. Has zero + impact on plugin development — works seamlessly with no special handling. + status: 'resolved' + + - skill: 'event-client-creation' + question: > + Are there recommended patterns for testing EventClient instances + and plugin panels in isolation (unit tests, integration tests)? + context: > + No recommended testing patterns exist currently. EventClient works + for same-page communication without the full devtools shell. + status: 'resolved' + + - skill: 'strategic-instrumentation' + question: > + What are the performance benchmarks for event emission? How many + events per second can the bus handle before degradation? + context: > + No formal benchmarks exist. General guidance: prototype freely with + many events, then reduce emission count for production. Debounce + high-frequency updates. + status: 'resolved' diff --git a/_artifacts/skill_spec.md b/_artifacts/skill_spec.md new file mode 100644 index 00000000..222ca6db --- /dev/null +++ b/_artifacts/skill_spec.md @@ -0,0 +1,171 @@ +# TanStack Devtools — Skill Spec + +TanStack Devtools is a framework-agnostic toolkit for building custom devtools panels via a plugin system with typed event communication. It provides the shell UI, event transport, framework adapters, and build tooling so library authors can focus on their debugging UI while end-app developers get a unified devtools experience across React, Vue, Solid, and Preact. + +## Domains + +| Domain | Description | Skills | +| --- | --- | --- | +| Setting up devtools | Installing, configuring, and mounting devtools in an app | app-setup | +| Building plugins | Creating custom plugins: event clients, panels, marketplace | plugin-panel-development, marketplace-publishing | +| Event communication | Typed event system for plugin data flow | event-client-creation, bidirectional-communication | +| Framework adaptation | Per-framework adapters using factory functions | framework-adapters | +| Build and production | Vite plugin features and production concerns | vite-plugin, production-setup | +| Library instrumentation | Strategic event emission placement in codebases | strategic-instrumentation | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| --- | --- | --- | --- | --- | +| app-setup | core | setup | TanStackDevtools component, plugins prop, config, framework adapters | 4 | +| vite-plugin | core | build-production | Source injection, console piping, enhanced logs, production stripping, server bus | 5 | +| event-client-creation | core | event-communication | EventClient class, event maps, connection lifecycle, namespacing | 7 | +| strategic-instrumentation | core | instrumentation | Finding critical emission points, consolidation, performance | 3 | +| plugin-panel-development | core | plugin-development | Panel components, event listening, theming, devtools-ui | 7 | +| framework-adapters | framework | framework-adaptation | createReactPlugin, createSolidPlugin, createVuePlugin, NoOp variants | 4 | +| bidirectional-communication | core | event-communication | Commands, state editing, time-travel, structuredClone | 3 | +| production-setup | lifecycle | build-production | removeDevtoolsOnBuild, conditional imports, NoOp patterns | 3 | +| marketplace-publishing | lifecycle | plugin-development | PluginMetadata, registry format, auto-install config | 3 | + +## Failure Mode Inventory + +### app-setup (4 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | Vite plugin not placed first in plugins array | HIGH | docs/vite-plugin.md | vite-plugin | +| 2 | Vue plugin uses render instead of component | CRITICAL | docs/framework/vue/adapter.md | framework-adapters | +| 3 | Installing as regular dep for dev-only use | MEDIUM | docs/installation.md | — | +| 4 | Mounting TanStackDevtools in SSR without client guard | HIGH | packages/devtools/src/core.ts | — | + +### vite-plugin (5 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | Expecting Vite plugin features in production | MEDIUM | packages/devtools-vite/src/plugin.ts | — | +| 2 | Not placing devtools() first in Vite plugins | HIGH | docs/vite-plugin.md | app-setup | +| 3 | Source injection on spread props elements | MEDIUM | packages/devtools-vite/src/inject-source.ts | — | +| 4 | Using devtools-vite with non-Vite bundlers | HIGH | packages/devtools-vite/package.json | — | +| 5 | Event bus port conflict in multi-project setups | MEDIUM | packages/event-bus/src/server/server.ts | — | + +### event-client-creation (7 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | Including pluginId prefix in event names | CRITICAL | packages/event-bus-client/src/plugin.ts | — | +| 2 | Creating multiple EventClient instances per plugin | CRITICAL | docs/building-custom-plugins.md | — | +| 3 | Non-unique pluginId causing event collisions | CRITICAL | maintainer interview | — | +| 4 | Not realizing events drop after 5 failed retries | HIGH | packages/event-bus-client/src/plugin.ts | — | +| 5 | Listening before emitting and expecting connection | HIGH | packages/event-bus-client/src/plugin.ts | — | +| 6 | Using non-serializable payloads | HIGH | packages/event-bus/src/utils/json.ts | bidirectional-communication | +| 7 | Not stripping EventClient emit calls for production | HIGH | maintainer interview | production-setup | + +### strategic-instrumentation (3 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | Emitting too many granular events | HIGH | maintainer interview | — | +| 2 | Emitting in hot loops without debouncing | HIGH | docs/bidirectional-communication.md | — | +| 3 | Not emitting at architecture boundaries | MEDIUM | maintainer interview | — | + +### plugin-panel-development (7 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | Not cleaning up event listeners | CRITICAL | docs/building-custom-plugins.md | — | +| 2 | Oversubscribing to events in multiple places | HIGH | maintainer interview | — | +| 3 | Hardcoding repeated event payload fields | MEDIUM | maintainer interview | strategic-instrumentation | +| 4 | Ignoring theme prop in panel component | MEDIUM | docs/plugin-lifecycle.md | — | +| 5 | Not knowing max 3 active plugins limit | MEDIUM | packages/devtools/src/utils/get-default-active-plugins.ts | — | +| 6 | Using raw DOM manipulation instead of framework portals | MEDIUM | docs/plugin-lifecycle.md | — | +| 7 | Not keeping devtools packages at latest versions | MEDIUM | maintainer interview | app-setup | + +### framework-adapters (4 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | Using React JSX pattern in Vue adapter | CRITICAL | packages/devtools-utils/src/vue/plugin.ts | app-setup | +| 2 | Solid render prop not wrapped in function | CRITICAL | docs/framework/solid/adapter.md | — | +| 3 | Ignoring NoOp variant for production | HIGH | docs/devtools-utils.md | production-setup | +| 4 | Not passing theme prop to panel component | MEDIUM | packages/devtools-utils/src/react/plugin.tsx | — | + +### bidirectional-communication (3 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | Not using structuredClone for snapshots | HIGH | docs/bidirectional-communication.md | — | +| 2 | Not distinguishing observation from command events | MEDIUM | docs/bidirectional-communication.md | — | +| 3 | Non-serializable payloads in cross-tab scenarios | HIGH | packages/event-bus/src/utils/json.ts | event-client-creation | + +### production-setup (3 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | Keeping devtools in prod without disabling stripping | HIGH | docs/production.md | — | +| 2 | Not using /production sub-export | MEDIUM | docs/production.md | — | +| 3 | Non-Vite projects not excluding devtools manually | HIGH | docs/production.md | — | + +### marketplace-publishing (3 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | Missing pluginImport metadata for auto-install | HIGH | docs/third-party-plugins.md | — | +| 2 | Not specifying requires.minVersion | MEDIUM | docs/third-party-plugins.md | — | +| 3 | Submitting without framework field | MEDIUM | docs/third-party-plugins.md | — | + +## Tensions + +| Tension | Skills | Agent implication | +| --- | --- | --- | +| Instrumentation completeness vs performance | strategic-instrumentation ↔ event-client-creation | Agent optimizing for debugging coverage emits in hot paths; agent optimizing for performance skips critical points | +| Development convenience vs production safety | app-setup ↔ production-setup | Agent installs as devDep with stripping; later production usage requires undoing those decisions | +| Framework-agnostic core vs framework ergonomics | framework-adapters ↔ plugin-panel-development | Agent trained on React uses render/JSX everywhere; Vue and Solid have different plugin definitions | + +## Cross-References + +| From | To | Reason | +| --- | --- | --- | +| app-setup | vite-plugin | Vite plugin adds enhanced features after basic setup | +| event-client-creation | strategic-instrumentation | Understanding event system informs where to emit | +| event-client-creation | plugin-panel-development | Client emits, panel listens — same event map | +| plugin-panel-development | framework-adapters | After building panel, wrap in framework adapters | +| framework-adapters | production-setup | NoOp variants are the primary tree-shaking mechanism | +| bidirectional-communication | event-client-creation | Bidirectional extends the base event system | +| vite-plugin | production-setup | Vite plugin handles production stripping defaults | +| plugin-panel-development | marketplace-publishing | After building plugin, publish to marketplace | +| strategic-instrumentation | bidirectional-communication | Emission points benefit from bidirectional patterns | + +## Subsystems & Reference Candidates + +| Skill | Subsystems | Reference candidates | +| --- | --- | --- | +| vite-plugin | Source injection, Console piping, Enhanced logging, Production stripping, Server event bus, Editor integration | Vite plugin options reference | +| framework-adapters | React adapter, Solid adapter, Vue adapter, Preact adapter | — | +| app-setup | — | Config options (position, hotkeys, theme, eventBus) | +| event-client-creation | — | EventClient constructor options, connection lifecycle states | + +## Remaining Gaps + +All gaps resolved during maintainer interview: +- PiP window mode has zero impact on plugin development +- No formal testing patterns exist; EventClient works standalone for same-page communication +- No performance benchmarks; guidance is to prototype freely, reduce for production +- Telemetry patterns to be derived from source code analysis + +## Recommended Skill File Structure + +- **Core skills:** app-setup, event-client-creation, strategic-instrumentation, plugin-panel-development, bidirectional-communication +- **Framework skills:** framework-adapters (covers all four frameworks with subsystem files) +- **Lifecycle skills:** production-setup, marketplace-publishing +- **Composition skills:** none needed (devtools is the composition primitive itself) +- **Reference files:** vite-plugin (6 subsystems with distinct config surfaces) + +## Composition Opportunities + +| Library | Integration points | Composition skill needed? | +| --- | --- | --- | +| TanStack Query | Query devtools panel plugin | No — TanStack Query ships its own devtools panel | +| TanStack Router | Router devtools panel plugin | No — TanStack Router ships its own devtools panel | +| TanStack Form | Form devtools panel plugin | No — TanStack Form ships its own devtools panel | +| Vite | Build tooling integration | No — covered by vite-plugin skill | +| Any state/data library | EventClient instrumentation | Yes — strategic-instrumentation skill | diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml new file mode 100644 index 00000000..3912108a --- /dev/null +++ b/_artifacts/skill_tree.yaml @@ -0,0 +1,205 @@ +# _artifacts/skill_tree.yaml +library: + name: '@tanstack/devtools' + version: '0.10.12' + repository: 'https://github.com/TanStack/devtools' + description: > + Framework-agnostic toolkit for building custom devtools panels via a plugin + system with typed event communication. Supports React, Vue, Solid, Preact. +generated_from: + domain_map: '_artifacts/domain_map.yaml' + skill_spec: '_artifacts/skill_spec.md' +generated_at: '2026-03-11' + +skills: + # ── Core package: @tanstack/devtools ── + + - name: 'Devtools app setup' + slug: 'devtools-app-setup' + type: 'core' + domain: 'setup' + path: 'packages/devtools/skills/devtools-app-setup/SKILL.md' + package: 'packages/devtools' + description: > + Install TanStack Devtools, pick framework adapter (React/Vue/Solid/Preact), + register plugins via plugins prop, configure shell (position, hotkeys, + theme, hideUntilHover, requireUrlFlag, eventBusConfig). TanStackDevtools + component, defaultOpen, localStorage persistence. + sources: + - 'TanStack/devtools:docs/quick-start.md' + - 'TanStack/devtools:docs/installation.md' + - 'TanStack/devtools:docs/configuration.md' + - 'TanStack/devtools:docs/overview.md' + + - name: 'Devtools plugin panel development' + slug: 'devtools-plugin-panel' + type: 'core' + domain: 'plugin-development' + path: 'packages/devtools/skills/devtools-plugin-panel/SKILL.md' + package: 'packages/devtools' + description: > + Build devtools panel components that display emitted event data. Listen + via EventClient.on(), handle theme (light/dark), use devtools-ui + components (JSONTree, buttons, sections). Plugin registration (name, + render, id, defaultOpen), lifecycle (mount, activate, destroy), max 3 + active plugins. Two paths: Solid.js core with devtools-ui or + framework-specific panels directly. + requires: + - 'devtools-event-client' + sources: + - 'TanStack/devtools:docs/building-custom-plugins.md' + - 'TanStack/devtools:docs/plugin-lifecycle.md' + - 'TanStack/devtools:docs/plugin-configuration.md' + - 'TanStack/devtools:packages/devtools/src/context/devtools-context.tsx' + + - name: 'Devtools production setup' + slug: 'devtools-production' + type: 'lifecycle' + domain: 'build-production' + path: 'packages/devtools/skills/devtools-production/SKILL.md' + package: 'packages/devtools' + description: > + Handle devtools in production vs development. removeDevtoolsOnBuild, + /production sub-export, devDependency vs regular dependency, conditional + imports, NoOp plugin variants for tree-shaking, non-Vite production + exclusion patterns. + requires: + - 'devtools-app-setup' + sources: + - 'TanStack/devtools:docs/production.md' + - 'TanStack/devtools:packages/devtools-vite/src/remove-devtools.ts' + + - name: 'Devtools marketplace publishing' + slug: 'devtools-marketplace' + type: 'lifecycle' + domain: 'plugin-development' + path: 'packages/devtools/skills/devtools-marketplace/SKILL.md' + package: 'packages/devtools' + description: > + Publish plugin to npm and submit to TanStack Devtools Marketplace. + PluginMetadata registry format, plugin-registry.ts, pluginImport + (importName, type), requires (packageName, minVersion), framework + tagging, multi-framework submissions, featured plugins. + requires: + - 'devtools-plugin-panel' + sources: + - 'TanStack/devtools:docs/third-party-plugins.md' + - 'TanStack/devtools:packages/devtools/src/tabs/plugin-registry.ts' + + # ── Event client package: @tanstack/devtools-event-client ── + + - name: 'Devtools event client' + slug: 'devtools-event-client' + type: 'core' + domain: 'event-communication' + path: 'packages/event-bus-client/skills/devtools-event-client/SKILL.md' + package: 'packages/event-bus-client' + description: > + Create typed EventClient for a library. Define event maps with typed + payloads, pluginId auto-prepend namespacing, emit()/on()/onAll()/ + onAllPluginEvents() API. Connection lifecycle (5 retries, 300ms), + event queuing, enabled/disabled state, SSR fallbacks, singleton + pattern. Unique pluginId requirement to avoid event collisions. + sources: + - 'TanStack/devtools:docs/event-system.md' + - 'TanStack/devtools:docs/building-custom-plugins.md' + - 'TanStack/devtools:packages/event-bus-client/src/plugin.ts' + + - name: 'Devtools strategic instrumentation' + slug: 'devtools-instrumentation' + type: 'core' + domain: 'instrumentation' + path: 'packages/event-bus-client/skills/devtools-instrumentation/SKILL.md' + package: 'packages/event-bus-client' + description: > + Analyze library codebase for critical architecture and debugging points, + add strategic event emissions. Identify middleware boundaries, state + transitions, lifecycle hooks. Consolidate events (1 not 15), debounce + high-frequency updates, DRY shared payload fields, guard emit() for + production. Transparent server/client event bridging. + requires: + - 'devtools-event-client' + sources: + - 'TanStack/devtools:docs/building-custom-plugins.md' + - 'TanStack/devtools:docs/bidirectional-communication.md' + + - name: 'Devtools bidirectional communication' + slug: 'devtools-bidirectional' + type: 'core' + domain: 'event-communication' + path: 'packages/event-bus-client/skills/devtools-bidirectional/SKILL.md' + package: 'packages/event-bus-client' + description: > + Two-way event patterns between devtools panel and application. + App-to-devtools observation, devtools-to-app commands, time-travel + debugging with snapshots and revert. structuredClone for snapshot + safety, distinct event suffixes for observation vs commands, + serializable payloads only. + requires: + - 'devtools-event-client' + sources: + - 'TanStack/devtools:docs/bidirectional-communication.md' + - 'TanStack/devtools:packages/event-bus-client/src/plugin.ts' + + # ── Vite plugin package: @tanstack/devtools-vite ── + + - name: 'Devtools Vite plugin' + slug: 'devtools-vite-plugin' + type: 'core' + domain: 'build-production' + path: 'packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md' + package: 'packages/devtools-vite' + description: > + Configure @tanstack/devtools-vite for source inspection (data-tsd-source, + inspectHotkey, ignore patterns), console piping (client-to-server, + server-to-client, levels), enhanced logging, server event bus (port, + host, HTTPS), production stripping (removeDevtoolsOnBuild), editor + integration (launch-editor, custom editor.open). Must be FIRST plugin + in Vite config. Vite ^6 || ^7 only. + sources: + - 'TanStack/devtools:docs/vite-plugin.md' + - 'TanStack/devtools:docs/source-inspector.md' + - 'TanStack/devtools:packages/devtools-vite/src/plugin.ts' + subsystems: + - 'Source injection' + - 'Console piping' + - 'Enhanced logging' + - 'Production stripping' + - 'Server event bus' + - 'Editor integration' + references: + - 'references/vite-options.md' + + # ── Utils package: @tanstack/devtools-utils ── + + - name: 'Devtools framework adapters' + slug: 'devtools-framework-adapters' + type: 'framework' + domain: 'framework-adaptation' + path: 'packages/devtools-utils/skills/devtools-framework-adapters/SKILL.md' + package: 'packages/devtools-utils' + description: > + Use devtools-utils factory functions to create per-framework plugin + adapters. createReactPlugin/createSolidPlugin/createVuePlugin/ + createPreactPlugin, createReactPanel/createSolidPanel/createVuePanel/ + createPreactPanel. [Plugin, NoOpPlugin] tuple for tree-shaking. + DevtoolsPanelProps (theme). Vue uses (name, component) not options + object. Solid render must be function. Class-based panel factories. + requires: + - 'devtools-plugin-panel' + sources: + - 'TanStack/devtools:docs/devtools-utils.md' + - 'TanStack/devtools:packages/devtools-utils/src/react/plugin.tsx' + - 'TanStack/devtools:packages/devtools-utils/src/vue/plugin.ts' + - 'TanStack/devtools:packages/devtools-utils/src/solid/plugin.tsx' + - 'TanStack/devtools:packages/devtools-utils/src/preact/plugin.tsx' + subsystems: + - 'React' + - 'Vue' + - 'Solid' + - 'Preact' + references: + - 'references/react.md' + - 'references/vue.md' + - 'references/solid.md' + - 'references/preact.md' diff --git a/bin/intent.js b/bin/intent.js new file mode 100644 index 00000000..2cf2efab --- /dev/null +++ b/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/package.json b/package.json index ab53cbf6..d3c5ff3d 100644 --- a/package.json +++ b/package.json @@ -1,91 +1,100 @@ { - "name": "root", - "private": true, - "repository": { - "type": "git", - "url": "git+https://github.com/TanStack/devtools.git" - }, - "packageManager": "pnpm@10.24.0", - "type": "module", - "scripts": { - "build": "nx affected --targets=build --exclude=examples/** && size-limit", - "build:all": "nx run-many --targets=build --exclude=examples/** && size-limit", - "build:core": "nx build @tanstack/devtools && size-limit", - "changeset": "changeset", - "changeset:publish": "changeset publish", - "changeset:version": "changeset version && pnpm install --no-frozen-lockfile && pnpm format", - "clean": "find . -name 'dist' -type d -prune -exec rm -rf {} +", - "clean:node_modules": "find . -name 'node_modules' -type d -prune -exec rm -rf {} +", - "clean:all": "pnpm run clean && pnpm run clean:node_modules", - "dev": "pnpm run watch", - "format": "prettier --experimental-cli --ignore-unknown '**/*' --write", - "generate-docs": "node scripts/generate-docs.ts", - "lint:fix": "nx affected --target=lint:fix --exclude=examples/**", - "lint:fix:all": "pnpm run format && nx run-many --targets=lint --fix", - "preinstall": "node -e \"if(process.env.CI == 'true') {console.log('Skipping preinstall...'); process.exit(1)}\" || npx -y only-allow pnpm", - "size": "size-limit", - "test": "pnpm run test:ci", - "test:build": "nx affected --target=test:build --exclude=examples/**", - "test:ci": "nx run-many --targets=test:eslint,test:sherif,test:knip,test:lib,test:types,test:build,build", - "test:docs": "node scripts/verify-links.ts", - "test:eslint": "nx affected --target=test:eslint --exclude=examples/**", - "test:knip": "knip", - "test:lib": "nx affected --targets=test:lib --exclude=examples/**", - "test:lib:dev": "pnpm test:lib && nx watch --all -- pnpm test:lib", - "test:pr": "nx affected --targets=test:eslint,test:sherif,test:knip,test:lib,test:types,test:build,build", - "test:sherif": "sherif", - "test:types": "nx affected --targets=test:types --exclude=examples/**", - "watch": "pnpm run build:all && nx watch --all -- pnpm run build:all" - }, - "nx": { - "includedScripts": [ - "test:docs", - "test:knip", - "test:sherif" - ] - }, - "size-limit": [ - { - "path": "packages/devtools/dist/index.js", - "limit": "60 KB" - }, - { - "path": "packages/event-bus-client/dist/esm/plugin.js", - "limit": "1.2 KB" - } - ], - "devDependencies": { - "@changesets/cli": "^2.29.7", - "@faker-js/faker": "^9.9.0", - "@size-limit/preset-small-lib": "^11.2.0", - "@svitejs/changesets-changelog-github-compact": "^1.2.0", - "@tanstack/eslint-config": "0.3.2", - "@tanstack/typedoc-config": "0.2.1", - "@tanstack/vite-config": "0.2.1", - "@testing-library/jest-dom": "^6.8.0", - "@types/node": "^22.15.2", - "eslint": "^9.36.0", - "eslint-plugin-unused-imports": "^4.2.0", - "jsdom": "^27.0.0", - "knip": "^5.64.0", - "markdown-link-extractor": "^4.0.2", - "nx": "22.1.3", - "premove": "^4.0.0", - "prettier": "^3.8.1", - "prettier-plugin-svelte": "^3.4.1", - "publint": "^0.3.13", - "sherif": "^1.7.0", - "size-limit": "^11.2.0", - "tinyglobby": "^0.2.15", - "typescript": "~5.9.2", - "vite": "^7.1.7", - "vitest": "^3.2.4" - }, - "overrides": { - "@tanstack/devtools": "workspace:*", - "@tanstack/react-devtools": "workspace:*", - "@tanstack/preact-devtools": "workspace:*", - "@tanstack/solid-devtools": "workspace:*", - "@tanstack/devtools-vite": "workspace:*" - } + "name": "root", + "private": true, + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/devtools.git" + }, + "packageManager": "pnpm@10.24.0", + "type": "module", + "scripts": { + "build": "nx affected --targets=build --exclude=examples/** && size-limit", + "build:all": "nx run-many --targets=build --exclude=examples/** && size-limit", + "build:core": "nx build @tanstack/devtools && size-limit", + "changeset": "changeset", + "changeset:publish": "changeset publish", + "changeset:version": "changeset version && pnpm install --no-frozen-lockfile && pnpm format", + "clean": "find . -name 'dist' -type d -prune -exec rm -rf {} +", + "clean:node_modules": "find . -name 'node_modules' -type d -prune -exec rm -rf {} +", + "clean:all": "pnpm run clean && pnpm run clean:node_modules", + "dev": "pnpm run watch", + "format": "prettier --experimental-cli --ignore-unknown '**/*' --write", + "generate-docs": "node scripts/generate-docs.ts", + "lint:fix": "nx affected --target=lint:fix --exclude=examples/**", + "lint:fix:all": "pnpm run format && nx run-many --targets=lint --fix", + "preinstall": "node -e \"if(process.env.CI == 'true') {console.log('Skipping preinstall...'); process.exit(1)}\" || npx -y only-allow pnpm", + "size": "size-limit", + "test": "pnpm run test:ci", + "test:build": "nx affected --target=test:build --exclude=examples/**", + "test:ci": "nx run-many --targets=test:eslint,test:sherif,test:knip,test:lib,test:types,test:build,build", + "test:docs": "node scripts/verify-links.ts", + "test:eslint": "nx affected --target=test:eslint --exclude=examples/**", + "test:knip": "knip", + "test:lib": "nx affected --targets=test:lib --exclude=examples/**", + "test:lib:dev": "pnpm test:lib && nx watch --all -- pnpm test:lib", + "test:pr": "nx affected --targets=test:eslint,test:sherif,test:knip,test:lib,test:types,test:build,build", + "test:sherif": "sherif", + "test:types": "nx affected --targets=test:types --exclude=examples/**", + "watch": "pnpm run build:all && nx watch --all -- pnpm run build:all" + }, + "nx": { + "includedScripts": [ + "test:docs", + "test:knip", + "test:sherif" + ] + }, + "size-limit": [ + { + "path": "packages/devtools/dist/index.js", + "limit": "60 KB" + }, + { + "path": "packages/event-bus-client/dist/esm/plugin.js", + "limit": "1.2 KB" + } + ], + "devDependencies": { + "@changesets/cli": "^2.29.7", + "@faker-js/faker": "^9.9.0", + "@size-limit/preset-small-lib": "^11.2.0", + "@svitejs/changesets-changelog-github-compact": "^1.2.0", + "@tanstack/eslint-config": "0.3.2", + "@tanstack/intent": "^0.0.14", + "@tanstack/typedoc-config": "0.2.1", + "@tanstack/vite-config": "0.2.1", + "@testing-library/jest-dom": "^6.8.0", + "@types/node": "^22.15.2", + "eslint": "^9.36.0", + "eslint-plugin-unused-imports": "^4.2.0", + "jsdom": "^27.0.0", + "knip": "^5.64.0", + "markdown-link-extractor": "^4.0.2", + "nx": "22.1.3", + "premove": "^4.0.0", + "prettier": "^3.8.1", + "prettier-plugin-svelte": "^3.4.1", + "publint": "^0.3.13", + "sherif": "^1.7.0", + "size-limit": "^11.2.0", + "tinyglobby": "^0.2.15", + "typescript": "~5.9.2", + "vite": "^7.1.7", + "vitest": "^3.2.4" + }, + "overrides": { + "@tanstack/devtools": "workspace:*", + "@tanstack/react-devtools": "workspace:*", + "@tanstack/preact-devtools": "workspace:*", + "@tanstack/solid-devtools": "workspace:*", + "@tanstack/devtools-vite": "workspace:*" + }, + "files": [ + "skills", + "bin", + "!skills/_artifacts" + ], + "bin": { + "intent": "./bin/intent.js" + } } diff --git a/packages/devtools-utils/skills/devtools-framework-adapters/SKILL.md b/packages/devtools-utils/skills/devtools-framework-adapters/SKILL.md new file mode 100644 index 00000000..3ff62570 --- /dev/null +++ b/packages/devtools-utils/skills/devtools-framework-adapters/SKILL.md @@ -0,0 +1,251 @@ +--- +name: devtools-framework-adapters +description: > + Use devtools-utils factory functions to create per-framework plugin adapters. + createReactPlugin/createSolidPlugin/createVuePlugin/createPreactPlugin, + createReactPanel/createSolidPanel/createVuePanel/createPreactPanel. + [Plugin, NoOpPlugin] tuple for tree-shaking. DevtoolsPanelProps (theme). + Vue uses (name, component) not options object. Solid render must be function. +type: framework +library: tanstack-devtools +library_version: "0.10.12" +requires: + - tanstack-devtools/plugin-panel +sources: + - "TanStack/devtools:docs/devtools-utils.md" + - "TanStack/devtools:packages/devtools-utils/src/react/plugin.tsx" + - "TanStack/devtools:packages/devtools-utils/src/vue/plugin.ts" + - "TanStack/devtools:packages/devtools-utils/src/solid/plugin.tsx" + - "TanStack/devtools:packages/devtools-utils/src/preact/plugin.tsx" +--- + +Use `@tanstack/devtools-utils` factory functions to create per-framework devtools plugin adapters. Each framework has a subpath export (`/react`, `/vue`, `/solid`, `/preact`) with two factories: + +1. **`createXPlugin`** -- wraps a component into a `[Plugin, NoOpPlugin]` tuple for tree-shaking. +2. **`createXPanel`** -- wraps a class-based devtools core (`mount`/`unmount`) into a `[Panel, NoOpPanel]` component tuple. + +## Key Source Files + +- `packages/devtools-utils/src/react/plugin.tsx` -- createReactPlugin +- `packages/devtools-utils/src/react/panel.tsx` -- createReactPanel, DevtoolsPanelProps +- `packages/devtools-utils/src/vue/plugin.ts` -- createVuePlugin (different API) +- `packages/devtools-utils/src/vue/panel.ts` -- createVuePanel, DevtoolsPanelProps (includes 'system' theme) +- `packages/devtools-utils/src/solid/plugin.tsx` -- createSolidPlugin +- `packages/devtools-utils/src/solid/panel.tsx` -- createSolidPanel +- `packages/devtools-utils/src/solid/class.ts` -- constructCoreClass (Solid-specific) +- `packages/devtools-utils/src/preact/plugin.tsx` -- createPreactPlugin +- `packages/devtools-utils/src/preact/panel.tsx` -- createPreactPanel + +## Shared Pattern + +All four frameworks follow the same two-factory pattern: + +### Plugin Factory + +Every `createXPlugin` returns `readonly [Plugin, NoOpPlugin]`: +- **Plugin** -- returns a plugin object with metadata and a `render` function that renders your component. +- **NoOpPlugin** -- returns a plugin object with the same metadata but renders an empty fragment. + +### Panel Factory + +Every `createXPanel` returns `readonly [Panel, NoOpPanel]`: +- **Panel** -- a framework component that creates a `
`, instantiates the core class, calls `core.mount(el, theme)` on mount, and `core.unmount()` on cleanup. +- **NoOpPanel** -- renders an empty fragment. + +### DevtoolsPanelProps + +```ts +// React, Solid, Preact +interface DevtoolsPanelProps { + theme?: 'light' | 'dark' +} + +// Vue (note: includes 'system') +interface DevtoolsPanelProps { + theme?: 'dark' | 'light' | 'system' +} +``` + +Import from the framework subpath: +```ts +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/react' +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/vue' +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid' +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/preact' +``` + +## Primary Example (React) + +```tsx +import { createReactPlugin } from '@tanstack/devtools-utils/react' + +function MyStorePanel({ theme }: { theme?: 'light' | 'dark' }) { + return
My Store Devtools
+} + +const [MyPlugin, NoOpPlugin] = createReactPlugin({ + name: 'My Store', + id: 'my-store', + defaultOpen: false, + Component: MyStorePanel, +}) + +// Tree-shaking: use NoOp in production +const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +``` + +### With Class-Based Panel + +```tsx +import { createReactPanel, createReactPlugin } from '@tanstack/devtools-utils/react' + +class MyDevtoolsCore { + mount(el: HTMLElement, theme: 'light' | 'dark') { /* render into el */ } + unmount() { /* cleanup */ } +} + +const [MyPanel, NoOpPanel] = createReactPanel(MyDevtoolsCore) + +const [MyPlugin, NoOpPlugin] = createReactPlugin({ + name: 'My Store', + Component: MyPanel, +}) +``` + +## Framework API Differences + +### React & Preact -- Options Object + +```ts +createReactPlugin({ name, id?, defaultOpen?, Component }) // => [Plugin, NoOpPlugin] +createPreactPlugin({ name, id?, defaultOpen?, Component }) // => [Plugin, NoOpPlugin] +``` + +- `Component` receives `DevtoolsPanelProps` (with `theme`). +- `Plugin()` returns `{ name, id?, defaultOpen?, render(el, theme) }`. +- Preact is identical to React but uses Preact JSX types and `preact/hooks`. + +### Vue -- Positional Arguments, NOT Options Object + +```ts +createVuePlugin(name: string, component: DefineComponent) // => [Plugin, NoOpPlugin] +``` + +- Takes `(name, component)` as separate arguments, NOT an options object. +- `Plugin(props)` returns `{ name, component, props }` -- it passes props through. +- `NoOpPlugin(props)` returns `{ name, component: Fragment, props }`. +- Vue's `DevtoolsPanelProps.theme` also accepts `'system'`. + +### Solid -- Same API as React, Different Internals + +```ts +createSolidPlugin({ name, id?, defaultOpen?, Component }) // => [Plugin, NoOpPlugin] +``` + +- Same options-object API as React. +- `Component` must be a Solid component function `(props: DevtoolsPanelProps) => JSX.Element`. +- The render function internally returns `` -- Solid handles reactivity. +- Solid also exports `constructCoreClass` from `@tanstack/devtools-utils/solid/class` for building lazy-loaded devtools cores. + +## Common Mistakes + +### CRITICAL: Using React JSX Pattern in Vue Adapter + +Vue uses positional `(name, component)` arguments, NOT an options object. + +```ts +// WRONG -- will fail at compile time or produce garbage at runtime +const [MyPlugin, NoOpPlugin] = createVuePlugin({ + name: 'My Plugin', + Component: MyPanel, +}) + +// CORRECT +const [MyPlugin, NoOpPlugin] = createVuePlugin('My Plugin', MyPanel) +``` + +Vue plugins also work differently at call time -- you pass props: +```ts +// WRONG -- calling Plugin() with no args (React pattern) +const plugin = MyPlugin() + +// CORRECT -- Vue Plugin takes props +const plugin = MyPlugin({ theme: 'dark' }) +``` + +### CRITICAL: Solid Render Prop Not Wrapped in Function + +When using Solid components, `Component` must be a function reference, not raw JSX. + +```tsx +// WRONG -- evaluates immediately, breaks Solid reactivity +createSolidPlugin({ + name: 'My Store', + Component: , // This is JSX.Element, not a component function +}) + +// CORRECT -- pass the component function itself +createSolidPlugin({ + name: 'My Store', + Component: (props) => , +}) + +// ALSO CORRECT -- pass the component reference directly +createSolidPlugin({ + name: 'My Store', + Component: MyPanel, +}) +``` + +### HIGH: Ignoring NoOp Variant for Production + +The factory returns `[Plugin, NoOpPlugin]`. Both must be destructured and used for proper tree-shaking. + +```tsx +// WRONG -- NoOp variant discarded, devtools code ships to production +const [MyPlugin] = createReactPlugin({ name: 'Store', Component: MyPanel }) + +// CORRECT -- conditionally use NoOp in production +const [MyPlugin, NoOpPlugin] = createReactPlugin({ + name: 'Store', + Component: MyPanel, +}) +const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +``` + +### MEDIUM: Not Passing Theme Prop to Panel Component + +`DevtoolsPanelProps` includes `theme`. The devtools shell passes it so panels can match light/dark mode. If your component ignores it, the panel will not adapt to theme changes. + +```tsx +// WRONG -- theme is ignored +const Component = () =>
My Panel
+ +// CORRECT -- use theme for styling +const Component = ({ theme }: DevtoolsPanelProps) => ( +
+ My Panel +
+) +``` + +## Design Tension + +The core architecture is framework-agnostic, but each framework has different idioms: +- React/Preact use an options object with `Component` as a JSX function component. +- Vue uses positional arguments with a `DefineComponent` and passes props through. +- Solid uses the same options API as React but with Solid's JSX and reactivity model. + +Agents trained on React patterns will get Vue wrong. Always check the import path to determine which factory API to use. + +## Cross-References + +- **devtools-plugin-panel** -- Build your panel component first, then wrap it with the appropriate framework adapter. +- **devtools-production** -- NoOp variants are the primary mechanism for stripping devtools from production bundles. + +## Reference Files + +- `references/react.md` -- Full React factory API and examples +- `references/vue.md` -- Full Vue factory API and examples (different from React) +- `references/solid.md` -- Full Solid factory API and examples +- `references/preact.md` -- Full Preact factory API and examples diff --git a/packages/devtools-utils/skills/devtools-framework-adapters/references/preact.md b/packages/devtools-utils/skills/devtools-framework-adapters/references/preact.md new file mode 100644 index 00000000..6063c377 --- /dev/null +++ b/packages/devtools-utils/skills/devtools-framework-adapters/references/preact.md @@ -0,0 +1,254 @@ +# Preact Framework Adapter Reference + +## Import + +```ts +import { createPreactPlugin, createPreactPanel } from '@tanstack/devtools-utils/preact' +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/preact' +``` + +## DevtoolsPanelProps + +```ts +interface DevtoolsPanelProps { + theme?: 'light' | 'dark' +} +``` + +## createPreactPlugin + +Creates a `[Plugin, NoOpPlugin]` tuple from a Preact component and plugin metadata. Identical API to `createReactPlugin`. + +### Signature + +```ts +function createPreactPlugin(options: { + name: string + id?: string + defaultOpen?: boolean + Component: (props: DevtoolsPanelProps) => JSX.Element +}): readonly [Plugin, NoOpPlugin] +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `name` | `string` | Yes | Display name shown in the devtools tab | +| `id` | `string` | No | Unique identifier for the plugin | +| `defaultOpen` | `boolean` | No | Whether the plugin panel starts open | +| `Component` | `(props: DevtoolsPanelProps) => JSX.Element` | Yes | Preact component to render in the panel | + +### Return Value + +A `readonly [Plugin, NoOpPlugin]` tuple: + +- **`Plugin()`** -- returns `{ name, id?, defaultOpen?, render(el, theme) }`. The `render` function renders ``. +- **`NoOpPlugin()`** -- returns the same shape but `render` returns `<>`. + +### Source + +```tsx +// packages/devtools-utils/src/preact/plugin.tsx +/** @jsxImportSource preact */ +import type { JSX } from 'preact' +import type { DevtoolsPanelProps } from './panel' + +export function createPreactPlugin({ + Component, + ...config +}: { + name: string + id?: string + defaultOpen?: boolean + Component: (props: DevtoolsPanelProps) => JSX.Element +}) { + function Plugin() { + return { + ...config, + render: (_el: HTMLElement, theme: 'light' | 'dark') => ( + + ), + } + } + function NoOpPlugin() { + return { + ...config, + render: (_el: HTMLElement, _theme: 'light' | 'dark') => <>, + } + } + return [Plugin, NoOpPlugin] as const +} +``` + +### Usage + +#### Basic + +```tsx +import { createPreactPlugin } from '@tanstack/devtools-utils/preact' + +function MyStorePanel({ theme }: { theme?: 'light' | 'dark' }) { + return ( +
+

My Store Devtools

+
+ ) +} + +const [MyPlugin, NoOpPlugin] = createPreactPlugin({ + name: 'My Store', + id: 'my-store', + defaultOpen: false, + Component: MyStorePanel, +}) +``` + +#### Inline Component + +```tsx +const [MyPlugin, NoOpPlugin] = createPreactPlugin({ + name: 'My Store', + Component: ({ theme }) => , +}) +``` + +#### Production Tree-Shaking + +```tsx +const [MyPlugin, NoOpPlugin] = createPreactPlugin({ + name: 'My Store', + Component: MyStorePanel, +}) + +const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +``` + +## createPreactPanel + +Wraps a class-based devtools core in a Preact component. Identical behavior to `createReactPanel` but uses `preact/hooks`. + +### Signature + +```ts +function createPreactPanel< + TComponentProps extends DevtoolsPanelProps | undefined, + TCoreDevtoolsClass extends { + mount: (el: HTMLElement, theme: 'light' | 'dark') => void + unmount: () => void + }, +>(CoreClass: new () => TCoreDevtoolsClass): readonly [Panel, NoOpPanel] +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `CoreClass` | `new () => { mount(el, theme): void; unmount(): void }` | Yes | Class constructor for the devtools core | + +### Return Value + +- **`Panel`** -- A Preact component that: + - Creates a `
` with a ref. + - Instantiates `CoreClass` on mount via `useEffect` (from `preact/hooks`). + - Calls `core.mount(el, props.theme ?? 'dark')`. + - Calls `core.unmount()` on cleanup. + - Re-runs the effect when `theme` prop changes. +- **`NoOpPanel`** -- Renders `<>`. + +### Source + +```tsx +// packages/devtools-utils/src/preact/panel.tsx +/** @jsxImportSource preact */ +import { useEffect, useRef } from 'preact/hooks' + +export function createPreactPanel< + TComponentProps extends DevtoolsPanelProps | undefined, + TCoreDevtoolsClass extends { + mount: (el: HTMLElement, theme: 'light' | 'dark') => void + unmount: () => void + }, +>(CoreClass: new () => TCoreDevtoolsClass) { + function Panel(props: TComponentProps) { + const devToolRef = useRef(null) + const devtools = useRef(null) + useEffect(() => { + if (devtools.current) return + devtools.current = new CoreClass() + if (devToolRef.current) { + devtools.current.mount(devToolRef.current, props?.theme ?? 'dark') + } + return () => { + if (devToolRef.current) { + devtools.current?.unmount() + devtools.current = null + } + } + }, [props?.theme]) + return
+ } + + function NoOpPanel(_props: TComponentProps) { + return <> + } + return [Panel, NoOpPanel] as const +} +``` + +### Usage + +```tsx +import { createPreactPanel, createPreactPlugin } from '@tanstack/devtools-utils/preact' + +class MyDevtoolsCore { + mount(el: HTMLElement, theme: 'light' | 'dark') { + // Use DOM APIs to render your devtools UI into the provided element + const container = document.createElement('div') + container.className = theme + container.textContent = 'Devtools loaded' + el.appendChild(container) + } + unmount() { + // cleanup + } +} + +// Step 1: Create panel from class +const [MyPanel, NoOpPanel] = createPreactPanel(MyDevtoolsCore) + +// Step 2: Create plugin from panel +const [MyPlugin, NoOpPlugin] = createPreactPlugin({ + name: 'My Store', + Component: MyPanel, +}) + +// Step 3: Conditional for production +const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +``` + +## Preact-Specific Notes + +1. **Identical to React API.** `createPreactPlugin` and `createPreactPanel` have the exact same API signatures as their React counterparts. The only difference is the JSX runtime (`preact` vs `react`) and hooks import (`preact/hooks` vs `react`). + +2. **Use `class` not `className`.** Preact supports both, but idiomatic Preact uses `class` in JSX. + +3. **No Strict Mode double-mount by default.** Preact does not have React's Strict Mode double-invocation behavior, but the ref guard (`if (devtools.current) return`) is still present and harmless. + +4. **Default theme is `'dark'`.** If `props.theme` is undefined, the panel defaults to `'dark'`. + +5. **Same hooks behavior.** The `useEffect` dependency on `[props?.theme]` means the panel re-mounts when theme changes, same as React. + +## Comparison with React + +| Aspect | React | Preact | +|--------|-------|--------| +| Import path | `@tanstack/devtools-utils/react` | `@tanstack/devtools-utils/preact` | +| JSX types | `react` JSX | `preact` JSX | +| Hooks import | `react` | `preact/hooks` | +| API shape | Identical | Identical | +| `createXPlugin` signature | Same | Same | +| `createXPanel` signature | Same | Same | +| `DevtoolsPanelProps` | Same | Same | + +If you have working React adapter code, converting to Preact is a matter of changing the import paths. diff --git a/packages/devtools-utils/skills/devtools-framework-adapters/references/react.md b/packages/devtools-utils/skills/devtools-framework-adapters/references/react.md new file mode 100644 index 00000000..5255da97 --- /dev/null +++ b/packages/devtools-utils/skills/devtools-framework-adapters/references/react.md @@ -0,0 +1,233 @@ +# React Framework Adapter Reference + +## Import + +```ts +import { createReactPlugin, createReactPanel } from '@tanstack/devtools-utils/react' +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/react' +``` + +## DevtoolsPanelProps + +```ts +interface DevtoolsPanelProps { + theme?: 'light' | 'dark' +} +``` + +## 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] +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `name` | `string` | Yes | Display name shown in the devtools tab | +| `id` | `string` | No | Unique identifier for the plugin | +| `defaultOpen` | `boolean` | No | Whether the plugin panel starts open | +| `Component` | `(props: DevtoolsPanelProps) => JSX.Element` | Yes | React component to render in the panel | + +### Return Value + +A `readonly [Plugin, NoOpPlugin]` tuple: + +- **`Plugin()`** -- returns `{ name, id?, defaultOpen?, render(el: HTMLElement, theme: 'light' | 'dark') => JSX.Element }`. The `render` function renders ``. +- **`NoOpPlugin()`** -- returns the same shape but `render` returns `<>`. + +### Source + +```tsx +// packages/devtools-utils/src/react/plugin.tsx +export function createReactPlugin({ + Component, + ...config +}: { + name: string + id?: string + defaultOpen?: boolean + Component: (props: DevtoolsPanelProps) => JSX.Element +}) { + function Plugin() { + return { + ...config, + render: (_el: HTMLElement, theme: 'light' | 'dark') => ( + + ), + } + } + function NoOpPlugin() { + return { + ...config, + render: (_el: HTMLElement, _theme: 'light' | 'dark') => <>, + } + } + return [Plugin, NoOpPlugin] as const +} +``` + +### Usage + +#### Basic + +```tsx +import { createReactPlugin } from '@tanstack/devtools-utils/react' + +function MyStorePanel({ theme }: { theme?: 'light' | 'dark' }) { + return ( +
+

My Store Devtools

+
+ ) +} + +const [MyPlugin, NoOpPlugin] = createReactPlugin({ + name: 'My Store', + id: 'my-store', + defaultOpen: false, + Component: MyStorePanel, +}) +``` + +#### Inline Component + +```tsx +const [MyPlugin, NoOpPlugin] = createReactPlugin({ + name: 'My Store', + Component: ({ theme }) => , +}) +``` + +#### Production Tree-Shaking + +```tsx +const [MyPlugin, NoOpPlugin] = createReactPlugin({ + name: 'My Store', + Component: MyStorePanel, +}) + +const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +``` + +## createReactPanel + +Wraps a class-based devtools core (with `mount` and `unmount` methods) in a React component that handles lifecycle. + +### 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] +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `CoreClass` | `new () => { mount(el, theme): void; unmount(): void }` | Yes | Class constructor for the devtools core | + +### Return Value + +A `readonly [Panel, NoOpPanel]` tuple: + +- **`Panel`** -- A React component that: + - Creates a `
` with a ref. + - Instantiates `CoreClass` on mount via `useEffect`. + - Calls `core.mount(el, props.theme ?? 'dark')`. + - Calls `core.unmount()` on cleanup. + - Re-runs the effect when `theme` prop changes. +- **`NoOpPanel`** -- Renders `<>`. + +### Source + +```tsx +// packages/devtools-utils/src/react/panel.tsx +export function createReactPanel< + TComponentProps extends DevtoolsPanelProps | undefined, + TCoreDevtoolsClass extends { + mount: (el: HTMLElement, theme: 'light' | 'dark') => void + unmount: () => void + }, +>(CoreClass: new () => TCoreDevtoolsClass) { + function Panel(props: TComponentProps) { + const devToolRef = useRef(null) + const devtools = useRef(null) + useEffect(() => { + if (devtools.current) return + devtools.current = new CoreClass() + if (devToolRef.current) { + devtools.current.mount(devToolRef.current, props?.theme ?? 'dark') + } + return () => { + if (devToolRef.current) { + devtools.current?.unmount() + devtools.current = null + } + } + }, [props?.theme]) + return
+ } + + function NoOpPanel(_props: TComponentProps) { + return <> + } + return [Panel, NoOpPanel] as const +} +``` + +### Usage + +#### Composing Panel + Plugin + +```tsx +import { createReactPanel, createReactPlugin } from '@tanstack/devtools-utils/react' + +class MyDevtoolsCore { + mount(el: HTMLElement, theme: 'light' | 'dark') { + // Use DOM APIs to render your devtools UI into the provided element + const container = document.createElement('div') + container.className = theme + container.textContent = 'Devtools loaded' + el.appendChild(container) + } + unmount() { + // Clean up event listeners, subscriptions, etc. + } +} + +// Step 1: Create the panel component from the class +const [MyPanel, NoOpPanel] = createReactPanel(MyDevtoolsCore) + +// Step 2: Create the plugin from the panel component +const [MyPlugin, NoOpPlugin] = createReactPlugin({ + name: 'My Store', + Component: MyPanel, +}) + +// Step 3: Use conditionally for production +const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +``` + +## React-Specific Gotchas + +1. **`useEffect` dependency on `theme`**: The panel re-runs the mount effect when `theme` changes. This means the core class is unmounted and re-mounted on theme change. Design your core class to handle this gracefully. + +2. **Ref guard**: `createReactPanel` uses `if (devtools.current) return` to prevent double-mounting in React Strict Mode. Do not remove this guard. + +3. **Default theme is `'dark'`**: If `props.theme` is undefined, the panel defaults to `'dark'`. diff --git a/packages/devtools-utils/skills/devtools-framework-adapters/references/solid.md b/packages/devtools-utils/skills/devtools-framework-adapters/references/solid.md new file mode 100644 index 00000000..55a6de69 --- /dev/null +++ b/packages/devtools-utils/skills/devtools-framework-adapters/references/solid.md @@ -0,0 +1,268 @@ +# Solid Framework Adapter Reference + +## Import + +```ts +import { createSolidPlugin, createSolidPanel } from '@tanstack/devtools-utils/solid' +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid' + +// For class-based lazy loading (separate subpath) +import { constructCoreClass } from '@tanstack/devtools-utils/solid/class' +``` + +## DevtoolsPanelProps + +```ts +interface DevtoolsPanelProps { + theme?: 'light' | 'dark' +} +``` + +## createSolidPlugin + +Creates a `[Plugin, NoOpPlugin]` tuple from a Solid component and plugin metadata. Same options-object API as React. + +### Signature + +```ts +function createSolidPlugin(options: { + name: string + id?: string + defaultOpen?: boolean + Component: (props: DevtoolsPanelProps) => JSX.Element +}): readonly [Plugin, NoOpPlugin] +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `name` | `string` | Yes | Display name shown in the devtools tab | +| `id` | `string` | No | Unique identifier for the plugin | +| `defaultOpen` | `boolean` | No | Whether the plugin panel starts open | +| `Component` | `(props: DevtoolsPanelProps) => JSX.Element` | Yes | Solid component function | + +### Return Value + +A `readonly [Plugin, NoOpPlugin]` tuple: + +- **`Plugin()`** -- returns `{ name, id?, defaultOpen?, render(el, theme) }`. The `render` function returns ``. +- **`NoOpPlugin()`** -- returns the same shape but `render` returns `<>`. + +### Source + +```tsx +// packages/devtools-utils/src/solid/plugin.tsx +/** @jsxImportSource solid-js */ +import type { JSX } from 'solid-js' +import type { DevtoolsPanelProps } from './panel' + +export function createSolidPlugin({ + Component, + ...config +}: { + name: string + id?: string + defaultOpen?: boolean + Component: (props: DevtoolsPanelProps) => JSX.Element +}) { + function Plugin() { + return { + ...config, + render: (_el: HTMLElement, theme: 'light' | 'dark') => { + return + }, + } + } + function NoOpPlugin() { + return { + ...config, + render: (_el: HTMLElement, _theme: 'light' | 'dark') => <>, + } + } + return [Plugin, NoOpPlugin] as const +} +``` + +### Usage + +#### Basic + +```tsx +import { createSolidPlugin } from '@tanstack/devtools-utils/solid' + +function MyStorePanel(props: { theme?: 'light' | 'dark' }) { + return ( +
+

My Store Devtools

+
+ ) +} + +const [MyPlugin, NoOpPlugin] = createSolidPlugin({ + name: 'My Store', + id: 'my-store', + defaultOpen: false, + Component: MyStorePanel, +}) +``` + +#### Inline Component + +```tsx +const [MyPlugin, NoOpPlugin] = createSolidPlugin({ + name: 'My Store', + Component: (props) => , +}) +``` + +#### Production Tree-Shaking + +```tsx +const [MyPlugin, NoOpPlugin] = createSolidPlugin({ + name: 'My Store', + Component: MyStorePanel, +}) + +const ActivePlugin = import.meta.env.DEV ? MyPlugin : NoOpPlugin +``` + +## createSolidPanel + +Wraps a class-based devtools core in a Solid component. + +### Signature + +```ts +function createSolidPanel< + TComponentProps extends DevtoolsPanelProps | undefined, +>(CoreClass: ClassType): readonly [Panel, NoOpPanel] +``` + +Where `ClassType` is `ReturnType[0]` -- a class with `mount(el, theme)` and `unmount()`. + +### Return Value + +- **`Panel`** -- A Solid component that: + - Creates a `
` with a ref. + - Instantiates `CoreClass` immediately via `createSignal`. + - Calls `core.mount(el, props.theme ?? 'dark')` inside `onMount`. + - Calls `core.unmount()` via `onCleanup` (nested inside `onMount`). +- **`NoOpPanel`** -- Renders `<>`. + +### Source + +```tsx +// packages/devtools-utils/src/solid/panel.tsx +/** @jsxImportSource solid-js */ +import { createSignal, onCleanup, onMount } from 'solid-js' +import type { ClassType } from './class' + +export function createSolidPanel< + TComponentProps extends DevtoolsPanelProps | undefined, +>(CoreClass: ClassType) { + function Panel(props: TComponentProps) { + let devToolRef: HTMLDivElement | undefined + const [devtools] = createSignal(new CoreClass()) + onMount(() => { + if (devToolRef) { + devtools().mount(devToolRef, props?.theme ?? 'dark') + } + onCleanup(() => { + devtools().unmount() + }) + }) + return
+ } + + function NoOpPanel(_props: TComponentProps) { + return <> + } + + return [Panel, NoOpPanel] as const +} +``` + +### Usage + +```tsx +import { createSolidPanel, createSolidPlugin } from '@tanstack/devtools-utils/solid' +import { constructCoreClass } from '@tanstack/devtools-utils/solid/class' + +// Step 1: Build a core class with lazy loading +const [MyDevtoolsCore, NoOpCore] = constructCoreClass( + () => import('./MyDevtoolsUI') +) + +// Step 2: Create panel from core class +const [MyPanel, NoOpPanel] = createSolidPanel(MyDevtoolsCore) + +// Step 3: Create plugin from panel +const [MyPlugin, NoOpPlugin] = createSolidPlugin({ + name: 'My Store', + Component: MyPanel, +}) +``` + +## constructCoreClass + +Solid has an additional utility for building lazy-loaded devtools cores. Import from the separate subpath `@tanstack/devtools-utils/solid/class`. + +### Signature + +```ts +function constructCoreClass( + importFn: () => Promise<{ default: () => JSX.Element }>, +): readonly [DevtoolsCore, NoOpDevtoolsCore] +``` + +### Behavior + +- **`DevtoolsCore`** -- Has an async `mount(el, theme)` that dynamically imports the component, then mounts it into `el`. Tracks mounting state to prevent double-mounting. Supports abort if `unmount()` is called during the async import. +- **`NoOpDevtoolsCore`** -- Extends `DevtoolsCore` but `mount` and `unmount` are no-ops. + +### Usage + +```ts +import { constructCoreClass } from '@tanstack/devtools-utils/solid/class' + +const [DevtoolsCore, NoOpDevtoolsCore] = constructCoreClass( + () => import('./MyDevtoolsPanel') +) + +// Use DevtoolsCore with createSolidPanel +// Use NoOpDevtoolsCore for production +``` + +## Solid-Specific Gotchas + +1. **Component must be a function, not JSX.** The `Component` field expects a function `(props) => JSX.Element`, not evaluated JSX. + + ```tsx + // WRONG -- is JSX.Element, not a component function + Component: + + // CORRECT -- pass the component function + Component: MyStorePanel + + // ALSO CORRECT -- wrap in arrow function + Component: (props) => + ``` + +2. **Solid props are accessed via `props.theme`, not destructured.** Solid's reactivity requires accessing props through the props object. Destructuring breaks reactivity. + + ```tsx + // CAUTION -- destructuring may break reactivity tracking + const Component = ({ theme }: DevtoolsPanelProps) =>
{theme}
+ + // PREFERRED -- access via props object + const Component = (props: DevtoolsPanelProps) =>
{props.theme}
+ ``` + +3. **Default theme is `'dark'`.** If `props.theme` is undefined, the panel defaults to `'dark'`. + +4. **`onCleanup` is nested inside `onMount`.** In `createSolidPanel`, cleanup is registered inside `onMount`, which is the Solid idiom for pairing mount/unmount lifecycle. + +5. **Core class instantiation is eager.** `createSignal(new CoreClass())` runs immediately when the Panel component is created, not lazily. The actual `mount` call happens in `onMount`. + +6. **`constructCoreClass` handles async import abort.** If `unmount()` is called while the dynamic import is still in flight, the mount is aborted cleanly. No need to handle this manually. diff --git a/packages/devtools-utils/skills/devtools-framework-adapters/references/vue.md b/packages/devtools-utils/skills/devtools-framework-adapters/references/vue.md new file mode 100644 index 00000000..a3c3760a --- /dev/null +++ b/packages/devtools-utils/skills/devtools-framework-adapters/references/vue.md @@ -0,0 +1,256 @@ +# Vue Framework Adapter Reference + +## Import + +```ts +import { createVuePlugin, createVuePanel } from '@tanstack/devtools-utils/vue' +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/vue' +``` + +## DevtoolsPanelProps + +```ts +// NOTE: Vue includes 'system' -- unlike React/Solid/Preact +interface DevtoolsPanelProps { + theme?: 'dark' | 'light' | 'system' +} +``` + +## createVuePlugin + +Creates a `[Plugin, NoOpPlugin]` tuple from a Vue component. + +**CRITICAL: Vue uses positional arguments `(name, component)`, NOT an options object.** + +### Signature + +```ts +function createVuePlugin>( + name: string, + component: DefineComponent, +): readonly [Plugin, NoOpPlugin] +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `name` | `string` | Yes | Display name shown in the devtools tab | +| `component` | `DefineComponent` | Yes | Vue component to render in the panel | + +Note: There is **no** `id` or `defaultOpen` parameter. Vue plugins are simpler. + +### Return Value + +A `readonly [Plugin, NoOpPlugin]` tuple. Both are **functions that accept props**: + +- **`Plugin(props: TComponentProps)`** -- returns `{ name, component, props }` where `component` is your Vue component. +- **`NoOpPlugin(props: TComponentProps)`** -- returns `{ name, component: Fragment, props }` where `Fragment` is Vue's built-in fragment (renders nothing visible). + +**This differs from React/Solid/Preact** where `Plugin()` takes no arguments. + +### Source + +```ts +// packages/devtools-utils/src/vue/plugin.ts +import { Fragment } from 'vue' +import type { DefineComponent } from 'vue' + +export function createVuePlugin>( + name: string, + component: DefineComponent, +) { + function Plugin(props: TComponentProps) { + return { + name, + component, + props, + } + } + function NoOpPlugin(props: TComponentProps) { + return { + name, + component: Fragment, + props, + } + } + return [Plugin, NoOpPlugin] as const +} +``` + +### Usage + +#### Basic + +```vue + + + + +``` + +```ts +import { createVuePlugin } from '@tanstack/devtools-utils/vue' +import MyStorePanel from './MyStorePanel.vue' + +const [MyPlugin, NoOpPlugin] = createVuePlugin('My Store', MyStorePanel) +``` + +#### Using the Plugin (Vue-specific -- pass props) + +```ts +// Vue plugins are called WITH props +const plugin = MyPlugin({ theme: 'dark' }) +// Returns: { name: 'My Store', component: MyStorePanel, props: { theme: 'dark' } } + +const noopPlugin = NoOpPlugin({ theme: 'dark' }) +// Returns: { name: 'My Store', component: Fragment, props: { theme: 'dark' } } +``` + +#### Production Tree-Shaking + +```ts +const [MyPlugin, NoOpPlugin] = createVuePlugin('My Store', MyStorePanel) + +const ActivePlugin = import.meta.env.DEV ? MyPlugin : NoOpPlugin +``` + +## createVuePanel + +Wraps a class-based devtools core in a Vue `defineComponent`. + +### Signature + +```ts +function createVuePanel< + TComponentProps extends DevtoolsPanelProps, + TCoreDevtoolsClass extends { + mount: (el: HTMLElement, theme?: DevtoolsPanelProps['theme']) => void + unmount: () => void + }, +>(CoreClass: new (props: TComponentProps) => TCoreDevtoolsClass): readonly [Panel, NoOpPanel] +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `CoreClass` | `new (props: TComponentProps) => { mount(el, theme?): void; unmount(): void }` | Yes | Class constructor. **Note:** Vue's constructor takes `props`, unlike React's no-arg constructor. | + +### Return Value + +A tuple of two Vue `DefineComponent`s: + +- **`Panel`** -- Accepts `theme` and `devtoolsProps` as props. On `onMounted`, instantiates `CoreClass(devtoolsProps)` and calls `mount(el, theme)`. On `onUnmounted`, calls `unmount()`. Renders a `
`. +- **`NoOpPanel`** -- Renders `null`. + +Both components have the type: +```ts +DefineComponent<{ + theme?: 'dark' | 'light' | 'system' + devtoolsProps: TComponentProps +}> +``` + +### Source + +```ts +// packages/devtools-utils/src/vue/panel.ts +export function createVuePanel< + TComponentProps extends DevtoolsPanelProps, + TCoreDevtoolsClass extends { + mount: (el: HTMLElement, theme?: DevtoolsPanelProps['theme']) => void + unmount: () => void + }, +>(CoreClass: new (props: TComponentProps) => TCoreDevtoolsClass) { + const props = { + theme: { type: String as () => DevtoolsPanelProps['theme'] }, + devtoolsProps: { type: Object as () => TComponentProps }, + } + + const Panel = defineComponent({ + props, + setup(config) { + const devToolRef = ref(null) + const devtools = ref(null) + onMounted(() => { + const instance = new CoreClass(config.devtoolsProps as TComponentProps) + devtools.value = instance + if (devToolRef.value) { + instance.mount(devToolRef.value, config.theme) + } + }) + onUnmounted(() => { + if (devToolRef.value && devtools.value) { + devtools.value.unmount() + } + }) + return () => h('div', { style: { height: '100%' }, ref: devToolRef }) + }, + }) + + const NoOpPanel = defineComponent({ + props, + setup() { + return () => null + }, + }) + + return [Panel, NoOpPanel] as unknown as [ + DefineComponent<{ theme?: DevtoolsPanelProps['theme']; devtoolsProps: TComponentProps }>, + DefineComponent<{ theme?: DevtoolsPanelProps['theme']; devtoolsProps: TComponentProps }>, + ] +} +``` + +### Usage + +```ts +import { createVuePanel, createVuePlugin } from '@tanstack/devtools-utils/vue' + +class MyDevtoolsCore { + constructor(private props: { theme?: string }) {} + mount(el: HTMLElement, theme?: 'dark' | 'light' | 'system') { + // render into el + } + unmount() { + // cleanup + } +} + +// Step 1: Create panel from class +const [MyPanel, NoOpPanel] = createVuePanel(MyDevtoolsCore) + +// Step 2: Create plugin from panel +const [MyPlugin, NoOpPlugin] = createVuePlugin('My Store', MyPanel) +``` + +#### Using the Panel Directly in a Template + +```vue + +``` + +## Vue-Specific Gotchas + +1. **Positional arguments, not options object.** This is the most common mistake. `createVuePlugin('name', Component)`, not `createVuePlugin({ name, Component })`. + +2. **Plugin functions accept props.** `MyPlugin(props)` returns `{ name, component, props }`. This differs from React/Solid/Preact where `Plugin()` takes no arguments. + +3. **Theme includes `'system'`.** Vue's `DevtoolsPanelProps` accepts `'dark' | 'light' | 'system'`, while all other frameworks only accept `'light' | 'dark'`. + +4. **No `id` or `defaultOpen`.** The Vue `createVuePlugin` API only takes `name` and `component`. There is no `id` or `defaultOpen` parameter. + +5. **Panel constructor takes props.** `createVuePanel`'s `CoreClass` constructor receives props: `new CoreClass(devtoolsProps)`. React/Preact/Solid constructors take no arguments. + +6. **`devtoolsProps` prop on panel.** The Vue panel component has a separate `devtoolsProps` prop for forwarding data to the core class, in addition to the `theme` prop. diff --git a/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md b/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md new file mode 100644 index 00000000..c4a4baef --- /dev/null +++ b/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md @@ -0,0 +1,306 @@ +--- +name: devtools-vite-plugin +description: > + Configure @tanstack/devtools-vite for source inspection (data-tsd-source, + inspectHotkey, ignore patterns), console piping (client-to-server, + server-to-client, levels), enhanced logging, server event bus (port, host, + HTTPS), production stripping (removeDevtoolsOnBuild), editor integration + (launch-editor, custom editor.open). Must be FIRST plugin in Vite config. + Vite ^6 || ^7 only. +type: core +library: tanstack-devtools +library_version: "0.10.12" +sources: + - "TanStack/devtools:docs/vite-plugin.md" + - "TanStack/devtools:docs/source-inspector.md" + - "TanStack/devtools:packages/devtools-vite/src/plugin.ts" +--- + +Configure @tanstack/devtools-vite -- the Vite plugin that enhances TanStack Devtools with source inspection, console piping, enhanced logging, a server event bus, production stripping, editor integration, and a plugin marketplace. The plugin returns an array of sub-plugins, all using `enforce: 'pre'`, so it must be the FIRST plugin in the Vite config. + +## Installation and Basic Setup + +```ts +// vite.config.ts +import { devtools } from '@tanstack/devtools-vite' + +export default { + plugins: [ + devtools(), + // ... other plugins AFTER devtools + ], +} +``` + +Install as a dev dependency: +```sh +pnpm add -D @tanstack/devtools-vite +``` + +There is also a `defineDevtoolsConfig` helper for type-safe config objects: +```ts +import { devtools, defineDevtoolsConfig } from '@tanstack/devtools-vite' + +const config = defineDevtoolsConfig({ + // fully typed options +}) + +export default { + plugins: [devtools(config)], +} +``` + +## Exports + +From `packages/devtools-vite/src/index.ts`: +- `devtools` -- main plugin factory, returns `Array` +- `defineDevtoolsConfig` -- identity function for type-safe config +- `TanStackDevtoolsViteConfig` -- config type (re-exported) +- `ConsoleLevel` -- `'log' | 'warn' | 'error' | 'info' | 'debug'` + +## Architecture: Sub-Plugins + +`devtools()` returns an array of Vite plugins. Each has `enforce: 'pre'` and only activates when its conditions are met (dev mode, serve command, etc.). + +| Sub-plugin name | What it does | When active | +|---|---|---| +| `@tanstack/devtools:inject-source` | Babel transform adding `data-tsd-source` attrs to JSX | dev mode + `injectSource.enabled` | +| `@tanstack/devtools:config` | Reserved for future config modifications | serve command only | +| `@tanstack/devtools:custom-server` | Starts ServerEventBus, registers middleware for open-source/console-pipe endpoints | dev mode | +| `@tanstack/devtools:remove-devtools-on-build` | Strips devtools imports/JSX from production bundles | build command or production mode + `removeDevtoolsOnBuild` | +| `@tanstack/devtools:event-client-setup` | Marketplace: listens for install/add-plugin events via devtoolsEventClient | dev mode + serve + not CI | +| `@tanstack/devtools:console-pipe-transform` | Injects runtime console-pipe code into entry files | dev mode + serve + `consolePiping.enabled` | +| `@tanstack/devtools:better-console-logs` | Babel transform prepending source location to `console.log`/`console.error` | dev mode + `enhancedLogs.enabled` | +| `@tanstack/devtools:inject-plugin` | Detects which file imports TanStackDevtools (for marketplace injection) | dev mode + serve | +| `@tanstack/devtools:connection-injection` | Replaces `__TANSTACK_DEVTOOLS_PORT__`, `__TANSTACK_DEVTOOLS_HOST__`, `__TANSTACK_DEVTOOLS_PROTOCOL__` placeholders | dev mode + serve | + +## Subsystem Details + +### Source Injection + +Adds `data-tsd-source="::"` attributes to every JSX opening element via Babel. This powers the "Go to Source" feature -- hold the inspect hotkey (default: Shift+Alt+Ctrl/Meta), hover over elements, click to open in editor. + +**Key behaviors:** +- Skips `` and `` +- Skips elements where the component's props parameter is spread (`{...props}`) -- this is because injecting the attribute would be overwritten by the spread +- Skips files matching `injectSource.ignore.files` patterns +- Skips components matching `injectSource.ignore.components` patterns +- Patterns can be strings (matched via picomatch) or RegExp +- Transform filter excludes `node_modules`, `?raw` imports, `/dist/`, `/build/` + +**Source files:** `packages/devtools-vite/src/inject-source.ts`, `packages/devtools-vite/src/matcher.ts` + +```ts +devtools({ + injectSource: { + enabled: true, + ignore: { + files: ['node_modules', /.*\.test\.(js|ts|jsx|tsx)$/], + components: ['InternalComponent', /.*Provider$/], + }, + }, +}) +``` + +### Console Piping + +Bidirectional console piping between client and server. Injects runtime code (IIFE) into entry files that: + +**Client side:** +1. Wraps `console[level]` to batch and POST entries to `/__tsd/console-pipe` +2. Opens an EventSource on `/__tsd/console-pipe/sse` to receive server logs +3. Server logs appear in browser console with a purple `[Server]` prefix +4. Client logs appear in terminal with a cyan `[Client]` prefix + +**Server side (SSR/Nitro):** +1. Wraps `console[level]` to batch and POST entries to `/__tsd/console-pipe/server` +2. These are then broadcast to all SSE clients + +**Entry file detection:** looks for `::` in chalk colors. + +The transform inserts a spread of a conditional expression: `...(typeof window === 'undefined' ? serverLogMessage : browserLogMessage)` as the first argument of the console call. + +**Source file:** `packages/devtools-vite/src/enhance-logs.ts` + +```ts +devtools({ + enhancedLogs: { + enabled: true, // default + }, +}) +``` + +### Production Stripping + +Removes all devtools code from production builds. The transform: +1. Finds files importing from these packages: `@tanstack/react-devtools`, `@tanstack/preact-devtools`, `@tanstack/solid-devtools`, `@tanstack/vue-devtools`, `@tanstack/devtools` +2. Removes the import declarations +3. Removes the JSX elements that use the imported components +4. Cleans up leftover imports that were only used inside the removed JSX (e.g., plugin panel components) + +Active when: `command !== 'serve'` OR `config.mode === 'production'` (handles hosting providers like Cloudflare/Netlify that may not use `build` command but set mode to production). + +**Source file:** `packages/devtools-vite/src/remove-devtools.ts` + +```ts +devtools({ + removeDevtoolsOnBuild: true, // default +}) +``` + +### Server Event Bus + +A WebSocket + SSE server for devtools-to-client communication. Managed by `@tanstack/devtools-event-bus/server`. + +**Key behaviors:** +- Default port: 4206 +- On EADDRINUSE: falls back to OS-assigned port (port 0) +- When Vite uses HTTPS: piggybacks on Vite's httpServer instead of creating a standalone one (shares TLS certificate) +- Uses global variables (`__TANSTACK_DEVTOOLS_SERVER__`, etc.) to survive HMR without restarting +- The actual port is injected into client code via `__TANSTACK_DEVTOOLS_PORT__` placeholder replacement + +**Source file:** `packages/event-bus/src/server/server.ts` + +```ts +devtools({ + eventBusConfig: { + port: 4206, // default + enabled: true, // default; set false for storybook/vitest + debug: false, // default; logs internal bus activity + }, +}) +``` + +### Editor Integration + +Uses `launch-editor` to open source files in the editor. Default editor is VS Code. The `editor.open` callback receives `(path, lineNumber, columnNumber)` as strings. + +The open-source flow: browser requests `/__tsd/open-source?source=` --> Vite middleware parses source param --> calls `editor.open`. + +Supported editors via launch-editor: VS Code, WebStorm, Sublime Text, Atom, and more. For unsupported editors, provide a custom `editor.open` function. + +**Source file:** `packages/devtools-vite/src/editor.ts` + +```ts +devtools({ + editor: { + name: 'Cursor', + open: async (path, lineNumber, columnNumber) => { + // Custom editor open logic + // path is the absolute file path + // lineNumber and columnNumber are strings or undefined + }, + }, +}) +``` + +### Plugin Marketplace + +When the dev server is running, listens for events via `devtoolsEventClient`: +- `install-devtools` -- runs package manager install, then auto-injects plugin into devtools setup file +- `add-plugin-to-devtools` -- injects plugin import and JSX/function call into the file containing `` +- `bump-package-version` -- updates a package to a minimum version +- `mounted` -- sends package.json and outdated deps to the UI + +Auto-detection of the devtools setup file: the `inject-plugin` sub-plugin scans transforms for files importing from `@tanstack/react-devtools`, `@tanstack/solid-devtools`, `@tanstack/vue-devtools`, etc., and stores the file ID. + +**Source files:** `packages/devtools-vite/src/inject-plugin.ts`, `packages/devtools-vite/src/package-manager.ts` + +## Common Mistakes + +### 1. Not placing devtools() first in Vite plugins (HIGH) + +All sub-plugins use `enforce: 'pre'`. They must transform code before framework plugins (React, Vue, Solid, etc.) process it. If devtools is not first, source injection and enhanced logs may silently fail because framework transforms remove the raw JSX before devtools can annotate it. + +```ts +// WRONG +export default { + plugins: [ + react(), + devtools(), // too late -- react() already transformed JSX + ], +} + +// CORRECT +export default { + plugins: [ + devtools(), + react(), + ], +} +``` + +### 2. Using devtools-vite with non-Vite bundlers (HIGH) + +`@tanstack/devtools-vite` has a peer dependency on `vite ^6.0.0 || ^7.0.0`. It uses Vite-specific APIs (`configureServer`, `handleHotUpdate`, `transform` with filter objects, `Plugin` type). It will not work with webpack, rspack, esbuild, or other bundlers. For non-Vite setups, use `@tanstack/devtools-event-bus` client directly without the Vite plugin. + +### 3. Expecting Vite plugin features in production (MEDIUM) + +Source injection, console piping, enhanced logging, the server event bus, and the marketplace only operate during development (`config.mode === 'development'` and `command === 'serve'`). In production builds, the only active sub-plugin is `remove-devtools-on-build` (which strips devtools code). Do not rely on any of these features being available at runtime in production. + +### 4. Source injection on spread-props elements (MEDIUM) + +The Babel transform in `inject-source.ts` explicitly skips any JSX element that has a `{...props}` spread where `props` is the component's parameter name. This is intentional -- the spread would overwrite the injected `data-tsd-source` attribute. If source inspection doesn't work for a specific component, check if it spreads its props parameter. + +```tsx +// data-tsd-source will NOT be injected on
here +const MyComponent = (props) => { + return
content
+} +``` + +### 5. Event bus port conflict in multi-project setups (MEDIUM) + +The default event bus port is 4206. When running multiple Vite dev servers concurrently (monorepo), the second server will hit EADDRINUSE. The event bus handles this by falling back to an OS-assigned port (port 0), and the actual port is injected via placeholder replacement. However, if you need predictable ports (e.g., for firewall rules), set different ports explicitly: + +```ts +// Project A +devtools({ eventBusConfig: { port: 4206 } }) + +// Project B +devtools({ eventBusConfig: { port: 4207 } }) +``` + +## Internal Middleware Endpoints + +These are registered on the Vite dev server (not the event bus server): + +| Endpoint | Method | Purpose | +|---|---|---| +| `/__tsd/open-source?source=` | GET | Opens file in editor, returns HTML that closes the window | +| `/__tsd/console-pipe` | POST | Receives client console entries (batched JSON) | +| `/__tsd/console-pipe/server` | POST | Receives server-side console entries | +| `/__tsd/console-pipe/sse` | GET | SSE stream for broadcasting server logs to browser | + +## Cross-References + +- **devtools-app-setup** -- How to set up `` in your app (must be done before the Vite plugin provides value) +- **devtools-production** -- Details on production stripping configuration and keeping devtools in production builds + +## Key Source Files + +- `packages/devtools-vite/src/plugin.ts` -- Main plugin factory with all sub-plugins and config type +- `packages/devtools-vite/src/inject-source.ts` -- Babel transform for data-tsd-source injection +- `packages/devtools-vite/src/enhance-logs.ts` -- Babel transform for enhanced console logs +- `packages/devtools-vite/src/remove-devtools.ts` -- Production stripping transform +- `packages/devtools-vite/src/virtual-console.ts` -- Console pipe runtime code generator +- `packages/devtools-vite/src/editor.ts` -- Editor config type and launch-editor integration +- `packages/devtools-vite/src/inject-plugin.ts` -- Marketplace plugin injection into devtools setup file +- `packages/devtools-vite/src/utils.ts` -- Middleware request handling and helpers +- `packages/devtools-vite/src/matcher.ts` -- Picomatch/RegExp pattern matcher +- `packages/event-bus/src/server/server.ts` -- ServerEventBus implementation (WebSocket + SSE + EADDRINUSE fallback) diff --git a/packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md b/packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md new file mode 100644 index 00000000..ae13905e --- /dev/null +++ b/packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md @@ -0,0 +1,404 @@ +# @tanstack/devtools-vite Options Reference + +Complete configuration reference for the `devtools()` Vite plugin. All options are optional -- calling `devtools()` with no arguments uses sensible defaults. + +**Source of truth:** `packages/devtools-vite/src/plugin.ts` (type `TanStackDevtoolsViteConfig`) + +## Top-Level Config Type + +```ts +import type { Plugin } from 'vite' + +type ConsoleLevel = 'log' | 'warn' | 'error' | 'info' | 'debug' + +type TanStackDevtoolsViteConfig = { + editor?: EditorConfig + eventBusConfig?: ServerEventBusConfig & { enabled?: boolean } + enhancedLogs?: { enabled: boolean } + removeDevtoolsOnBuild?: boolean + logging?: boolean + injectSource?: { + enabled: boolean + ignore?: { + files?: Array + components?: Array + } + } + consolePiping?: { + enabled?: boolean + levels?: Array + } +} + +// Returns Array (9 sub-plugins) +declare function devtools(args?: TanStackDevtoolsViteConfig): Array + +// Identity function for type-safe config objects +declare function defineDevtoolsConfig( + config: TanStackDevtoolsViteConfig, +): TanStackDevtoolsViteConfig +``` + +--- + +## `injectSource` + +Controls source injection -- the Babel transform that adds `data-tsd-source` attributes to JSX elements for the "Go to Source" feature. + +| Field | Type | Default | Description | +|---|---|---|---| +| `enabled` | `boolean` | `true` | Whether to inject `data-tsd-source` attributes into JSX elements during development. | +| `ignore` | `object` | `undefined` | Patterns to exclude from injection. | +| `ignore.files` | `Array` | `[]` | File paths to skip. Strings are matched via picomatch glob syntax. RegExp patterns are tested directly. Matched against the file's path relative to `process.cwd()`. | +| `ignore.components` | `Array` | `[]` | Component/element names to skip. Strings are matched via picomatch. RegExp patterns are tested directly. Matched against the JSX element name (e.g., `"div"`, `"MyComponent"`, `"Namespace.Component"`). | + +**Built-in exclusions (hardcoded in transform filter, not configurable):** +- `node_modules` +- `?raw` imports +- `/dist/` paths +- `/build/` paths +- `` and `` elements +- Elements with `{...propsParam}` spread (where `propsParam` is the function's parameter name) + +**Example:** + +```ts +devtools({ + injectSource: { + enabled: true, + ignore: { + files: [ + 'node_modules', + /.*\.test\.(js|ts|jsx|tsx)$/, + '**/generated/**', + ], + components: [ + 'InternalComponent', + /.*Provider$/, + /^Styled/, + ], + }, + }, +}) +``` + +--- + +## `consolePiping` + +Controls bidirectional console log piping between client (browser) and server (terminal/SSR runtime). + +| Field | Type | Default | Description | +|---|---|---|---| +| `enabled` | `boolean` | `true` | Whether to enable console piping. When enabled, client `console.*` calls are forwarded to the terminal, and server `console.*` calls are forwarded to the browser console. | +| `levels` | `Array` | `['log', 'warn', 'error', 'info', 'debug']` | Which console methods to intercept and pipe. `ConsoleLevel` is `'log' \| 'warn' \| 'error' \| 'info' \| 'debug'`. | + +**Runtime behavior:** +- Client batches entries (max 50, flush after 100ms) and POSTs to `/__tsd/console-pipe` +- Server batches entries (max 20, flush after 50ms) and POSTs to `/__tsd/console-pipe/server` +- Browser subscribes to server logs via `EventSource` at `/__tsd/console-pipe/sse` +- Self-referential log messages (containing `[TSD Console Pipe]` or `[@tanstack/devtools`) are excluded to prevent recursion +- Flushes remaining batch on `beforeunload` (client only) + +**Example:** + +```ts +// Only pipe errors and warnings +devtools({ + consolePiping: { + enabled: true, + levels: ['error', 'warn'], + }, +}) + +// Disable entirely +devtools({ + consolePiping: { + enabled: false, + }, +}) +``` + +--- + +## `enhancedLogs` + +Controls the Babel transform that prepends source location information to `console.log()` and `console.error()` calls. + +| Field | Type | Default | Description | +|---|---|---|---| +| `enabled` | `boolean` | `true` | Whether to enhance console.log and console.error with source location. When enabled, each log call gets a clickable "Go to Source" link in the browser and a file:line:column prefix in the terminal. | + +**What gets transformed:** +- Only `console.log(...)` and `console.error(...)` calls (not `warn`, `info`, `debug`) +- Skips `node_modules`, `?raw`, `/dist/`, `/build/` +- Skips files that don't contain the string `console.` + +**Browser output format:** +``` +%cLOG%c %cGo to Source: http://localhost:5173/__tsd/open-source?source=...%c + -> +``` + +**Server output format (chalk):** +``` +LOG /src/components/Header.tsx:26:13 + -> +``` + +**Example:** + +```ts +devtools({ + enhancedLogs: { + enabled: false, // disable source-annotated logs + }, +}) +``` + +--- + +## `removeDevtoolsOnBuild` + +Controls whether devtools code is stripped from production builds. + +| Field | Type | Default | Description | +|---|---|---|---| +| `removeDevtoolsOnBuild` | `boolean` | `true` | When true, removes all devtools imports and JSX usage from production builds. | + +**Packages stripped:** +- `@tanstack/react-devtools` +- `@tanstack/preact-devtools` +- `@tanstack/solid-devtools` +- `@tanstack/vue-devtools` +- `@tanstack/devtools` + +**Activation condition:** Active when `command !== 'serve'` OR `config.mode === 'production'`. This dual check supports hosting providers (Cloudflare, Netlify, Heroku) that may not use the `build` command but always set mode to `production`. + +**What gets removed:** +1. Import declarations from the listed packages +2. JSX elements using the imported component names +3. Leftover imports that were only referenced inside the removed JSX (e.g., plugin panel components referenced in the `plugins` prop) + +**Example:** + +```ts +// Keep devtools in production (for staging/QA environments) +devtools({ + removeDevtoolsOnBuild: false, +}) +``` + +--- + +## `logging` + +Controls the plugin's own console output. + +| Field | Type | Default | Description | +|---|---|---|---| +| `logging` | `boolean` | `true` | Whether the devtools plugin logs status messages to the terminal (e.g., "Removed devtools code from: ..."). | + +**Example:** + +```ts +devtools({ + logging: false, // suppress devtools plugin output +}) +``` + +--- + +## `eventBusConfig` + +Configuration for the server event bus that handles devtools-to-client communication via WebSocket and SSE. + +| Field | Type | Default | Description | +|---|---|---|---| +| `enabled` | `boolean` | `true` | Whether to start the server event bus. Set to `false` when running devtools in environments that don't need it (e.g., Storybook, Vitest). This field is specific to the Vite plugin wrapper; it is not part of `ServerEventBusConfig` from `@tanstack/devtools-event-bus/server`. | +| `port` | `number` | `4206` | Preferred port for the event bus server. If the port is in use (EADDRINUSE), the bus falls back to an OS-assigned port (port 0). | +| `host` | `string` | Derived from `server.host` in Vite config, or `'localhost'` | Hostname to bind the event bus server to. | +| `debug` | `boolean` | `false` | When true, logs internal event bus activity (connections, dispatches, etc.) to the console. | +| `httpServer` | `HttpServerLike` | `undefined` | An external HTTP server to attach to instead of creating a standalone one. The Vite plugin automatically sets this when HTTPS is enabled (uses `server.httpServer` from Vite) so WebSocket/SSE connections share the same TLS certificate. You generally do not need to set this manually. | + +**`HttpServerLike` interface:** +```ts +interface HttpServerLike { + on: (event: string, listener: (...args: Array) => void) => this + removeListener: (event: string, listener: (...args: Array) => void) => this + address: () => { port: number; family: string; address: string } | string | null +} +``` + +**`ServerEventBusConfig` type (from `@tanstack/devtools-event-bus/server`):** +```ts +interface ServerEventBusConfig { + port?: number | undefined + host?: string | undefined + debug?: boolean | undefined + httpServer?: HttpServerLike | undefined +} +``` + +**HTTPS behavior:** When `server.https` is configured in Vite, the plugin passes `server.httpServer` as `httpServer` to the event bus. This causes the bus to piggyback on Vite's server rather than creating a standalone HTTP server, ensuring WebSocket and SSE connections use the same TLS certificate. + +**Port fallback:** The `ServerEventBus.start()` method tries the configured port first. On `EADDRINUSE`, it retries with port 0 (OS-assigned). The actual port is stored and injected into client code via `__TANSTACK_DEVTOOLS_PORT__` placeholder. + +**Example:** + +```ts +devtools({ + eventBusConfig: { + port: 4300, + enabled: true, + debug: true, // see all event bus activity + }, +}) + +// Disable for Storybook +devtools({ + eventBusConfig: { + enabled: false, + }, +}) +``` + +--- + +## `editor` + +Configuration for the "open in editor" functionality used by the source inspector. + +| Field | Type | Default | Description | +|---|---|---|---| +| `name` | `string` | `'VSCode'` | Name of the editor, used for debugging/logging purposes. | +| `open` | `(path: string, lineNumber: string \| undefined, columnNumber?: string) => Promise` | Uses `launch-editor` to open VS Code | Callback function that opens a file in the editor. The `path` is an absolute file path. `lineNumber` and `columnNumber` are strings (not numbers) or undefined. | + +**`EditorConfig` type (from `packages/devtools-vite/src/editor.ts`):** +```ts +type EditorConfig = { + name: string + open: ( + path: string, + lineNumber: string | undefined, + columnNumber?: string, + ) => Promise +} +``` + +**Default implementation:** +```ts +const DEFAULT_EDITOR_CONFIG: EditorConfig = { + name: 'VSCode', + open: async (path, lineNumber, columnNumber) => { + const launch = (await import('launch-editor')).default + launch( + `${path.replaceAll('$', '\\$')}${lineNumber ? `:${lineNumber}` : ''}${columnNumber ? `:${columnNumber}` : ''}`, + undefined, + (filename, err) => { + console.warn(`Failed to open ${filename} in editor: ${err}`) + }, + ) + }, +} +``` + +**Supported editors via launch-editor:** VS Code, WebStorm, IntelliJ IDEA, Sublime Text, Atom, Vim, Emacs, and more. Full list: https://github.com/yyx990803/launch-editor#supported-editors + +**Example -- custom editor:** + +```ts +devtools({ + editor: { + name: 'Neovim', + open: async (path, lineNumber, columnNumber) => { + const { execFile } = await import('node:child_process') + const lineArg = lineNumber ? `+${lineNumber}` : '' + execFile('nvim', [lineArg, path].filter(Boolean)) + }, + }, +}) +``` + +--- + +## Connection Placeholders + +These are not user-facing config options but are relevant if you work on `@tanstack/devtools` internals. The `connection-injection` sub-plugin replaces these string literals in `@tanstack/devtools*` and `@tanstack/event-bus` source code during dev: + +| Placeholder | Replaced with | Fallback | +|---|---|---| +| `__TANSTACK_DEVTOOLS_PORT__` | Actual event bus port (number) | `4206` | +| `__TANSTACK_DEVTOOLS_HOST__` | Event bus hostname (JSON string) | `"localhost"` | +| `__TANSTACK_DEVTOOLS_PROTOCOL__` | `"http"` or `"https"` (JSON string) | `"http"` | + +--- + +## Full Configuration Example + +```ts +import { devtools } from '@tanstack/devtools-vite' + +export default { + plugins: [ + devtools({ + // Source injection for Go to Source feature + injectSource: { + enabled: true, + ignore: { + files: [/.*\.stories\.(js|ts|jsx|tsx)$/], + components: [/^Styled/, 'InternalWrapper'], + }, + }, + + // Bidirectional console piping + consolePiping: { + enabled: true, + levels: ['log', 'warn', 'error'], + }, + + // Enhanced console.log/error with source locations + enhancedLogs: { + enabled: true, + }, + + // Strip devtools from production builds + removeDevtoolsOnBuild: true, + + // Plugin console output + logging: true, + + // Server event bus + eventBusConfig: { + port: 4206, + enabled: true, + debug: false, + }, + + // Editor integration (default: VS Code via launch-editor) + // editor: { name: 'VSCode', open: async (path, line, col) => { ... } }, + }), + // ... framework plugin (react(), vue(), solid(), etc.) + ], +} +``` + +--- + +## Defaults Summary + +| Option | Default Value | +|---|---| +| `injectSource.enabled` | `true` | +| `injectSource.ignore` | `undefined` (no ignores) | +| `consolePiping.enabled` | `true` | +| `consolePiping.levels` | `['log', 'warn', 'error', 'info', 'debug']` | +| `enhancedLogs.enabled` | `true` | +| `removeDevtoolsOnBuild` | `true` | +| `logging` | `true` | +| `eventBusConfig.enabled` | `true` | +| `eventBusConfig.port` | `4206` | +| `eventBusConfig.host` | Vite's `server.host` or `'localhost'` | +| `eventBusConfig.debug` | `false` | +| `editor.name` | `'VSCode'` | +| `editor.open` | Uses `launch-editor` | diff --git a/packages/devtools/skills/devtools-app-setup/SKILL.md b/packages/devtools/skills/devtools-app-setup/SKILL.md new file mode 100644 index 00000000..a9fa86a5 --- /dev/null +++ b/packages/devtools/skills/devtools-app-setup/SKILL.md @@ -0,0 +1,359 @@ +--- +name: devtools-app-setup +description: > + Install TanStack Devtools, pick framework adapter (React/Vue/Solid/Preact), + register plugins via plugins prop, configure shell (position, hotkeys, theme, + hideUntilHover, requireUrlFlag, eventBusConfig). TanStackDevtools component, + defaultOpen, localStorage persistence. +type: core +library: "@tanstack/devtools" +library_version: "0.10.12" +sources: + - docs/quick-start.md + - docs/installation.md + - docs/configuration.md + - docs/overview.md + - packages/devtools/src/context/devtools-store.ts + - packages/vue-devtools/src/types.ts + - packages/react-devtools/src/devtools.tsx +--- + +# TanStack Devtools App Setup + +## Setup + +### React (primary) + +Install as dev dependencies: + +```bash +npm install -D @tanstack/react-devtools @tanstack/devtools-vite +``` + +Mount `TanStackDevtools` at the root of your application: + +```tsx +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { TanStackDevtools } from '@tanstack/react-devtools' +import App from './App' + +createRoot(document.getElementById('root')!).render( + + + + , +) +``` + +Add plugins via the `plugins` prop. Each plugin needs `name` (string) and `render` (JSX element or render function): + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools' +import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' +import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' + +, + }, + { + name: 'TanStack Router', + render: , + }, + ]} +/> +``` + +### Vue + +```bash +npm install -D @tanstack/vue-devtools +``` + +Vue uses `component` (not `render`) in plugin definitions. This is the `TanStackDevtoolsVuePlugin` type: + +```vue + + + +``` + +The Vite plugin (`@tanstack/devtools-vite`) is optional for Vue but recommended for enhanced console logs and go-to-source. + +### Solid + +```bash +npm install -D @tanstack/solid-devtools @tanstack/devtools-vite +``` + +```tsx +import { render } from 'solid-js/web' +import { TanStackDevtools } from '@tanstack/solid-devtools' +import { SolidQueryDevtoolsPanel } from '@tanstack/solid-query-devtools' +import App from './App' + +render(() => ( + <> + + , + }, + ]} + /> + +), document.getElementById('root')!) +``` + +### Preact + +```bash +npm install -D @tanstack/preact-devtools @tanstack/devtools-vite +``` + +```tsx +import { render } from 'preact' +import { TanStackDevtools } from '@tanstack/preact-devtools' +import App from './App' + +render( + <> + + , + }, + ]} + /> + , + document.getElementById('root')!, +) +``` + +## Core Patterns + +### Shell Configuration + +Pass a `config` prop to `TanStackDevtools` to set initial shell behavior. These values are persisted to `localStorage` after first load and can be changed through the settings panel at runtime. + +Storage keys used internally: +- `tanstack_devtools_settings` -- persisted settings +- `tanstack_devtools_state` -- persisted UI state (active tab, panel height, active plugins, persistOpen) + +All config properties are optional. Defaults shown below: + +```tsx + +``` + +### Event Bus Configuration + +The `eventBusConfig` prop configures the client-side event bus that plugins use for communication: + +```tsx + +``` + +The server event bus requires the `@tanstack/devtools-vite` plugin to be running. + +### Plugin Registration with defaultOpen + +Each plugin entry can include a `defaultOpen` flag to control whether that plugin tab is active when devtools first opens: + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools' +import { FormDevtools } from '@tanstack/react-form' + +, + defaultOpen: true, + }, + ]} +/> +``` + +### Conditional Devtools with URL Flag + +Use `requireUrlFlag` to hide devtools unless a specific URL parameter is present. This is useful for staging environments or team-internal debugging: + +```tsx + +``` + +## Common Mistakes + +### CRITICAL: Vue plugin uses `render` instead of `component` + +The Vue adapter uses `component` (a Vue component reference) and optional `props`, not JSX `render`. Using `render` produces a silent failure -- the plugin tab appears but renders nothing. + +Wrong: + +```vue + + +``` + +Correct: + +```vue + +``` + +The `TanStackDevtoolsVuePlugin` type enforces this at compile time. Always import and use it. + +### HIGH: Vite plugin not placed first in plugins array + +The `@tanstack/devtools-vite` plugin performs source injection that must run before framework plugins (React, Vue, Solid, etc.) process the code. + +Wrong: + +```ts +import { devtools } from '@tanstack/devtools-vite' +import react from '@vitejs/plugin-react' + +export default { + plugins: [react(), devtools()], +} +``` + +Correct: + +```ts +import { devtools } from '@tanstack/devtools-vite' +import react from '@vitejs/plugin-react' + +export default { + plugins: [devtools(), react()], +} +``` + +### HIGH: Mounting TanStackDevtools in SSR without client guard + +The devtools core shell requires DOM APIs (`document`, `window`, `localStorage`). The React adapter includes `'use client'` at its entry point, so standard Next.js/Remix setups work. However, custom SSR setups or frameworks that do not respect the `'use client'` directive need explicit guards. + +Wrong: + +```tsx +// In a server-rendered component without framework 'use client' support +import { TanStackDevtools } from '@tanstack/react-devtools' + +export default function Layout({ children }) { + return ( + <> + {children} + + + ) +} +``` + +Correct: + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools' + +export default function Layout({ children }) { + return ( + <> + {children} + {typeof window !== 'undefined' && } + + ) +} +``` + +Or use dynamic imports / lazy loading to ensure the component only loads on the client. + +### MEDIUM: Installing as regular dependency for dev-only use + +When using the Vite plugin for production stripping, devtools packages should be dev dependencies. Installing them as regular dependencies increases production bundle size unnecessarily. + +Wrong: + +```bash +npm install @tanstack/react-devtools +``` + +Correct: + +```bash +npm install -D @tanstack/react-devtools +npm install -D @tanstack/devtools-vite +``` + +Exception: if you intentionally want devtools in production, install `@tanstack/devtools` (core) as a regular dependency. See the production skill for details. + +### MEDIUM: Not keeping devtools packages at latest versions + +All `@tanstack/devtools-*` packages share internal protocols (event bus messages, plugin mount lifecycle). Mixing versions can cause silent failures where plugins register but never receive events, or the shell mounts but plugins do not render. + +Always update all devtools packages together: + +```bash +npm install -D @tanstack/react-devtools@latest @tanstack/devtools-vite@latest +``` + +When building custom plugins, ensure `@tanstack/devtools-event-client` matches the version of `@tanstack/devtools` used by the shell. + +## See Also + +- **devtools-vite-plugin** -- Vite plugin configuration: source inspection, console piping, production stripping, server event bus setup +- **devtools-production** -- Production build handling: keeping devtools in prod, tree-shaking, URL flag gating +- **devtools-plugin-panel** -- Building custom plugin panels with the EventClient API diff --git a/packages/devtools/skills/devtools-marketplace/SKILL.md b/packages/devtools/skills/devtools-marketplace/SKILL.md new file mode 100644 index 00000000..2fba60d4 --- /dev/null +++ b/packages/devtools/skills/devtools-marketplace/SKILL.md @@ -0,0 +1,389 @@ +--- +name: devtools-marketplace +description: > + Publish plugin to npm and submit to TanStack Devtools Marketplace. + PluginMetadata registry format, plugin-registry.ts, pluginImport (importName, type), + requires (packageName, minVersion), framework tagging, multi-framework submissions, + featured plugins. +type: lifecycle +library: "@tanstack/devtools" +library_version: "0.10.12" +requires: + - devtools-plugin-panel +sources: + - docs/third-party-plugins.md + - packages/devtools/src/tabs/plugin-registry.ts + - packages/devtools/src/tabs/marketplace/types.ts + - packages/devtools/src/tabs/marketplace/plugin-utils.ts + - packages/devtools-vite/src/inject-plugin.ts + - packages/devtools-client/src/index.ts +--- + +# TanStack Devtools Marketplace + +> **Prerequisite:** Build a working plugin first using the **devtools-plugin-panel** skill. The marketplace submission assumes you already have a published npm package that exports either a JSX panel component or a function-based plugin. + +## Overview + +The TanStack Devtools Marketplace is a built-in registry inside the devtools shell. Users browse it from the Marketplace tab, and can install plugins with a single click. Submission is a PR to the `packages/devtools/src/tabs/plugin-registry.ts` file in the [TanStack/devtools](https://github.com/TanStack/devtools) repository. + +## PluginMetadata Interface + +Every marketplace entry conforms to the `PluginMetadata` interface exported from `packages/devtools/src/tabs/plugin-registry.ts`: + +```ts +export interface PluginMetadata { + /** Package name on npm (e.g., '@acme/react-analytics-devtools') */ + packageName: string + + /** Display title shown on the marketplace card */ + title: string + + /** Short description of what the plugin does */ + description?: string + + /** URL to a logo image (SVG, PNG, etc.) */ + logoUrl?: string + + /** Required base package dependency */ + requires?: { + /** Required package name (e.g., '@tanstack/react-query') */ + packageName: string + /** Minimum required version (semver) */ + minVersion: string + /** Maximum version (if there's a known breaking change) */ + maxVersion?: string + } + + /** Plugin import configuration -- enables one-click auto-install */ + pluginImport?: { + /** The exact export name to import from the package + * (e.g., 'FormDevtoolsPlugin' or 'ReactQueryDevtoolsPanel') */ + importName: string + /** 'jsx' = component rendered via { name, render: } + * 'function' = called directly as FnName() in the plugins array */ + type: 'jsx' | 'function' + } + + /** Custom plugin ID for matching against registered plugins. + * The default behavior lowercases the package name and replaces + * non-alphanumeric characters with '-'. + * Example: pluginId: 'tanstack-form' matches 'tanstack-form-4'. */ + pluginId?: string + + /** URL to the plugin's documentation */ + docsUrl?: string + + /** Plugin author/maintainer */ + author?: string + + /** Repository URL */ + repoUrl?: string + + /** Framework this plugin supports */ + framework: 'react' | 'solid' | 'vue' | 'svelte' | 'angular' | 'other' + + /** Mark as featured -- appears in the Featured section with animated border. + * Reserved for official TanStack partners. */ + featured?: boolean + + /** Mark as new -- shows a "New" banner on the card */ + isNew?: boolean + + /** Tags for filtering and categorization */ + tags?: Array +} +``` + +### Required vs Optional Fields + +Only two fields are strictly required by the TypeScript interface: `packageName`, `title`, and `framework`. In practice, always provide `requires`, `pluginImport`, and `description` -- without them the marketplace card is functional but auto-install cannot wire up the plugin. + +## Registry Entry Examples + +### React Plugin (function-based) + +A function-based plugin exports a factory function that returns a plugin object. The auto-injector calls it as `FormDevtoolsPlugin()` inside the `plugins` array: + +```ts +// In packages/devtools/src/tabs/plugin-registry.ts + +'@acme/react-analytics-devtools': { + packageName: '@acme/react-analytics-devtools', + title: 'Acme Analytics Devtools', + description: 'Inspect analytics events, funnels, and session data', + requires: { + packageName: '@acme/react-analytics', + minVersion: '2.0.0', + }, + pluginImport: { + importName: 'AnalyticsDevtoolsPlugin', + type: 'function', + }, + pluginId: 'acme-analytics', + docsUrl: 'https://acme.dev/analytics/devtools', + repoUrl: 'https://github.com/acme/analytics', + author: 'Acme Corp', + framework: 'react', + isNew: true, + tags: ['Analytics', 'Tracking'], +}, +``` + +When a user clicks "Install" in the marketplace, the Vite plugin: +1. Runs the package manager to install `@acme/react-analytics-devtools` +2. Finds the file containing `` +3. Adds `import { AnalyticsDevtoolsPlugin } from '@acme/react-analytics-devtools'` +4. Injects `AnalyticsDevtoolsPlugin()` into the `plugins` array + +### React Plugin (JSX-based) + +A JSX-based plugin exports a React component. The auto-injector wraps it in `{ name, render: }`: + +```ts +'@acme/react-state-devtools': { + packageName: '@acme/react-state-devtools', + title: 'Acme State Inspector', + description: 'Real-time state tree visualization', + requires: { + packageName: '@acme/react-state', + minVersion: '1.5.0', + }, + pluginImport: { + importName: 'AcmeStateDevtoolsPanel', + type: 'jsx', + }, + author: 'Acme Corp', + framework: 'react', + tags: ['State Management'], +}, +``` + +The injected code looks like: + +```tsx +import { AcmeStateDevtoolsPanel } from '@acme/react-state-devtools' + + }, + ]} +/> +``` + +### Multi-Framework Submission (React + Solid) + +When your devtools package supports multiple frameworks, add one entry per framework. Each entry is keyed by its own npm package name: + +```ts +'@acme/react-analytics-devtools': { + packageName: '@acme/react-analytics-devtools', + title: 'Acme Analytics Devtools', + description: 'Inspect analytics events, funnels, and session data', + requires: { + packageName: '@acme/react-analytics', + minVersion: '2.0.0', + }, + pluginImport: { + importName: 'AnalyticsDevtoolsPlugin', + type: 'function', + }, + pluginId: 'acme-analytics', + author: 'Acme Corp', + framework: 'react', + isNew: true, + tags: ['Analytics', 'Tracking'], +}, +'@acme/solid-analytics-devtools': { + packageName: '@acme/solid-analytics-devtools', + title: 'Acme Analytics Devtools', + description: 'Inspect analytics events, funnels, and session data', + requires: { + packageName: '@acme/solid-analytics', + minVersion: '2.0.0', + }, + pluginImport: { + importName: 'AnalyticsDevtoolsPlugin', + type: 'function', + }, + pluginId: 'acme-analytics', + author: 'Acme Corp', + framework: 'solid', + isNew: true, + tags: ['Analytics', 'Tracking'], +}, +``` + +The marketplace auto-detects the user's framework from their `package.json` dependencies and shows only matching entries. Users can still browse other frameworks via the filter controls. + +## How Auto-Install Works + +The auto-install pipeline lives in `packages/devtools-vite/src/inject-plugin.ts`. Understanding it clarifies why `pluginImport` matters: + +1. **Package installation** -- The Vite plugin detects the project's package manager and runs the appropriate install command. +2. **File detection** -- It scans project files for imports from `@tanstack/react-devtools`, `@tanstack/solid-devtools`, `@tanstack/vue-devtools`, etc. +3. **AST transformation** -- It parses the file with Babel, finds the `` JSX element, and modifies the `plugins` prop. +4. **Import insertion** -- It adds `import { } from ''` after the last existing import. +5. **Plugin injection** -- Based on `pluginImport.type`: + - `'function'`: Appends `ImportName()` directly to the plugins array + - `'jsx'`: Appends `{ name: '', render: <ImportName /> }` to the plugins array + +If `pluginImport` is missing, step 3-5 are skipped entirely. The package gets installed but the user must manually wire it into the `plugins` prop. + +## PR Submission Process + +1. **Publish your package to npm.** The marketplace links to npm for installation; the package must be publicly available. + +2. **Fork and clone** the [TanStack/devtools](https://github.com/TanStack/devtools) repository. + +3. **Edit `packages/devtools/src/tabs/plugin-registry.ts`.** Add your entry to the `PLUGIN_REGISTRY` object under the `THIRD-PARTY PLUGINS` comment section: + + ```ts + // ========================================== + // THIRD-PARTY PLUGINS - Examples + // ========================================== + // External contributors can add their plugins below! + ``` + +4. **Open a PR** against the `main` branch. Title format: `feat(marketplace): add <your-plugin-name>`. + +5. **The PR will be reviewed** by TanStack maintainers. Common review feedback: + - Missing `pluginImport` -- reviewers will ask you to add it + - Missing `framework` -- required for marketplace filtering + - Missing `requires.minVersion` -- avoids runtime errors for users on older versions + - Incorrect `importName` -- must match the exact named export from your package + +## Framework Detection + +The marketplace determines the user's current framework by scanning their `package.json` dependencies for known framework packages: + +| Framework | Detected packages | +|-----------|------------------| +| react | `react`, `react-dom` | +| solid | `solid-js` | +| vue | `vue`, `@vue/core` | +| svelte | `svelte` | +| angular | `@angular/core` | + +Plugins with `framework: 'other'` are shown regardless of the detected framework. + +## Featured Plugins + +The `featured` field is reserved for official TanStack partners and select library authors. Featured plugins appear in a dedicated section at the top of the marketplace with an animated border. + +To request featured status, email <partners+devtools@tanstack.com>. + +Do not set `featured: true` in your PR submission -- it will be rejected. The TanStack team sets this flag. + +## Plugin ID Matching + +When the marketplace checks if a plugin is already active, it uses `pluginId` for matching. The matching logic in `packages/devtools/src/tabs/marketplace/plugin-utils.ts` does: + +1. If `pluginId` is set, checks whether any registered plugin's ID starts with or contains the `pluginId` (case-insensitive). +2. Otherwise falls back to matching on `packageName` and extracting keyword segments. + +Set a custom `pluginId` when your plugin registers with an ID that differs from the default (lowercased package name with non-alphanumeric characters replaced by `-`). For example, `@tanstack/react-form-devtools` registers as `tanstack-form-4` at runtime, so the registry entry uses `pluginId: 'tanstack-form'` to match it. + +## Common Mistakes + +### HIGH: Missing pluginImport metadata for auto-install + +Without `pluginImport.importName` and `pluginImport.type`, the marketplace auto-install pipeline installs the npm package but cannot inject the plugin into the user's code. The user sees a successful install but the plugin tab never appears -- they must manually add the import and wire it into the `plugins` prop. + +Wrong -- no pluginImport: + +```ts +'@acme/react-analytics-devtools': { + packageName: '@acme/react-analytics-devtools', + title: 'Acme Analytics Devtools', + requires: { + packageName: '@acme/react-analytics', + minVersion: '2.0.0', + }, + author: 'Acme Corp', + framework: 'react', +}, +``` + +Correct -- pluginImport provided: + +```ts +'@acme/react-analytics-devtools': { + packageName: '@acme/react-analytics-devtools', + title: 'Acme Analytics Devtools', + requires: { + packageName: '@acme/react-analytics', + minVersion: '2.0.0', + }, + pluginImport: { + importName: 'AnalyticsDevtoolsPlugin', + type: 'function', + }, + author: 'Acme Corp', + framework: 'react', +}, +``` + +The `importName` must be the exact named export from your package. The `type` must match how the export is consumed: +- `'function'` if your export is a factory like `export function AnalyticsDevtoolsPlugin() { return { name: '...', ... } }` +- `'jsx'` if your export is a component like `export function AnalyticsDevtoolsPanel() { return <div>...</div> }` + +### MEDIUM: Not specifying requires.minVersion + +When `requires` is present but `minVersion` is omitted or set too low, users running older versions of the base package get runtime errors when the devtools plugin tries to access APIs that do not exist in their version. + +Wrong -- missing minVersion: + +```ts +requires: { + packageName: '@acme/react-analytics', +}, +``` + +This does not type-check -- `minVersion` is a required field inside `requires`. But setting it to `'0.0.0'` or an arbitrarily low version has the same practical effect: the marketplace shows the plugin as installable even when the user's version lacks the APIs your devtools plugin depends on. + +Correct -- specify the actual minimum version your plugin is tested against: + +```ts +requires: { + packageName: '@acme/react-analytics', + minVersion: '2.0.0', +}, +``` + +If there is a known breaking change in a later version, also set `maxVersion`: + +```ts +requires: { + packageName: '@acme/react-analytics', + minVersion: '2.0.0', + maxVersion: '3.0.0', +}, +``` + +The marketplace uses semver comparison (`packages/devtools/src/tabs/semver-utils.ts`) to determine if the user's installed version satisfies the range. When it does not, the card shows a "Bump Version" action instead of "Install". + +### MEDIUM: Submitting without framework field + +The `framework` field enables marketplace filtering. Without it (or with it set incorrectly), users cannot find your plugin when browsing by framework, and the marketplace cannot determine whether to show it for the current project. + +The framework is required by the TypeScript interface, so omitting it is a compile error. The real mistake is setting it to `'other'` when the plugin is framework-specific. A React-only plugin tagged `'other'` will appear for Solid, Vue, and Angular users who cannot use it. + +Wrong: + +```ts +framework: 'other', // but the plugin only works with React +``` + +Correct: + +```ts +framework: 'react', +``` + +Use `'other'` only for truly framework-agnostic plugins that work in any environment. + +## See Also + +- **devtools-plugin-panel** -- Build a working devtools plugin panel before submitting to the marketplace +- **devtools-app-setup** -- TanStackDevtools component setup, plugins prop format, framework adapters diff --git a/packages/devtools/skills/devtools-plugin-panel/SKILL.md b/packages/devtools/skills/devtools-plugin-panel/SKILL.md new file mode 100644 index 00000000..471c02cc --- /dev/null +++ b/packages/devtools/skills/devtools-plugin-panel/SKILL.md @@ -0,0 +1,406 @@ +--- +name: devtools-plugin-panel +description: > + Build devtools panel components that display emitted event data. Listen via + EventClient.on(), handle theme (light/dark), use @tanstack/devtools-ui + components. Plugin registration (name, render, id, defaultOpen), lifecycle + (mount, activate, destroy), max 3 active plugins. Two paths: Solid.js core + with devtools-ui for multi-framework support, or framework-specific panels. +type: core +library: tanstack-devtools +library_version: "0.10.12" +requires: + - devtools-event-client +sources: + - "TanStack/devtools:docs/building-custom-plugins.md" + - "TanStack/devtools:docs/plugin-lifecycle.md" + - "TanStack/devtools:docs/plugin-configuration.md" + - "TanStack/devtools:packages/devtools/src/context/devtools-context.tsx" +--- + +## TanStackDevtoolsPlugin Interface + +The low-level contract every plugin implements. Framework adapters wrap this automatically. + +```ts +// Source: packages/devtools/src/context/devtools-context.tsx +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 +} +``` + +- **`name`** (required) -- String tab title, or function receiving `(el, theme)` for custom rendering. +- **`render`** (required) -- Called on activation with a `<div>` container and theme. Called again on theme change. +- **`id`** (optional) -- Stable identifier. If omitted: `name.toLowerCase().replace(' ', '-')-{index}`. Explicit ids persist selection across reloads. +- **`defaultOpen`** (optional) -- Opens panel on first load when no saved state. Max 3 open. Does not override saved preferences. +- **`destroy`** (optional) -- Called on deactivation or unmount. Framework adapters handle cleanup automatically. + +--- + +## Two Development Paths + +### Path 1: Solid.js Core + Framework Adapters (Multi-Framework) + +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. + +### Path 2: Framework-Specific Panel (Single Framework) + +Build directly in your framework and use `createReactPlugin`/`createVuePlugin`/`createSolidPlugin`/`createPreactPlugin` from `@tanstack/devtools-utils`. + +--- + +## Path 1: Solid.js Core Panel + +### Step 1: Define Event Map and Create EventClient + +```ts +// src/event-client.ts +import { EventClient } from '@tanstack/devtools-event-client' + +type StoreEvents = { + 'state-changed': { storeName: string; state: unknown; timestamp: number } + 'action-dispatched': { storeName: string; action: string; payload: unknown } + 'reset': void +} + +class StoreInspectorClient extends EventClient<StoreEvents> { + constructor() { + super({ pluginId: 'store-inspector' }) + } +} + +export const storeInspector = new StoreInspectorClient() +``` + +Event names are suffixes only. The `pluginId` is prepended automatically: `'store-inspector:state-changed'`. + +### Step 2: Build the Solid.js Panel Component + +```tsx +/** @jsxImportSource solid-js */ +import { createSignal, onCleanup, For } from 'solid-js' +import { + MainPanel, Header, HeaderLogo, Section, SectionTitle, + JsonTree, Button, Tag, useTheme, +} from '@tanstack/devtools-ui' +import { storeInspector } from './event-client' + +export default function StoreInspectorPanel() { + const { theme } = useTheme() + const [state, setState] = createSignal<Record<string, unknown>>({}) + const [actions, setActions] = createSignal<Array<{ action: string; payload: unknown }>>([]) + + 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 }]) + }) + + onCleanup(() => { + cleanupState() + cleanupActions() + }) + + return ( + <MainPanel> + <Header> + <HeaderLogo flavor={{ light: '#1a1a2e', dark: '#e0e0e0' }}> + Store Inspector + </HeaderLogo> + </Header> + <Section> + <SectionTitle>Current State</SectionTitle> + <JsonTree value={state()} copyable defaultExpansionDepth={2} /> + </Section> + <Section> + <SectionTitle> + Action Log + <Tag color="purple" label="Actions" count={actions().length} /> + </SectionTitle> + <For each={actions()}> + {(a) => ( + <div> + <strong>{a.action}</strong> + <JsonTree value={a.payload} copyable defaultExpansionDepth={1} /> + </div> + )} + </For> + <Button variant="danger" onClick={() => setActions([])}>Clear Log</Button> + </Section> + </MainPanel> + ) +} +``` + +### Step 3: Create Core Class and Framework Adapters + +```ts +// src/core.ts +import { constructCoreClass } from '@tanstack/devtools-utils/solid/class' + +export const [StoreInspectorCore, NoOpStoreInspectorCore] = constructCoreClass( + () => import('./panel'), +) +``` + +```tsx +// src/react.tsx +import { createReactPanel } from '@tanstack/devtools-utils/react' +import { StoreInspectorCore } from './core' + +export const [StoreInspectorPanel, NoOpStoreInspectorPanel] = + createReactPanel(StoreInspectorCore) +``` + +```tsx +// src/react-plugin.tsx +import { createReactPlugin } from '@tanstack/devtools-utils/react' +import { StoreInspectorPanel } from './react' + +export const [StoreInspectorPlugin, NoOpStoreInspectorPlugin] = createReactPlugin({ + name: 'Store Inspector', + id: 'store-inspector', + defaultOpen: true, + Component: StoreInspectorPanel, +}) +``` + +### Step 4: Register + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools' +import { StoreInspectorPlugin } from 'your-package/react-plugin' + +function App() { + return ( + <> + <YourApp /> + <TanStackDevtools plugins={[StoreInspectorPlugin()]} /> + </> + ) +} +``` + +--- + +## Path 2: Framework-Specific Panel (React Example) + +```tsx +import { useState, useEffect } from 'react' +import { EventClient } from '@tanstack/devtools-event-client' +import { createReactPlugin } from '@tanstack/devtools-utils/react' + +type MyEvents = { + 'data-update': { items: Array<{ id: string; value: number }> } +} + +class MyPluginClient extends EventClient<MyEvents> { + constructor() { + super({ pluginId: 'my-plugin' }) + } +} + +export const myPlugin = new MyPluginClient() + +function MyPluginPanel({ theme }: { theme?: 'light' | 'dark' }) { + const [items, setItems] = useState<Array<{ id: string; value: number }>>([]) + + useEffect(() => { + const cleanup = myPlugin.on('data-update', (e) => { + setItems(e.payload.items) + }) + return cleanup + }, []) + + return ( + <div style={{ color: theme === 'dark' ? '#fff' : '#000' }}> + <h3>My Plugin</h3> + <ul> + {items.map((item) => ( + <li key={item.id}>{item.id}: {item.value}</li> + ))} + </ul> + </div> + ) +} + +export const [MyPlugin, NoOpMyPlugin] = createReactPlugin({ + name: 'My Plugin', + id: 'my-plugin', + defaultOpen: false, + Component: MyPluginPanel, +}) +``` + +--- + +## Plugin Lifecycle Sequence + +1. **Initialization** -- `TanStackDevtoolsCore` receives `plugins` array. Each plugin gets an `id` (explicit or generated). +2. **DOM containers created** -- Core creates `<div id="plugin-container-{id}">` and `<h3 id="plugin-title-container-{id}">` per plugin. +3. **Activation** -- On tab click or `defaultOpen`, `plugin.render(container, theme)` called. +4. **Framework portaling** -- React uses `createPortal`, Solid uses `<Portal>`, Vue uses `<Teleport>`. +5. **Theme change** -- `render` called again with new theme value. +6. **Deactivation/Unmount** -- `destroy(pluginId)` called if provided. Framework adapters handle cleanup. + +Active plugin selection persisted in `localStorage` under key `tanstack_devtools_state`. + +--- + +## Common Mistakes + +### CRITICAL: Not Cleaning Up Event Listeners + +Each `on()` returns a cleanup function. Forgetting it causes memory leaks and duplicate handlers. + +Wrong: + +```ts +useEffect(() => { + client.on('state', cb) +}, []) +``` + +Correct: + +```ts +useEffect(() => { + const cleanup = client.on('state', cb) + return cleanup +}, []) +``` + +In Solid, use `onCleanup()`: + +```ts +const cleanup = storeInspector.on('state-changed', handler) +onCleanup(cleanup) +``` + +Source: docs/building-custom-plugins.md + +### HIGH: Oversubscribing to Events in Multiple Components + +Do not call `on()` in multiple components for the same event. Subscribe once in a shared store/hook. + +Wrong: + +```ts +function ComponentA() { + useEffect(() => { const c = client.on('state', cb1); return c }, []) +} +function ComponentB() { + useEffect(() => { const c = client.on('state', cb2); return c }, []) +} +``` + +Correct: + +```ts +function useStoreState() { + const [state, setState] = useState(null) + useEffect(() => { + const cleanup = client.on('state', (e) => setState(e.payload)) + return cleanup + }, []) + return state +} +``` + +Source: maintainer interview + +### MEDIUM: Hardcoding Repeated Event Payload Fields + +When emitting events that share common fields, create a shared base object. + +Wrong: + +```ts +client.emit('state-changed', { storeName: 'main', version: '1.0', state }) +client.emit('action-dispatched', { storeName: 'main', version: '1.0', action }) +``` + +Correct: + +```ts +const base = { storeName: 'main', version: '1.0' } +client.emit('state-changed', { ...base, state }) +client.emit('action-dispatched', { ...base, action }) +``` + +Source: maintainer interview + +### MEDIUM: Ignoring Theme Prop in Panel Component + +Panels must adapt styling to theme. Factory-created plugins receive `props.theme`. + +Wrong: + +```tsx +function MyPanel() { + return <div style={{ color: 'white' }}>Always white text</div> +} +``` + +Correct: + +```tsx +function MyPanel({ theme }: { theme?: 'light' | 'dark' }) { + return ( + <div style={{ color: theme === 'dark' ? '#e0e0e0' : '#1a1a1a' }}> + Theme-aware text + </div> + ) +} +``` + +In Solid panels using devtools-ui, use `useTheme()` instead of prop drilling. + +Source: docs/plugin-lifecycle.md + +### MEDIUM: Not Knowing Max 3 Active Plugins Limit + +`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. + +Source: packages/devtools/src/utils/get-default-active-plugins.ts + +### MEDIUM: Using Raw DOM Manipulation Instead of Framework Portals + +Framework adapters handle portaling. Do not manually manipulate DOM. + +Wrong: + +```ts +render: (el) => { + const div = document.createElement('div') + div.textContent = 'Hello' + el.appendChild(div) +} +``` + +Correct: + +```tsx +import { createReactPlugin } from '@tanstack/devtools-utils/react' +const [Plugin, NoOpPlugin] = createReactPlugin({ + name: 'My Plugin', + Component: ({ theme }) => <div>Hello</div>, +}) +``` + +Source: docs/plugin-lifecycle.md + +### MEDIUM: Not Keeping Devtools Packages at Latest Versions + +All `@tanstack/devtools-*` packages should be on compatible versions. For external plugins, pin to compatible ranges. + +Source: maintainer interview + +## References + +- [devtools-ui components and API](references/panel-api.md) diff --git a/packages/devtools/skills/devtools-plugin-panel/references/panel-api.md b/packages/devtools/skills/devtools-plugin-panel/references/panel-api.md new file mode 100644 index 00000000..2eb82db8 --- /dev/null +++ b/packages/devtools/skills/devtools-plugin-panel/references/panel-api.md @@ -0,0 +1,122 @@ +# Plugin Panel API Reference + +## Plugin Factory Functions + +All factories return `[Plugin, NoOpPlugin]` tuples for production tree-shaking. + +| Factory | Import Path | Framework | +|---|---|---| +| `createReactPlugin` | `@tanstack/devtools-utils/react` | React | +| `createSolidPlugin` | `@tanstack/devtools-utils/solid` | Solid.js | +| `createVuePlugin` | `@tanstack/devtools-utils/vue` | Vue 3 | +| `createPreactPlugin` | `@tanstack/devtools-utils/preact` | Preact | +| `createReactPanel` | `@tanstack/devtools-utils/react` | React (wraps Solid core) | +| `createSolidPanel` | `@tanstack/devtools-utils/solid` | Solid (wraps Solid core) | +| `constructCoreClass` | `@tanstack/devtools-utils/solid/class` | Core class construction | + +### createReactPlugin / createSolidPlugin / createPreactPlugin + +```ts +function createReactPlugin(config: { + name: string + id?: string + defaultOpen?: boolean + Component: (props: { theme?: 'light' | 'dark' }) => JSX.Element +}): readonly [() => PluginConfig, () => PluginConfig] +``` + +### createVuePlugin + +```ts +function createVuePlugin<TComponentProps extends Record<string, any>>( + name: string, + component: DefineComponent<TComponentProps, {}, unknown>, +): readonly [ + (props: TComponentProps) => { name: string; component: DefineComponent; props: TComponentProps }, + (props: TComponentProps) => { name: string; component: Fragment; props: TComponentProps }, +] +``` + +Vue uses positional `(name, component)` args, not an options object. + +--- + +## devtools-ui Components + +All components are Solid.js. Use in Path 1 (Solid core) panels only. + +| Component | Purpose | +|---|---| +| `MainPanel` | Root container with optional padding | +| `Header` | Top header bar | +| `HeaderLogo` | Logo section; accepts `flavor` colors | +| `Section` | Content section wrapper | +| `SectionTitle` | `<h3>` section heading | +| `SectionDescription` | `<p>` description text | +| `SectionIcon` | Icon wrapper in sections | +| `JsonTree` | Expandable JSON tree viewer with copy support | +| `Button` | Variants: primary, secondary, danger, success, info, warning; supports `outline` and `ghost` | +| `Tag` | Colored label tag with optional count badge | +| `Select` | Dropdown select with label and description | +| `Input` | Text input | +| `Checkbox` | Checkbox input | +| `TanStackLogo` | TanStack logo SVG | +| `ThemeContextProvider` | Wraps children with theme context | +| `useTheme` | Returns `{ theme: Accessor<Theme>, setTheme }` -- must be inside ThemeContextProvider | + +### JsonTree Props + +```ts +function JsonTree<TData>(props: { + value: TData + copyable?: boolean + defaultExpansionDepth?: number // default: 1 + collapsePaths?: Array<string> + config?: { dateFormat?: string } +}): JSX.Element +``` + +--- + +## EventClient API (Quick Reference) + +```ts +class EventClient<TEventMap extends Record<string, any>> { + constructor(config: { + pluginId: string + debug?: boolean // default: false + enabled?: boolean // default: true + reconnectEveryMs?: number // default: 300 + }) + + emit<TEvent extends keyof TEventMap & string>( + eventSuffix: TEvent, + payload: TEventMap[TEvent], + ): void + + on<TEvent extends keyof TEventMap & string>( + eventSuffix: TEvent, + cb: (event: { type: TEvent; payload: TEventMap[TEvent]; pluginId?: string }) => void, + options?: { withEventTarget?: boolean }, + ): () => void + + onAll(cb: (event: { type: string; payload: any }) => void): () => void + onAllPluginEvents(cb: (event: AllDevtoolsEvents<TEventMap>) => void): () => void + getPluginId(): string +} +``` + +--- + +## Key Source Files + +| File | Purpose | +|---|---| +| `packages/devtools/src/context/devtools-context.tsx` | `TanStackDevtoolsPlugin` interface, plugin ID generation | +| `packages/devtools/src/core.ts` | `TanStackDevtoolsCore` class | +| `packages/devtools/src/utils/constants.ts` | `MAX_ACTIVE_PLUGINS = 3` | +| `packages/devtools/src/utils/get-default-active-plugins.ts` | defaultOpen resolution logic | +| `packages/event-bus-client/src/plugin.ts` | `EventClient` class | +| `packages/devtools-utils/src/solid/class.ts` | `constructCoreClass` | +| `packages/devtools-ui/src/index.ts` | All UI component exports | +| `packages/devtools-ui/src/components/theme.tsx` | `ThemeContextProvider`, `useTheme` | diff --git a/packages/devtools/skills/devtools-production/SKILL.md b/packages/devtools/skills/devtools-production/SKILL.md new file mode 100644 index 00000000..83a08791 --- /dev/null +++ b/packages/devtools/skills/devtools-production/SKILL.md @@ -0,0 +1,427 @@ +--- +name: devtools-production +description: > + Handle devtools in production vs development. removeDevtoolsOnBuild, + devDependency vs regular dependency, conditional imports, NoOp plugin + variants for tree-shaking, non-Vite production exclusion patterns. +type: lifecycle +library: "@tanstack/devtools" +library_version: "0.10.12" +requires: devtools-app-setup +sources: + - docs/production.md + - docs/vite-plugin.md + - packages/devtools-vite/src/plugin.ts + - packages/devtools-vite/src/remove-devtools.ts + - packages/devtools/package.json + - packages/devtools/tsup.config.ts + - packages/devtools-utils/src/react/plugin.tsx + - packages/devtools-utils/src/react/panel.tsx +--- + +# TanStack Devtools Production Handling + +> **Prerequisite:** Read the **devtools-app-setup** skill first. The initial setup decisions (framework adapter, Vite plugin, dependency type) directly determine which production strategy applies. + +## How Production Stripping Works + +TanStack Devtools has two independent mechanisms for keeping devtools out of production bundles. Understanding both is essential because they serve different project types. + +### Mechanism 1: Vite Plugin Auto-Stripping (Vite projects) + +The `@tanstack/devtools-vite` plugin includes a sub-plugin named `@tanstack/devtools:remove-devtools-on-build`. When `removeDevtoolsOnBuild` is `true` (the default), this plugin runs during `vite build` and any non-`serve` command where the mode is `production`. + +It uses Babel to parse every source file, find imports from these packages, and remove them along with any JSX elements they produce: + +- `@tanstack/react-devtools` +- `@tanstack/preact-devtools` +- `@tanstack/solid-devtools` +- `@tanstack/devtools` + +The stripping is AST-based. It removes the import declaration, then finds and removes any JSX elements whose tag name matches one of the imported identifiers. It also traces plugin references inside the `plugins` prop array and removes their imports if they become unused. + +Source: `packages/devtools-vite/src/remove-devtools.ts` + +This means for a standard Vite project, the default setup from **devtools-app-setup** already handles production correctly with zero additional configuration: + +```tsx +// This import and JSX element are completely removed from the production build +import { TanStackDevtools } from '@tanstack/react-devtools' + +function App() { + return ( + <> + <YourApp /> + <TanStackDevtools plugins={[/* ... */]} /> + </> + ) +} +``` + +### Mechanism 2: Conditional Exports (package.json) + +The `@tanstack/devtools` core package uses Node.js conditional exports to serve different bundles based on the environment: + +```json +{ + "exports": { + "workerd": { "import": "./dist/server.js" }, + "browser": { + "development": { "import": "./dist/dev.js" }, + "import": "./dist/index.js" + }, + "node": { "import": "./dist/server.js" } + } +} +``` + +Key points: +- `browser` + `development` condition resolves to `dev.js` (dev-only extras). +- `browser` without `development` resolves to `index.js` (production build). +- `node` and `workerd` resolve to `server.js` (server-safe, no DOM). + +These are built via `tsup-preset-solid` with `dev_entry: true` and `server_entry: true` in `packages/devtools/tsup.config.ts`. + +## The Two Workflows + +### Development-Only Workflow (Default, Recommended) + +This is the standard path from **devtools-app-setup**. Devtools are present during `vite dev` and stripped automatically on `vite build`. + +**Install as dev dependencies:** + +```bash +npm install -D @tanstack/react-devtools @tanstack/devtools-vite +``` + +**Vite config -- default behavior:** + +```ts +import { devtools } from '@tanstack/devtools-vite' +import react from '@vitejs/plugin-react' + +export default { + plugins: [ + devtools(), // removeDevtoolsOnBuild defaults to true + react(), + ], +} +``` + +**Application code -- no guards needed:** + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools' + +function App() { + return ( + <> + <YourApp /> + <TanStackDevtools plugins={[/* ... */]} /> + </> + ) +} +``` + +The Vite plugin handles everything. The import and JSX are removed from the production build. Since the packages are dev dependencies, they are not even available in a production `node_modules` after `npm install --production`. + +### Production Workflow (Intentional) + +When you deliberately want devtools accessible in a deployed application. This requires three changes from the default setup. + +**1. Install as regular dependencies (not `-D`):** + +```bash +npm install @tanstack/react-devtools @tanstack/devtools-vite +``` + +This ensures the packages are available in production `node_modules`. + +**2. Disable auto-stripping in the Vite config:** + +```ts +import { devtools } from '@tanstack/devtools-vite' +import react from '@vitejs/plugin-react' + +export default { + plugins: [ + devtools({ + removeDevtoolsOnBuild: false, + }), + react(), + ], +} +``` + +**3. Application code remains the same:** + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools' + +function App() { + return ( + <> + <YourApp /> + <TanStackDevtools plugins={[/* ... */]} /> + </> + ) +} +``` + +With `removeDevtoolsOnBuild: false`, the Vite build plugin skips the AST stripping pass entirely, so all devtools code ships to production. + +You can combine this with `requireUrlFlag` from the shell config to hide the devtools UI unless a URL parameter is present: + +```tsx +<TanStackDevtools + config={{ + requireUrlFlag: true, + urlFlag: 'debug', // visit ?debug to show devtools + }} + plugins={[/* ... */]} +/> +``` + +## Non-Vite Projects + +Without the Vite plugin, there is no automatic stripping. You must manually prevent devtools from entering production bundles using one of these strategies. + +### Strategy A: Conditional Dynamic Import + +Create a separate file for devtools setup, then conditionally import it: + +```tsx +// devtools-setup.tsx +import { TanStackDevtools } from '@tanstack/react-devtools' + +export default function Devtools() { + return ( + <TanStackDevtools + plugins={[ + // your plugins + ]} + /> + ) +} +``` + +```tsx +// App.tsx +const Devtools = + process.env.NODE_ENV === 'development' + ? (await import('./devtools-setup')).default + : () => null + +function App() { + return ( + <> + <YourApp /> + <Devtools /> + </> + ) +} +``` + +When `NODE_ENV` is `'production'`, bundlers eliminate the dead `import()` path. The devtools-setup module and all its transitive dependencies are never included in the bundle. + +### Strategy B: Bundler-Specific Dead Code Elimination + +For bundlers that support define/replace plugins (webpack `DefinePlugin`, esbuild `define`, Rollup `@rollup/plugin-replace`), wrap the import in a condition that the bundler can statically evaluate: + +```tsx +// webpack example with DefinePlugin +let DevtoolsComponent: React.ComponentType = () => null + +if (__DEV__) { + const { TanStackDevtools } = await import('@tanstack/react-devtools') + DevtoolsComponent = () => <TanStackDevtools plugins={[/* ... */]} /> +} + +function App() { + return ( + <> + <YourApp /> + <DevtoolsComponent /> + </> + ) +} +``` + +The key requirement is that the condition must be statically resolvable by the bundler. `process.env.NODE_ENV === 'development'` works for most bundlers. Framework-specific globals like `__DEV__` also work. + +## NoOp Plugin Variants for Tree-Shaking + +When building reusable plugin packages with `@tanstack/devtools-utils`, the factory functions return a `[Plugin, NoOpPlugin]` tuple. The `NoOpPlugin` renders an empty fragment and carries no real dependencies. This is the primary mechanism for library authors to make their plugins tree-shakable. + +```tsx +import { createReactPlugin } from '@tanstack/devtools-utils/react' + +const [QueryPlugin, QueryNoOpPlugin] = createReactPlugin({ + name: 'TanStack Query', + Component: ({ theme }) => <QueryDevtoolsPanel theme={theme} />, +}) + +// The library exports both, and consumers choose: +export { QueryPlugin, QueryNoOpPlugin } +``` + +Consumer code uses the NoOp variant in production: + +```tsx +import { QueryPlugin, QueryNoOpPlugin } from '@tanstack/query-devtools' + +const ActivePlugin = + process.env.NODE_ENV === 'development' ? QueryPlugin : QueryNoOpPlugin + +function App() { + return ( + <TanStackDevtools + plugins={[ActivePlugin()]} + /> + ) +} +``` + +The NoOp pattern exists for every framework adapter: + +| Framework | Factory | Source | +|-----------|---------|--------| +| React | `createReactPlugin` | `packages/devtools-utils/src/react/plugin.tsx` | +| React (panel) | `createReactPanel` | `packages/devtools-utils/src/react/panel.tsx` | +| Preact | `createPreactPlugin` | `packages/devtools-utils/src/preact/plugin.tsx` | +| Solid | `createSolidPlugin` | `packages/devtools-utils/src/solid/plugin.tsx` | +| Vue | `createVuePlugin` | `packages/devtools-utils/src/vue/plugin.ts` | + +All return `readonly [Plugin, NoOpPlugin]`. The `NoOpPlugin` always has the same metadata (`name`, `id`, `defaultOpen`) but its render function produces an empty fragment, so the bundler can tree-shake the real panel component and all its dependencies. + +See the **devtools-framework-adapters** skill for the full factory API details. + +## Common Mistakes + +### HIGH: Keeping devtools in production without disabling stripping + +The Vite plugin's `removeDevtoolsOnBuild` defaults to `true`. If you want devtools in production, you must both disable stripping AND install as a regular dependency. Missing either step causes failure. + +**Wrong -- devtools stripped despite wanting them in production:** + +```ts +// vite.config.ts +export default { + plugins: [ + devtools(), // removeDevtoolsOnBuild defaults to true -- code is stripped + react(), + ], +} +``` + +```bash +# package.json has devtools as devDependency +npm install -D @tanstack/react-devtools +``` + +**Correct -- both changes together:** + +```ts +// vite.config.ts +export default { + plugins: [ + devtools({ removeDevtoolsOnBuild: false }), + react(), + ], +} +``` + +```bash +# regular dependency so it's available in production node_modules +npm install @tanstack/react-devtools +``` + +Missing `removeDevtoolsOnBuild: false` causes the AST stripping to remove all devtools imports and JSX at build time. Missing the regular dependency means `node_modules` may not contain the package in production environments that prune dev dependencies. + +### HIGH: Non-Vite projects not excluding devtools manually + +Without the Vite plugin, devtools code is never automatically stripped. If you import `TanStackDevtools` unconditionally, the entire devtools shell and all plugin panels ship to production. + +**Wrong -- always imports devtools regardless of environment:** + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools' + +function App() { + return ( + <> + <YourApp /> + <TanStackDevtools plugins={[/* ... */]} /> + </> + ) +} +``` + +**Correct -- conditional import based on NODE_ENV:** + +```tsx +const Devtools = + process.env.NODE_ENV === 'development' + ? (await import('./devtools-setup')).default + : () => null + +function App() { + return ( + <> + <YourApp /> + <Devtools /> + </> + ) +} +``` + +The conditional must be statically evaluable by your bundler so it can eliminate the dead branch. Using a separate file for the devtools setup ensures the entire module subgraph is tree-shaken. + +### MEDIUM: Not using NoOp variants in plugin libraries + +When building a reusable plugin package, exporting only the `Plugin` function (ignoring the `NoOpPlugin` from the tuple) means consumers have no lightweight alternative for production builds. + +**Wrong -- NoOp variant discarded:** + +```tsx +const [MyPlugin] = createReactPlugin({ + name: 'Store Inspector', + Component: StoreInspectorPanel, +}) + +export { MyPlugin } +``` + +**Correct -- both variants exported:** + +```tsx +const [MyPlugin, MyNoOpPlugin] = createReactPlugin({ + name: 'Store Inspector', + Component: StoreInspectorPanel, +}) + +export { MyPlugin, MyNoOpPlugin } +``` + +Consumers then choose the appropriate variant based on their environment. Without the NoOp export, the only way to exclude the plugin is to not import the package at all, which requires the conditional-import pattern at the application level. + +## Design Tension + +Development convenience pulls toward automatic stripping (dev dependencies, Vite plugin handles everything). Production usage pulls toward explicit inclusion (regular dependencies, disabled stripping, URL flag gating). These two paths are mutually exclusive in their dependency and configuration choices. A project must commit to one path. Attempting to mix them -- for example, keeping devtools as a dev dependency while setting `removeDevtoolsOnBuild: false` -- leads to builds that fail silently when the production environment prunes dev dependencies. + +For staging/preview environments where you want devtools but not in the final production deployment, use `requireUrlFlag` with the development-only workflow intact, rather than switching to the production workflow. + +## Cross-References + +- **devtools-app-setup** -- Initial setup decisions (framework, install command, Vite plugin placement) that this skill builds on. +- **devtools-vite-plugin** -- The `removeDevtoolsOnBuild` option and AST stripping logic live in the Vite plugin. See that skill for all Vite plugin configuration. +- **devtools-framework-adapters** -- The `[Plugin, NoOpPlugin]` tuple pattern and all framework-specific factory APIs. + +## Key Source Files + +- `packages/devtools-vite/src/plugin.ts` -- Vite plugin entry, `removeDevtoolsOnBuild` option, sub-plugin registration +- `packages/devtools-vite/src/remove-devtools.ts` -- AST-based stripping logic (Babel parse, traverse, codegen) +- `packages/devtools/package.json` -- Conditional exports (`browser.development` -> `dev.js`, `browser` -> `index.js`, `node`/`workerd` -> `server.js`) +- `packages/devtools/tsup.config.ts` -- Build config producing `dev.js`, `index.js`, `server.js` via `tsup-preset-solid` +- `packages/devtools-utils/src/react/plugin.tsx` -- `createReactPlugin` returning `[Plugin, NoOpPlugin]` +- `packages/devtools-utils/src/react/panel.tsx` -- `createReactPanel` returning `[Panel, NoOpPanel]` diff --git a/packages/event-bus-client/skills/devtools-bidirectional/SKILL.md b/packages/event-bus-client/skills/devtools-bidirectional/SKILL.md new file mode 100644 index 00000000..6c0737ff --- /dev/null +++ b/packages/event-bus-client/skills/devtools-bidirectional/SKILL.md @@ -0,0 +1,471 @@ +--- +name: devtools-bidirectional +description: Two-way event patterns between devtools panel and application. App-to-devtools observation, devtools-to-app commands, time-travel debugging with snapshots and revert. structuredClone for snapshot safety, distinct event suffixes for observation vs commands, serializable payloads only. +type: core +library: "@tanstack/devtools-event-client" +library_version: "0.10.12" +requires: devtools-event-client +sources: + - packages/event-bus-client/src/plugin.ts + - docs/bidirectional-communication.md +--- + +# devtools-bidirectional + +> **Prerequisite:** Read and understand the `devtools-event-client` skill first. This skill builds on `EventClient`, its event map types, `emit()`/`on()` API, pluginId namespacing, connection lifecycle, and singleton pattern. Everything here assumes you already have a working `EventClient` instance. + +Two-way communication between your application and a TanStack Devtools panel using `EventClient`. The same client instance handles both directions: the app emits observation events that the panel listens to, and the panel emits command events that the app listens to. + +## Core Concept + +`EventClient` is not unidirectional. Both `emit()` and `on()` work from either side -- application code or panel code -- on the same shared event bus. The direction is a convention you establish through your event map design, not a limitation of the API. + +``` +App code calls: client.emit('state-update', ...) // observation +Panel code calls: client.on('state-update', ...) // observation + +Panel code calls: client.emit('set-state', ...) // command +App code calls: client.on('set-state', ...) // command +``` + +## Core Patterns + +### 1. App-to-Devtools Observation + +The app emits state changes. The panel listens and renders. + +**Event map and client (shared module):** + +```ts +import { EventClient } from '@tanstack/devtools-event-client' + +type CounterEvents = { + // Observation: app -> panel + 'state-update': { count: number; updatedAt: number } +} + +class CounterDevtoolsClient extends EventClient<CounterEvents> { + constructor() { + super({ + pluginId: 'counter-inspector', + enabled: process.env.NODE_ENV !== 'production', + }) + } +} + +export const counterClient = new CounterDevtoolsClient() +``` + +**App side -- emit on state changes:** + +```ts +import { counterClient } from './counter-devtools-client' + +function increment() { + count += 1 + counterClient.emit('state-update', { + count, + updatedAt: Date.now(), + }) +} +``` + +**Panel side -- listen and display:** + +```ts +import { counterClient } from './counter-devtools-client' + +const cleanup = counterClient.on('state-update', (event) => { + // event.payload.count + // event.payload.updatedAt + renderPanel(event.payload) +}) +``` + +### 2. Devtools-to-App Commands + +The panel sends commands. The app listens and mutates state. + +**Extend the event map with command events:** + +```ts +type CounterEvents = { + // Observation: app -> panel + 'state-update': { count: number; updatedAt: number } + // Commands: panel -> app + 'reset': void + 'set-count': { count: number } +} +``` + +**Panel side -- emit commands on user interaction:** + +```ts +import { counterClient } from './counter-devtools-client' + +function handleResetClick() { + counterClient.emit('reset', undefined) +} + +function handleSetCount(newCount: number) { + counterClient.emit('set-count', { count: newCount }) +} +``` + +**App side -- listen for commands and react:** + +```ts +import { counterClient } from './counter-devtools-client' + +counterClient.on('reset', () => { + count = 0 + // Re-emit observation so panel updates + counterClient.emit('state-update', { + count, + updatedAt: Date.now(), + }) +}) + +counterClient.on('set-count', (event) => { + count = event.payload.count + counterClient.emit('state-update', { + count, + updatedAt: Date.now(), + }) +}) +``` + +The command handler re-emits an observation event after mutating state. This closes the loop so the panel sees the result of its own command. + +### 3. Time-Travel Debugging + +Combine observation (snapshots) with commands (revert) to build a time-travel slider. + +**Event map:** + +```ts +type TimeTravelEvents = { + // Observation: app -> panel + 'snapshot': { state: unknown; timestamp: number; label: string } + // Command: panel -> app + 'revert': { state: unknown } +} + +class TimeTravelClient extends EventClient<TimeTravelEvents> { + constructor() { + super({ + pluginId: 'time-travel', + enabled: process.env.NODE_ENV !== 'production', + }) + } +} + +export const timeTravelClient = new TimeTravelClient() +``` + +**App side -- emit snapshots with structuredClone:** + +```ts +import { timeTravelClient } from './time-travel-client' + +function applyAction(action: { type: string; payload: unknown }) { + 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', (event) => { + state = event.payload.state + rerender() +}) +``` + +`structuredClone(state)` is required here. Without it, the snapshot payload holds a reference to the live state object. When the app mutates state later, all previously stored snapshots in the panel are corrupted because they point to the same object. + +**Panel side -- collect snapshots and revert:** + +```tsx +import { timeTravelClient } from './time-travel-client' + +function TimeTravelPanel() { + const [snapshots, setSnapshots] = useState< + Array<{ state: unknown; timestamp: number; label: string }> + >([]) + const [index, setIndex] = useState(0) + + useEffect(() => { + return timeTravelClient.on('snapshot', (event) => { + setSnapshots((prev) => [...prev, event.payload]) + setIndex((prev) => prev + 1) + }) + }, []) + + const handleSliderChange = (newIndex: number) => { + setIndex(newIndex) + timeTravelClient.emit('revert', { + state: snapshots[newIndex].state, + }) + } + + return ( + <div> + <input + type="range" + min={0} + max={snapshots.length - 1} + value={index} + onChange={(e) => handleSliderChange(Number(e.target.value))} + /> + <p> + {snapshots[index]?.label} ( + {new Date(snapshots[index]?.timestamp).toLocaleTimeString()}) + </p> + <pre>{JSON.stringify(snapshots[index]?.state, null, 2)}</pre> + </div> + ) +} +``` + +After the app handles `revert`, it should re-emit a `snapshot` so the panel timeline stays current. The revert handler in the app side example above does not re-emit -- add it if your UI needs the timeline to update after a revert: + +```ts +timeTravelClient.on('revert', (event) => { + state = event.payload.state + rerender() + // Optional: re-emit so the timeline reflects the revert + timeTravelClient.emit('snapshot', { + state: structuredClone(state), + timestamp: Date.now(), + label: 'revert', + }) +}) +``` + +### 4. Bidirectional Event Map Design + +When a single plugin needs both observation and command events, define them all in one event map. Use naming conventions to distinguish direction: + +```ts +type StoreInspectorEvents = { + // Observation: app -> panel (describe what happened) + 'state-update': { storeName: string; state: unknown; timestamp: number } + 'action-dispatched': { storeName: string; action: string; payload: unknown } + 'error-caught': { storeName: string; error: string; stack?: string } + + // Commands: panel -> app (describe what to do) + 'set-state': { storeName: string; state: unknown } + 'dispatch-action': { storeName: string; action: string; payload: unknown } + 'reset': void + 'revert': { state: unknown } +} +``` + +Naming convention: +- **Observation events** describe what happened: `state-update`, `action-dispatched`, `error-caught`, `snapshot` +- **Command events** describe what to do: `set-state`, `dispatch-action`, `reset`, `revert` + +This distinction is purely a convention in your event map keys. The `EventClient` API is the same for both. But maintaining it makes your event map self-documenting and prevents confusion about which side emits vs listens. + +**Full bidirectional wiring with one client:** + +```ts +import { EventClient } from '@tanstack/devtools-event-client' + +type StoreInspectorEvents = { + 'state-update': { storeName: string; state: unknown; timestamp: number } + 'set-state': { storeName: string; state: unknown } + 'reset': void +} + +class StoreInspectorClient extends EventClient<StoreInspectorEvents> { + constructor() { + super({ + pluginId: 'store-inspector', + enabled: process.env.NODE_ENV !== 'production', + }) + } +} + +export const storeInspector = new StoreInspectorClient() +``` + +**App side:** + +```ts +import { storeInspector } from './store-inspector-client' + +// Observation: emit state changes +function updateStore(storeName: string, newState: unknown) { + stores[storeName] = newState + storeInspector.emit('state-update', { + storeName, + state: structuredClone(newState), + timestamp: Date.now(), + }) +} + +// Command handlers: listen for panel commands +storeInspector.on('set-state', (event) => { + const { storeName, state } = event.payload + stores[storeName] = state + storeInspector.emit('state-update', { + storeName, + state: structuredClone(state), + timestamp: Date.now(), + }) +}) + +storeInspector.on('reset', () => { + for (const storeName of Object.keys(stores)) { + stores[storeName] = initialStates[storeName] + storeInspector.emit('state-update', { + storeName, + state: structuredClone(initialStates[storeName]), + timestamp: Date.now(), + }) + } +}) +``` + +**Panel side:** + +```ts +import { storeInspector } from './store-inspector-client' + +// Observation: listen for state changes +storeInspector.on('state-update', (event) => { + renderStore(event.payload.storeName, event.payload.state) +}) + +// Commands: emit on user action +function handleEditState(storeName: string, newState: unknown) { + storeInspector.emit('set-state', { storeName, state: newState }) +} + +function handleReset() { + storeInspector.emit('reset', undefined) +} +``` + +## Debouncing Frequent Observations + +High-frequency state changes (e.g., mouse tracking, animation frames) can flood the event bus. Debounce on the emit side: + +```ts +import { storeInspector } from './store-inspector-client' + +let debounceTimer: ReturnType<typeof setTimeout> | null = null + +function emitStateUpdate(storeName: string, state: unknown) { + if (debounceTimer) clearTimeout(debounceTimer) + debounceTimer = setTimeout(() => { + storeInspector.emit('state-update', { + storeName, + state: structuredClone(state), + timestamp: Date.now(), + }) + }, 16) // ~60fps cap +} +``` + +Do not debounce command events. Commands are user-initiated and infrequent. + +## Common Mistakes + +### 1. Not using structuredClone for snapshots (HIGH) + +Without `structuredClone`, snapshot payloads hold references to the live state object. When the app mutates state later, every stored snapshot in the panel is silently corrupted. + +Wrong: + +```ts +timeTravelClient.emit('snapshot', { + state, + timestamp: Date.now(), + label: action.type, +}) +``` + +The panel stores `event.payload.state`, which is a reference to the app's `state` variable. On the next mutation, the panel's stored snapshot now reflects the new state, not the historical state. + +Correct: + +```ts +timeTravelClient.emit('snapshot', { + state: structuredClone(state), + timestamp: Date.now(), + label: action.type, +}) +``` + +`structuredClone` creates a deep copy. The snapshot is frozen in time regardless of future mutations. This applies to any observation event where the panel accumulates historical data -- not just time-travel. + +### 2. Non-serializable payloads in cross-tab scenarios (HIGH) + +When using the server event bus (WebSocket/SSE/BroadcastChannel), payloads are serialized for transport. Functions, DOM nodes, class instances with methods, `Map`, `Set`, `WeakRef`, and circular references all fail silently or lose data. + +This is especially dangerous in bidirectional patterns because command payloads flow panel-to-app and may cross transport boundaries. + +Wrong: + +```ts +storeInspector.emit('set-state', { + storeName: 'main', + state: { + items: new Map([['a', 1]]), // Map -- lost on serialization + onClick: () => alert('hi'), // Function -- lost on serialization + ref: document.getElementById('x'), // DOM node -- lost on serialization + }, +}) +``` + +Correct: + +```ts +storeInspector.emit('set-state', { + storeName: 'main', + state: { + items: Object.fromEntries(new Map([['a', 1]])), + timestamp: Date.now(), + }, +}) +``` + +Rule of thumb: if `JSON.parse(JSON.stringify(payload))` does not round-trip cleanly, the payload is not safe for the event bus. + +### 3. Not distinguishing observation from command events (MEDIUM) + +Mixing naming conventions makes the event map confusing and error-prone. Developers end up emitting observation events from the panel or command events from the app, breaking the communication contract. + +Wrong: + +```ts +type MyEvents = { + 'state': unknown // Is this observation or command? + 'update': unknown // Who emits this? + 'count': number // Unclear direction +} +``` + +Correct: + +```ts +type MyEvents = { + 'state-update': unknown // Observation: describes what happened + 'set-state': unknown // Command: describes what to do + 'count-changed': number // Observation: past tense / descriptive + 'reset': void // Command: imperative +} +``` + +Use observation suffixes that describe what happened (`-update`, `-changed`, `-dispatched`, `-caught`). Use command suffixes that describe what to do (`set-`, `dispatch-`, `reset`, `revert`). The naming convention is not enforced by the API, but consistent naming prevents wiring mistakes. + +## See Also + +- `devtools-event-client` -- base event system: event maps, `emit()`/`on()`, connection lifecycle, singleton pattern +- `devtools-instrumentation` -- strategic placement of `emit()` calls in library code benefits from bidirectional awareness (knowing that commands will flow back) diff --git a/packages/event-bus-client/skills/devtools-event-client/SKILL.md b/packages/event-bus-client/skills/devtools-event-client/SKILL.md new file mode 100644 index 00000000..da51871f --- /dev/null +++ b/packages/event-bus-client/skills/devtools-event-client/SKILL.md @@ -0,0 +1,285 @@ +--- +name: devtools-event-client +description: Create typed EventClient for a library. Define event maps with typed payloads, pluginId auto-prepend namespacing, emit()/on()/onAll()/onAllPluginEvents() API. Connection lifecycle (5 retries, 300ms), event queuing, enabled/disabled state, SSR fallbacks, singleton pattern. Unique pluginId requirement to avoid event collisions. +type: core +library: "@tanstack/devtools-event-client" +library_version: "0.10.12" +sources: + - packages/event-bus-client/src/plugin.ts + - docs/event-system.md + - docs/building-custom-plugins.md +--- + +# devtools-event-client + +Typed event emitter/listener that connects application code to TanStack Devtools panels. Framework-agnostic. Works in React, Vue, Solid, Preact, and vanilla JS. + +## Setup + +Install the package: + +```bash +npm i @tanstack/devtools-event-client +``` + +The package exports a single class: + +```ts +import { EventClient } from '@tanstack/devtools-event-client' +``` + +### Constructor Options + +| Option | Type | Required | Default | Description | +| ------------------ | --------- | -------- | ------- | ------------------------------------------------ | +| `pluginId` | `string` | Yes | -- | Identifies this plugin in the event system. Must be unique across all plugins. | +| `debug` | `boolean` | No | `false` | Enable verbose console logging prefixed with `[tanstack-devtools:{pluginId}-plugin]`. | +| `enabled` | `boolean` | No | `true` | When `false`, `emit()` is a no-op and `on()` returns a no-op cleanup function. | +| `reconnectEveryMs` | `number` | No | `300` | Interval in ms between connection retry attempts (max 5 retries). | + +## Core Patterns + +### 1. Define an Event Map and Create a Singleton Client + +Define a TypeScript type mapping event suffixes to payload types. Extend `EventClient` and export a single instance at module level. + +```ts +import { EventClient } from '@tanstack/devtools-event-client' + +type StoreEvents = { + 'state-changed': { storeName: string; state: unknown; timestamp: number } + 'action-dispatched': { storeName: string; action: string; payload: unknown } + 'reset': void +} + +class StoreInspectorClient extends EventClient<StoreEvents> { + constructor() { + super({ pluginId: 'store-inspector' }) + } +} + +// Module-level singleton -- one instance per plugin +export const storeInspector = new StoreInspectorClient() +``` + +Event map keys are suffixes only. The `pluginId` is prepended automatically. With `pluginId: 'store-inspector'` and key `'state-changed'`, the fully qualified event on the bus is `'store-inspector:state-changed'`. + +### 2. Emit Events + +Call `emit(suffix, payload)` from library code. Pass only the suffix. + +```ts +function dispatch(action: string, payload: unknown) { + state = reducer(state, action, payload) + + storeInspector.emit('state-changed', { + storeName: 'main', + state, + timestamp: Date.now(), + }) + storeInspector.emit('action-dispatched', { + storeName: 'main', + action, + payload, + }) +} +``` + +If the bus is not connected yet, events are queued in memory and flushed once the connection succeeds. If the connection fails after 5 retries (1.5s at default settings), the client gives up and subsequent `emit()` calls are silently dropped. + +Connection to the bus is initiated lazily on the first `emit()` call, not on construction or `on()`. + +### 3. Listen to Events + +All listener methods return a cleanup function. + +**`on(suffix, callback)`** -- listen to a specific event from this plugin: + +```ts +const cleanup = storeInspector.on('state-changed', (event) => { + // event.type === 'store-inspector:state-changed' + // event.payload === { storeName: string; state: unknown; timestamp: number } + // event.pluginId === 'store-inspector' + console.log(event.payload.state) +}) + +// Stop listening +cleanup() +``` + +**`on(suffix, callback, { withEventTarget: true })`** -- also register on an internal EventTarget so events emitted and listened to on the same client instance are delivered immediately without going through the global bus: + +```ts +const cleanup = storeInspector.on('state-changed', (event) => { + console.log(event.payload.state) +}, { withEventTarget: true }) +``` + +**`onAll(callback)`** -- listen to all events from all plugins: + +```ts +const cleanup = storeInspector.onAll((event) => { + console.log(event.type, event.payload) +}) +``` + +**`onAllPluginEvents(callback)`** -- listen to all events from this plugin only (filtered by `pluginId`): + +```ts +const cleanup = storeInspector.onAllPluginEvents((event) => { + // Only fires when event.pluginId === 'store-inspector' + console.log(event.type, event.payload) +}) +``` + +### 4. Connection Lifecycle and Disabling + +The connection lifecycle is: + +1. First `emit()` dispatches `tanstack-connect` and starts a retry loop. +2. Retries every `reconnectEveryMs` (default 300ms), up to 5 attempts. +3. On `tanstack-connect-success`, queued events are flushed in order. +4. After 5 failed retries, `failedToConnect` is set permanently. All subsequent `emit()` calls are silently dropped (not queued). + +To disable the client entirely (e.g., in production): + +```ts +class StoreInspectorClient extends EventClient<StoreEvents> { + constructor() { + super({ + pluginId: 'store-inspector', + enabled: process.env.NODE_ENV !== 'production', + }) + } +} +``` + +When `enabled` is `false`, `emit()` is a no-op and `on()`/`onAll()`/`onAllPluginEvents()` return no-op cleanup functions. + +## Common Mistakes + +### 1. Including pluginId prefix in event names (CRITICAL) + +`EventClient` auto-prepends the `pluginId` to all event names. Including the prefix manually produces a double-prefixed event name that nothing will match. + +Wrong: + +```ts +storeInspector.emit('store-inspector:state-changed', data) +// Dispatches 'store-inspector:store-inspector:state-changed' +``` + +Correct: + +```ts +storeInspector.emit('state-changed', data) +// Dispatches 'store-inspector:state-changed' +``` + +This applies to `on()` as well. Pass only the suffix. + +### 2. Creating multiple EventClient instances per plugin (CRITICAL) + +Each `EventClient` instance manages its own connection, event queue, and listeners independently. Creating multiple instances for the same plugin causes duplicate handlers, multiple connection attempts, and unpredictable event delivery. + +Wrong: + +```tsx +function MyComponent() { + // New instance on every render + const client = new StoreInspectorClient() + client.emit('state-changed', data) +} +``` + +Correct: + +```ts +// store-inspector-client.ts +export const storeInspector = new StoreInspectorClient() + +// MyComponent.tsx +import { storeInspector } from './store-inspector-client' +function MyComponent() { + storeInspector.emit('state-changed', data) +} +``` + +### 3. Non-unique pluginId causing event collisions (CRITICAL) + +Two plugins with the same `pluginId` share an event namespace. Events emitted by one are received by listeners on the other. Choose a unique, descriptive `pluginId` (e.g., `'my-org-store-inspector'` rather than `'store'`). + +### 4. Not realizing events drop after 5 failed retries (HIGH) + +After 5 retries (1.5s at default `reconnectEveryMs: 300`), `failedToConnect` is set permanently. Subsequent `emit()` calls are silently dropped -- they are not queued and will never be delivered, even if the bus becomes available later. + +If you need events to survive longer startup delays, increase `reconnectEveryMs`: + +```ts +super({ pluginId: 'store-inspector', reconnectEveryMs: 1000 }) +// 5 retries * 1000ms = 5s window +``` + +There is no way to increase the retry count (hardcoded to 5). + +### 5. Expecting connection on construction or on() (HIGH) + +The connection to the event bus is initiated lazily on the first `emit()` call. Calling `on()` alone does not trigger a connection. If your panel calls `on()` but the library side never calls `emit()`, the client never connects to the bus. + +This means if you only listen (no emitting), the `on()` handler still works for events dispatched directly on the global event target, but the connection handshake (`tanstack-connect` / `tanstack-connect-success`) never runs. + +### 6. Using non-serializable payloads (HIGH) + +When the server event bus is enabled, events are serialized via JSON for transport over WebSocket/SSE/BroadcastChannel. Payloads containing functions, DOM nodes, class instances, `Map`/`Set`, or circular references will fail silently or lose data. + +Wrong: + +```ts +storeInspector.emit('state-changed', { + storeName: 'main', + state, + callback: () => {}, // Function -- not serializable + element: document.body, // DOM node -- not serializable +}) +``` + +Correct: + +```ts +storeInspector.emit('state-changed', { + storeName: 'main', + state: JSON.parse(JSON.stringify(state)), // Ensure serializable + timestamp: Date.now(), +}) +``` + +### 7. Not stripping EventClient emit calls for production (HIGH) + +The Vite plugin strips adapter imports (e.g., `@tanstack/react-devtools`) from production builds, but it does NOT strip `@tanstack/devtools-event-client` imports or `emit()` calls. Library authors must guard emit calls themselves. + +Options: + +**Option A:** Use the `enabled` constructor option: + +```ts +super({ + pluginId: 'store-inspector', + enabled: process.env.NODE_ENV !== 'production', +}) +``` + +**Option B:** Conditional guard at the call site: + +```ts +if (process.env.NODE_ENV !== 'production') { + storeInspector.emit('state-changed', data) +} +``` + +When `enabled` is `false`, `emit()` returns immediately (no event creation, no queuing, no connection attempt). This is the preferred approach. + +## See Also + +- `devtools-instrumentation` -- after creating a client, instrument library code with strategic emissions +- `devtools-plugin-panel` -- the client emits events, the panel listens using the same event map +- `devtools-bidirectional` -- two-way communication between panel and application using the same EventClient diff --git a/packages/event-bus-client/skills/devtools-instrumentation/SKILL.md b/packages/event-bus-client/skills/devtools-instrumentation/SKILL.md new file mode 100644 index 00000000..ac105b99 --- /dev/null +++ b/packages/event-bus-client/skills/devtools-instrumentation/SKILL.md @@ -0,0 +1,351 @@ +--- +name: devtools-instrumentation +description: Analyze library codebase for critical architecture and debugging points, add strategic event emissions. Identify middleware boundaries, state transitions, lifecycle hooks. Consolidate events (1 not 15), debounce high-frequency updates, DRY shared payload fields, guard emit() for production. Transparent server/client event bridging. +type: core +library: "@tanstack/devtools-event-client" +library_version: "0.10.12" +requires: devtools-event-client +sources: + - packages/event-bus-client/src/plugin.ts + - packages/event-bus/src/client/client.ts + - packages/event-bus/src/server/server.ts + - packages/devtools-client/src/index.ts + - docs/building-custom-plugins.md + - docs/bidirectional-communication.md +--- + +# devtools-instrumentation + +> **Prerequisite:** Read the `devtools-event-client` skill first for EventClient creation, event maps, and `emit()`/`on()` API. + +Strategic placement of `emit()` calls inside a library to send high-value diagnostic data to TanStack Devtools panels. Maximum insight with minimum noise. + +## Key Insight + +The event bus transparently bridges server/client and cross-tab boundaries. `emit()` on the server arrives on the client via WebSocket/SSE. `emit()` in one tab reaches other tabs via `BroadcastChannel`. No transport code needed -- just emit at the right place. + +For prototyping, throw in many events. For production, consolidate down to the fewest events that carry the most information. + +## Where to Instrument + +Emit at **architecture boundaries**, not inside implementation details: + +1. **Middleware/interceptor entry and exit** -- wrap the chain, not each middleware +2. **State transitions** -- when state moves between logical phases (idle -> loading -> success/error) +3. **Lifecycle hooks** -- mount, unmount, connect, disconnect, ready +4. **Error boundaries** -- caught exceptions, retries, fallbacks +5. **User-initiated actions processed** -- after fully applied, not before + +Do NOT emit from: internal utility functions, loop iterations, getter/setter accesses, intermediate computation steps. + +## Core Patterns + +### 1. Middleware/Interceptor Instrumentation + +Wrap the pipeline at the boundary, not each middleware individually. + +```ts +import { EventClient } from '@tanstack/devtools-event-client' + +type RouterEvents = { + 'request-processed': { + id: string + method: string + path: string + duration: number + middlewareChain: Array<{ name: string; durationMs: number }> + status: number + error?: string + } +} + +class RouterDevtoolsClient extends EventClient<RouterEvents> { + constructor() { + super({ + pluginId: 'my-router', + enabled: process.env.NODE_ENV !== 'production', + }) + } +} + +export const routerDevtools = new RouterDevtoolsClient() +``` + +```ts +async function runMiddlewarePipeline(req: Request, middlewares: Middleware[]): Promise<Response> { + const requestId = crypto.randomUUID() + const pipelineStart = performance.now() + const chain: Array<{ name: string; durationMs: number }> = [] + let status = 200 + let error: string | undefined + + for (const mw of middlewares) { + const mwStart = performance.now() + try { + await mw.handle(req) + } catch (e) { + error = e instanceof Error ? e.message : String(e) + status = 500 + break + } + chain.push({ name: mw.name, durationMs: performance.now() - mwStart }) + } + + // Single consolidated event at the boundary + routerDevtools.emit('request-processed', { + id: requestId, + method: req.method, + path: req.url, + duration: performance.now() - pipelineStart, + middlewareChain: chain, + status, + error, + }) + + return new Response(null, { status }) +} +``` + +ONE event per request, not 2N events (start + end for each middleware). + +### 2. State Transition Emission + +Emit when the state machine moves between phases, not on every internal mutation. + +```ts +type QueryEvents = { + 'query-lifecycle': { + queryKey: string + from: 'idle' | 'loading' | 'success' | 'error' | 'stale' + to: 'idle' | 'loading' | 'success' | 'error' | 'stale' + data?: unknown + error?: string + fetchDuration?: number + timestamp: number + } +} + +class QueryDevtoolsClient extends EventClient<QueryEvents> { + constructor() { + super({ pluginId: 'my-query-lib', enabled: process.env.NODE_ENV !== 'production' }) + } +} + +export const queryDevtools = new QueryDevtoolsClient() +``` + +```ts +class Query { + #state: QueryState = 'idle' + + private transition(to: QueryState, extra?: Partial<QueryEvents['query-lifecycle']>) { + const from = this.#state + if (from === to) return // No transition, no event + this.#state = to + queryDevtools.emit('query-lifecycle', { queryKey: this.key, from, to, timestamp: Date.now(), ...extra }) + } + + async fetch() { + this.transition('loading') + const start = performance.now() + try { + const data = await this.fetcher() + this.transition('success', { data: structuredClone(data), fetchDuration: performance.now() - start }) + } catch (e) { + this.transition('error', { error: e instanceof Error ? e.message : String(e), fetchDuration: performance.now() - start }) + } + } +} +``` + +### 3. Consolidated Events with DRY Payloads + +When multiple events share fields, build a shared base and spread it. + +```ts +class Store { + private basePayload() { + return { storeName: this.#name, version: this.#version, sessionId: this.#sessionId, timestamp: Date.now() } + } + + dispatch(action: string, updater: (s: Record<string, unknown>) => Record<string, unknown>) { + const prevState = structuredClone(this.#state) + this.#state = updater(this.#state) + this.#version++ + storeDevtools.emit('store-updated', { ...this.basePayload(), action, prevState, nextState: structuredClone(this.#state) }) + } + + reset(initial: Record<string, unknown>) { + this.#state = initial + this.#version++ + storeDevtools.emit('store-reset', this.basePayload()) + } +} +``` + +### 4. Debouncing High-Frequency Emissions + +Reactive systems, scroll handlers, and streaming data can trigger hundreds of emissions per second. Debounce or throttle these. + +```ts +function createDebouncedEmitter<TEvents extends Record<string, any>>( + client: EventClient<TEvents>, + delayMs: number, +) { + const timers = new Map<string, ReturnType<typeof setTimeout>>() + return function debouncedEmit<K extends keyof TEvents & string>(event: K, payload: TEvents[K]) { + const existing = timers.get(event) + if (existing) clearTimeout(existing) + timers.set(event, setTimeout(() => { client.emit(event, payload); timers.delete(event) }, delayMs)) + } +} + +const debouncedEmit = createDebouncedEmitter(storeDevtools, 100) +signal.subscribe((value) => { + debouncedEmit('signal-updated', { value, timestamp: Date.now() }) +}) +``` + +For leading+trailing (throttle), use the same pattern with a `lastEmit` timestamp check to emit immediately on the leading edge. + +### 5. Production Guarding + +`enabled: false` is the primary guard -- `emit()` returns immediately with no allocation, no queuing, no connection. + +```ts +class MyLibDevtools extends EventClient<MyEvents> { + constructor() { + super({ pluginId: 'my-lib', enabled: process.env.NODE_ENV !== 'production' }) + } +} +``` + +For expensive payload construction (e.g., `structuredClone` of large state), guard at the call site: + +```ts +if (process.env.NODE_ENV !== 'production') { + myDevtools.emit('state-snapshot', { state: structuredClone(largeState), timestamp: Date.now() }) +} +``` + +**Important:** The Vite plugin strips `@tanstack/react-devtools` from production but does NOT strip `@tanstack/devtools-event-client`. You must guard yourself. + +### 6. Server/Client Transparent Bridging + +The same `emit()` works on server and client: + +- **Client**: dispatches `CustomEvent` on `window` -> `ClientEventBus` -> other tabs via `BroadcastChannel` + server via WebSocket +- **Server**: dispatches on `globalThis.__TANSTACK_EVENT_TARGET__` -> `ServerEventBus` -> all WebSocket/SSE clients + +```ts +// Server-side (e.g., SSR handler) -- arrives in browser devtools panel automatically +routerDevtools.emit('request-processed', { + id: crypto.randomUUID(), + method: req.method, + path: new URL(req.url).pathname, + duration: performance.now() - start, + middlewareChain: chain, + status: 200, +}) +``` + +## Instrumentation Checklist + +1. Map architecture boundaries (middleware chain, state machine, lifecycle hooks, error paths) +2. Design ONE consolidated event per boundary with full context payload +3. Keep event map small (3-7 types typical, not 15-30) +4. Create EventClient with `enabled: process.env.NODE_ENV !== 'production'` +5. Use shared base payloads (DRY) for fields common across events +6. Debounce any emission point that fires >10 times/second +7. Guard expensive payload construction with `process.env.NODE_ENV` check +8. Test with `debug: true` to see `[tanstack-devtools:{pluginId}-plugin]` prefixed logs + +## Common Mistakes + +### HIGH: Emitting too many granular events + +Wrong -- 15 events per request: + +```ts +routerDevtools.emit('request-start', { id, method, path }) +routerDevtools.emit('middleware-1-start', { id, name: 'auth' }) +routerDevtools.emit('middleware-1-end', { id, name: 'auth', duration: 5 }) +// ... 10 more ... +routerDevtools.emit('response-end', { id, duration: 50 }) +``` + +Correct -- 1 event with all data: + +```ts +routerDevtools.emit('request-processed', { + id, method, path, duration: 50, + middlewareChain: [{ name: 'auth', durationMs: 5 }, { name: 'cors', durationMs: 1 }], + status: 200, +}) +``` + +Source: maintainer interview + +### HIGH: Emitting in hot loops without debouncing + +Wrong: + +```ts +signal.subscribe((value) => { + devtools.emit('signal-updated', { value, timestamp: Date.now() }) // 60+ times/sec +}) +``` + +Correct: + +```ts +const debouncedEmit = createDebouncedEmitter(devtools, 100) +signal.subscribe((value) => { + debouncedEmit('signal-updated', { value, timestamp: Date.now() }) +}) +``` + +Source: docs/bidirectional-communication.md + +### MEDIUM: Not emitting at architecture boundaries + +Wrong -- instrumented inside a helper: + +```ts +function parseQueryString(url: string) { + const params = new URLSearchParams(url) + devtools.emit('query-parsed', { params: Object.fromEntries(params) }) + return params +} +``` + +Correct -- instrumented at the handler boundary: + +```ts +function handleRequest(req: Request) { + const params = parseQueryString(req.url) + const result = processRequest(params) + devtools.emit('request-processed', { path: req.url, params: Object.fromEntries(params), result: result.summary, duration: performance.now() - start }) +} +``` + +Source: maintainer interview + +### MEDIUM: Hardcoding repeated payload fields + +Wrong: + +```ts +devtools.emit('action-a', { storeName: this.name, version: this.version, sessionId: this.sessionId, timestamp: Date.now(), data }) +devtools.emit('action-b', { storeName: this.name, version: this.version, sessionId: this.sessionId, timestamp: Date.now(), other }) +``` + +Correct: + +```ts +const base = this.basePayload() +devtools.emit('action-a', { ...base, data }) +devtools.emit('action-b', { ...base, other }) +``` + +Source: maintainer interview diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3aedc10b..f984e52b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@tanstack/eslint-config': specifier: 0.3.2 version: 0.3.2(@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@tanstack/intent': + specifier: ^0.0.14 + version: 0.0.14 '@tanstack/typedoc-config': specifier: 0.2.1 version: 0.2.1(typescript@5.9.3) @@ -216,28 +219,28 @@ importers: version: 4.2.1(vite@7.3.1(@types/node@22.19.13)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/ai': specifier: latest - version: 0.6.1 + version: 0.6.3 '@tanstack/ai-anthropic': specifier: latest - version: 0.6.0(@tanstack/ai@0.6.1)(zod@4.3.6) + version: 0.6.0(@tanstack/ai@0.6.3)(zod@4.3.6) '@tanstack/ai-client': specifier: latest - version: 0.5.1 + version: 0.6.0 '@tanstack/ai-gemini': specifier: latest - version: 0.7.0(@tanstack/ai@0.6.1) + version: 0.8.0(@tanstack/ai@0.6.3) '@tanstack/ai-ollama': specifier: latest - version: 0.6.0(@tanstack/ai@0.6.1) + version: 0.6.0(@tanstack/ai@0.6.3) '@tanstack/ai-openai': specifier: latest - version: 0.6.0(@tanstack/ai@0.6.1)(ws@8.19.0)(zod@4.3.6) + version: 0.6.0(@tanstack/ai@0.6.3)(ws@8.19.0)(zod@4.3.6) '@tanstack/ai-react': specifier: latest - version: 0.6.1(@tanstack/ai@0.6.1)(@types/react@19.2.14)(react@19.2.4) + version: 0.6.4(@tanstack/ai@0.6.3)(@types/react@19.2.14)(react@19.2.4) '@tanstack/react-ai-devtools': specifier: latest - version: 0.2.10(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.29(typescript@5.9.3)) + version: 0.2.13(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.29(typescript@5.9.3)) '@tanstack/react-devtools': specifier: latest version: link:../../../packages/react-devtools @@ -3282,16 +3285,16 @@ packages: '@tanstack/ai': ^0.6.1 zod: ^4.0.0 - '@tanstack/ai-client@0.5.1': - resolution: {integrity: sha512-96Qm8sQYBgfLIUR3f09aaLERsNtg+lpZ1J2jiqFTc8YiL+21Ya2Q1JDU3Opd8nNDIhvjwv1tdNxXAsZnwGKKKQ==} + '@tanstack/ai-client@0.6.0': + resolution: {integrity: sha512-i82147BKAx6Ip4M1Z5+iuwIw8BWDGI41aJpxGcXsUUnXKUuVp0oEkhR3ii44+6GfV9FAdVHXOu7lpMvgBy2mpg==} - '@tanstack/ai-devtools-core@0.3.6': - resolution: {integrity: sha512-SpAtapoc5y0Typvjc5CCKLppUkwof0/L6qA7w9/xp6rPqqzr7K2ckFWe6K3Z+nTt0CwVW6MJKL54f0zHzK7y7w==} + '@tanstack/ai-devtools-core@0.3.9': + resolution: {integrity: sha512-fU6cyp2O4OLVaVd9kkM8FGovl+InAcQbqr1OaNa5uJksmlocwXv9k0rO+zoI9lGtWmxSaVm3NfApDQGTZL93Ew==} - '@tanstack/ai-gemini@0.7.0': - resolution: {integrity: sha512-A2ahYPbQ15bOfn0byE6g6dzCL/pSxhmBBCDjj+Mrwu5xLnhfwkSTkb+GnXCs6QTQOrg3lww1vZjRt251tr6u9A==} + '@tanstack/ai-gemini@0.8.0': + resolution: {integrity: sha512-SjVoue+hWBmo1JlkfWQPSgdLplAmino9gmv6KmZSfE8UFmMaE2AGDGGIXd4W2W1m/WjaK8fr/AHfC7Z0pBJ2Dw==} peerDependencies: - '@tanstack/ai': ^0.6.1 + '@tanstack/ai': ^0.6.2 '@tanstack/ai-ollama@0.6.0': resolution: {integrity: sha512-fWa2PfPZ4xeYdN8PUEoiFx30RUaLhLISOOMfux3MPYKW6lNu+04TDHWC6mKP5JXTCc1wVxpSpeVjBfwbrsnu9w==} @@ -3304,15 +3307,15 @@ packages: '@tanstack/ai': ^0.6.1 zod: ^4.0.0 - '@tanstack/ai-react@0.6.1': - resolution: {integrity: sha512-9cGHe1z5NSV5QpFRV5CX2CBXco393JfVxEl4DbGkO9bHsc4cHOUm3sglYZ01Ebcq/pDVoUFDrkHaBxn511GrxA==} + '@tanstack/ai-react@0.6.4': + resolution: {integrity: sha512-xg87Wf1oE7jNVZfZZWxZi6ela/wdGtct2UxfEt+Z2M/JYA/bOkt3QwcklENwVCmo1TlTSjkjpmXAqB5QfgH0gw==} peerDependencies: - '@tanstack/ai': ^0.6.1 + '@tanstack/ai': ^0.6.3 '@types/react': '>=18.0.0' react: '>=18.0.0' - '@tanstack/ai@0.6.1': - resolution: {integrity: sha512-k+4JrjBm5O1j5ccxErlUVC2IC2rJIqQCK45loLckeLow0cJ5rWdKtG03UwR+9VjppDdL6oO27jyk5CzU6ym+HQ==} + '@tanstack/ai@0.6.3': + resolution: {integrity: sha512-uCuP27PScfJXAKS/laqinlVSFSzPc2RMgXj0d7S7f6aqoIP/ack/0t5HjLdFgRMEOJ1wxbYmUV5Ln94wYg5t8Q==} engines: {node: '>=18'} '@tanstack/devtools-event-client@0.3.5': @@ -3335,8 +3338,8 @@ packages: peerDependencies: solid-js: '>=1.9.7' - '@tanstack/devtools-utils@0.3.1': - resolution: {integrity: sha512-vdcqwQX1a1SbYxjT1HFGbvZySUPIVlIYd8++CEXCMqutDNEDkjKjMJQFAV14zcn83fanBIlUmrN4LXfTMO8GhA==} + '@tanstack/devtools-utils@0.3.2': + resolution: {integrity: sha512-fu9wmE2bHigiE1Lc5RFSchgdN35wX15TqfB4O4vJa6SqX9JH2ov57J60u18lheROaBiteloPzcCbkLNpx0aacw==} engines: {node: '>=18'} peerDependencies: '@types/react': '>=17.0.0' @@ -3381,6 +3384,10 @@ packages: resolution: {integrity: sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww==} engines: {node: '>=20.19'} + '@tanstack/intent@0.0.14': + resolution: {integrity: sha512-83cjen+tYll6z0MIN0JC9GuMljUkfYfHFDOLhmROz4XMmbD/0ZjvKNOsFgpO7eq957XHB/qyK+hCO+3OS/D6HQ==} + hasBin: true + '@tanstack/match-sorter-utils@8.19.4': resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} engines: {node: '>=12'} @@ -3399,8 +3406,8 @@ packages: '@tanstack/query-devtools@5.93.0': resolution: {integrity: sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg==} - '@tanstack/react-ai-devtools@0.2.10': - resolution: {integrity: sha512-nR7HtXldh8b8UX0nUq2va0/WV2Y+5DvSQrxbPyhxUS4M7fmZX9OX/BEnfKXFPcHfD7PZTlO0SG5foWcvJAwxyg==} + '@tanstack/react-ai-devtools@0.2.13': + resolution: {integrity: sha512-2O4pOMfDhVgfKcybt8JRUwYGHyZZuF9Tf4WvOc68RDWPEDqWwMheX9uepjUn6Q/s4vG4cjbo11spzHaU9PiF4A==} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -11083,21 +11090,21 @@ snapshots: tailwindcss: 4.2.1 vite: 7.3.1(@types/node@22.19.13)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - '@tanstack/ai-anthropic@0.6.0(@tanstack/ai@0.6.1)(zod@4.3.6)': + '@tanstack/ai-anthropic@0.6.0(@tanstack/ai@0.6.3)(zod@4.3.6)': dependencies: '@anthropic-ai/sdk': 0.71.2(zod@4.3.6) - '@tanstack/ai': 0.6.1 + '@tanstack/ai': 0.6.3 zod: 4.3.6 - '@tanstack/ai-client@0.5.1': + '@tanstack/ai-client@0.6.0': dependencies: - '@tanstack/ai': 0.6.1 + '@tanstack/ai': 0.6.3 - '@tanstack/ai-devtools-core@0.3.6(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(vue@3.5.29(typescript@5.9.3))': + '@tanstack/ai-devtools-core@0.3.9(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(vue@3.5.29(typescript@5.9.3))': dependencies: - '@tanstack/ai': 0.6.1 - '@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.11) - '@tanstack/devtools-utils': 0.3.1(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.29(typescript@5.9.3)) + '@tanstack/ai': 0.6.3 + '@tanstack/devtools-ui': 0.5.0(csstype@3.2.3)(solid-js@1.9.11) + '@tanstack/devtools-utils': 0.3.2(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.29(typescript@5.9.3)) goober: 2.1.18(csstype@3.2.3) solid-js: 1.9.11 transitivePeerDependencies: @@ -11107,37 +11114,37 @@ snapshots: - react - vue - '@tanstack/ai-gemini@0.7.0(@tanstack/ai@0.6.1)': + '@tanstack/ai-gemini@0.8.0(@tanstack/ai@0.6.3)': dependencies: '@google/genai': 1.43.0 - '@tanstack/ai': 0.6.1 + '@tanstack/ai': 0.6.3 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - bufferutil - supports-color - utf-8-validate - '@tanstack/ai-ollama@0.6.0(@tanstack/ai@0.6.1)': + '@tanstack/ai-ollama@0.6.0(@tanstack/ai@0.6.3)': dependencies: - '@tanstack/ai': 0.6.1 + '@tanstack/ai': 0.6.3 ollama: 0.6.3 - '@tanstack/ai-openai@0.6.0(@tanstack/ai@0.6.1)(ws@8.19.0)(zod@4.3.6)': + '@tanstack/ai-openai@0.6.0(@tanstack/ai@0.6.3)(ws@8.19.0)(zod@4.3.6)': dependencies: - '@tanstack/ai': 0.6.1 + '@tanstack/ai': 0.6.3 openai: 6.25.0(ws@8.19.0)(zod@4.3.6) zod: 4.3.6 transitivePeerDependencies: - ws - '@tanstack/ai-react@0.6.1(@tanstack/ai@0.6.1)(@types/react@19.2.14)(react@19.2.4)': + '@tanstack/ai-react@0.6.4(@tanstack/ai@0.6.3)(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@tanstack/ai': 0.6.1 - '@tanstack/ai-client': 0.5.1 + '@tanstack/ai': 0.6.3 + '@tanstack/ai-client': 0.6.0 '@types/react': 19.2.14 react: 19.2.4 - '@tanstack/ai@0.6.1': + '@tanstack/ai@0.6.3': dependencies: '@tanstack/devtools-event-client': 0.4.1 partial-json: 0.1.7 @@ -11163,7 +11170,7 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-utils@0.3.1(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.29(typescript@5.9.3))': + '@tanstack/devtools-utils@0.3.2(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.29(typescript@5.9.3))': dependencies: '@tanstack/devtools-ui': 0.5.0(csstype@3.2.3)(solid-js@1.9.11) optionalDependencies: @@ -11229,6 +11236,10 @@ snapshots: '@tanstack/history@1.161.4': {} + '@tanstack/intent@0.0.14': + dependencies: + yaml: 2.8.2 + '@tanstack/match-sorter-utils@8.19.4': dependencies: remove-accents: 0.5.0 @@ -11244,10 +11255,10 @@ snapshots: '@tanstack/query-devtools@5.93.0': {} - '@tanstack/react-ai-devtools@0.2.10(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.29(typescript@5.9.3))': + '@tanstack/react-ai-devtools@0.2.13(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.29(typescript@5.9.3))': dependencies: - '@tanstack/ai-devtools-core': 0.3.6(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(vue@3.5.29(typescript@5.9.3)) - '@tanstack/devtools-utils': 0.3.1(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.29(typescript@5.9.3)) + '@tanstack/ai-devtools-core': 0.3.9(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(vue@3.5.29(typescript@5.9.3)) + '@tanstack/devtools-utils': 0.3.2(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.29(typescript@5.9.3)) '@types/react': 19.2.14 react: 19.2.4 transitivePeerDependencies: From 9279aa791befc17cf90ed1179e3cb239707ab0ee Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:52:21 +0000 Subject: [PATCH 2/5] ci: apply automated fixes --- _artifacts/skill_spec.md | 211 +++++++++--------- package.json | 196 ++++++++-------- .../devtools-framework-adapters/SKILL.md | 42 ++-- .../references/preact.md | 52 +++-- .../references/react.md | 34 +-- .../references/solid.md | 26 ++- .../references/vue.md | 36 ++- .../skills/devtools-vite-plugin/SKILL.md | 60 ++--- .../references/vite-options.md | 131 ++++++----- .../skills/devtools-app-setup/SKILL.md | 62 ++--- .../skills/devtools-marketplace/SKILL.md | 22 +- .../skills/devtools-plugin-panel/SKILL.md | 63 ++++-- .../references/panel-api.md | 104 +++++---- .../skills/devtools-production/SKILL.md | 92 +++++--- .../skills/devtools-bidirectional/SKILL.md | 35 +-- .../skills/devtools-event-client/SKILL.md | 30 +-- .../skills/devtools-instrumentation/SKILL.md | 112 ++++++++-- 17 files changed, 761 insertions(+), 547 deletions(-) diff --git a/_artifacts/skill_spec.md b/_artifacts/skill_spec.md index 222ca6db..f00fa7a3 100644 --- a/_artifacts/skill_spec.md +++ b/_artifacts/skill_spec.md @@ -4,149 +4,150 @@ TanStack Devtools is a framework-agnostic toolkit for building custom devtools p ## Domains -| Domain | Description | Skills | -| --- | --- | --- | -| Setting up devtools | Installing, configuring, and mounting devtools in an app | app-setup | -| Building plugins | Creating custom plugins: event clients, panels, marketplace | plugin-panel-development, marketplace-publishing | -| Event communication | Typed event system for plugin data flow | event-client-creation, bidirectional-communication | -| Framework adaptation | Per-framework adapters using factory functions | framework-adapters | -| Build and production | Vite plugin features and production concerns | vite-plugin, production-setup | -| Library instrumentation | Strategic event emission placement in codebases | strategic-instrumentation | +| Domain | Description | Skills | +| ----------------------- | ----------------------------------------------------------- | -------------------------------------------------- | +| Setting up devtools | Installing, configuring, and mounting devtools in an app | app-setup | +| Building plugins | Creating custom plugins: event clients, panels, marketplace | plugin-panel-development, marketplace-publishing | +| Event communication | Typed event system for plugin data flow | event-client-creation, bidirectional-communication | +| Framework adaptation | Per-framework adapters using factory functions | framework-adapters | +| Build and production | Vite plugin features and production concerns | vite-plugin, production-setup | +| Library instrumentation | Strategic event emission placement in codebases | strategic-instrumentation | ## Skill Inventory -| Skill | Type | Domain | What it covers | Failure modes | -| --- | --- | --- | --- | --- | -| app-setup | core | setup | TanStackDevtools component, plugins prop, config, framework adapters | 4 | -| vite-plugin | core | build-production | Source injection, console piping, enhanced logs, production stripping, server bus | 5 | -| event-client-creation | core | event-communication | EventClient class, event maps, connection lifecycle, namespacing | 7 | -| strategic-instrumentation | core | instrumentation | Finding critical emission points, consolidation, performance | 3 | -| plugin-panel-development | core | plugin-development | Panel components, event listening, theming, devtools-ui | 7 | -| framework-adapters | framework | framework-adaptation | createReactPlugin, createSolidPlugin, createVuePlugin, NoOp variants | 4 | -| bidirectional-communication | core | event-communication | Commands, state editing, time-travel, structuredClone | 3 | -| production-setup | lifecycle | build-production | removeDevtoolsOnBuild, conditional imports, NoOp patterns | 3 | -| marketplace-publishing | lifecycle | plugin-development | PluginMetadata, registry format, auto-install config | 3 | +| Skill | Type | Domain | What it covers | Failure modes | +| --------------------------- | --------- | -------------------- | --------------------------------------------------------------------------------- | ------------- | +| app-setup | core | setup | TanStackDevtools component, plugins prop, config, framework adapters | 4 | +| vite-plugin | core | build-production | Source injection, console piping, enhanced logs, production stripping, server bus | 5 | +| event-client-creation | core | event-communication | EventClient class, event maps, connection lifecycle, namespacing | 7 | +| strategic-instrumentation | core | instrumentation | Finding critical emission points, consolidation, performance | 3 | +| plugin-panel-development | core | plugin-development | Panel components, event listening, theming, devtools-ui | 7 | +| framework-adapters | framework | framework-adaptation | createReactPlugin, createSolidPlugin, createVuePlugin, NoOp variants | 4 | +| bidirectional-communication | core | event-communication | Commands, state editing, time-travel, structuredClone | 3 | +| production-setup | lifecycle | build-production | removeDevtoolsOnBuild, conditional imports, NoOp patterns | 3 | +| marketplace-publishing | lifecycle | plugin-development | PluginMetadata, registry format, auto-install config | 3 | ## Failure Mode Inventory ### app-setup (4 failure modes) -| # | Mistake | Priority | Source | Cross-skill? | -| --- | --- | --- | --- | --- | -| 1 | Vite plugin not placed first in plugins array | HIGH | docs/vite-plugin.md | vite-plugin | -| 2 | Vue plugin uses render instead of component | CRITICAL | docs/framework/vue/adapter.md | framework-adapters | -| 3 | Installing as regular dep for dev-only use | MEDIUM | docs/installation.md | — | -| 4 | Mounting TanStackDevtools in SSR without client guard | HIGH | packages/devtools/src/core.ts | — | +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ----------------------------------------------------- | -------- | ----------------------------- | ------------------ | +| 1 | Vite plugin not placed first in plugins array | HIGH | docs/vite-plugin.md | vite-plugin | +| 2 | Vue plugin uses render instead of component | CRITICAL | docs/framework/vue/adapter.md | framework-adapters | +| 3 | Installing as regular dep for dev-only use | MEDIUM | docs/installation.md | — | +| 4 | Mounting TanStackDevtools in SSR without client guard | HIGH | packages/devtools/src/core.ts | — | ### vite-plugin (5 failure modes) -| # | Mistake | Priority | Source | Cross-skill? | -| --- | --- | --- | --- | --- | -| 1 | Expecting Vite plugin features in production | MEDIUM | packages/devtools-vite/src/plugin.ts | — | -| 2 | Not placing devtools() first in Vite plugins | HIGH | docs/vite-plugin.md | app-setup | -| 3 | Source injection on spread props elements | MEDIUM | packages/devtools-vite/src/inject-source.ts | — | -| 4 | Using devtools-vite with non-Vite bundlers | HIGH | packages/devtools-vite/package.json | — | -| 5 | Event bus port conflict in multi-project setups | MEDIUM | packages/event-bus/src/server/server.ts | — | +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ----------------------------------------------- | -------- | ------------------------------------------- | ------------ | +| 1 | Expecting Vite plugin features in production | MEDIUM | packages/devtools-vite/src/plugin.ts | — | +| 2 | Not placing devtools() first in Vite plugins | HIGH | docs/vite-plugin.md | app-setup | +| 3 | Source injection on spread props elements | MEDIUM | packages/devtools-vite/src/inject-source.ts | — | +| 4 | Using devtools-vite with non-Vite bundlers | HIGH | packages/devtools-vite/package.json | — | +| 5 | Event bus port conflict in multi-project setups | MEDIUM | packages/event-bus/src/server/server.ts | — | ### event-client-creation (7 failure modes) -| # | Mistake | Priority | Source | Cross-skill? | -| --- | --- | --- | --- | --- | -| 1 | Including pluginId prefix in event names | CRITICAL | packages/event-bus-client/src/plugin.ts | — | -| 2 | Creating multiple EventClient instances per plugin | CRITICAL | docs/building-custom-plugins.md | — | -| 3 | Non-unique pluginId causing event collisions | CRITICAL | maintainer interview | — | -| 4 | Not realizing events drop after 5 failed retries | HIGH | packages/event-bus-client/src/plugin.ts | — | -| 5 | Listening before emitting and expecting connection | HIGH | packages/event-bus-client/src/plugin.ts | — | -| 6 | Using non-serializable payloads | HIGH | packages/event-bus/src/utils/json.ts | bidirectional-communication | -| 7 | Not stripping EventClient emit calls for production | HIGH | maintainer interview | production-setup | +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --------------------------------------------------- | -------- | --------------------------------------- | --------------------------- | +| 1 | Including pluginId prefix in event names | CRITICAL | packages/event-bus-client/src/plugin.ts | — | +| 2 | Creating multiple EventClient instances per plugin | CRITICAL | docs/building-custom-plugins.md | — | +| 3 | Non-unique pluginId causing event collisions | CRITICAL | maintainer interview | — | +| 4 | Not realizing events drop after 5 failed retries | HIGH | packages/event-bus-client/src/plugin.ts | — | +| 5 | Listening before emitting and expecting connection | HIGH | packages/event-bus-client/src/plugin.ts | — | +| 6 | Using non-serializable payloads | HIGH | packages/event-bus/src/utils/json.ts | bidirectional-communication | +| 7 | Not stripping EventClient emit calls for production | HIGH | maintainer interview | production-setup | ### strategic-instrumentation (3 failure modes) -| # | Mistake | Priority | Source | Cross-skill? | -| --- | --- | --- | --- | --- | -| 1 | Emitting too many granular events | HIGH | maintainer interview | — | -| 2 | Emitting in hot loops without debouncing | HIGH | docs/bidirectional-communication.md | — | -| 3 | Not emitting at architecture boundaries | MEDIUM | maintainer interview | — | +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ---------------------------------------- | -------- | ----------------------------------- | ------------ | +| 1 | Emitting too many granular events | HIGH | maintainer interview | — | +| 2 | Emitting in hot loops without debouncing | HIGH | docs/bidirectional-communication.md | — | +| 3 | Not emitting at architecture boundaries | MEDIUM | maintainer interview | — | ### plugin-panel-development (7 failure modes) -| # | Mistake | Priority | Source | Cross-skill? | -| --- | --- | --- | --- | --- | -| 1 | Not cleaning up event listeners | CRITICAL | docs/building-custom-plugins.md | — | -| 2 | Oversubscribing to events in multiple places | HIGH | maintainer interview | — | -| 3 | Hardcoding repeated event payload fields | MEDIUM | maintainer interview | strategic-instrumentation | -| 4 | Ignoring theme prop in panel component | MEDIUM | docs/plugin-lifecycle.md | — | -| 5 | Not knowing max 3 active plugins limit | MEDIUM | packages/devtools/src/utils/get-default-active-plugins.ts | — | -| 6 | Using raw DOM manipulation instead of framework portals | MEDIUM | docs/plugin-lifecycle.md | — | -| 7 | Not keeping devtools packages at latest versions | MEDIUM | maintainer interview | app-setup | +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ------------------------------------------------------- | -------- | --------------------------------------------------------- | ------------------------- | +| 1 | Not cleaning up event listeners | CRITICAL | docs/building-custom-plugins.md | — | +| 2 | Oversubscribing to events in multiple places | HIGH | maintainer interview | — | +| 3 | Hardcoding repeated event payload fields | MEDIUM | maintainer interview | strategic-instrumentation | +| 4 | Ignoring theme prop in panel component | MEDIUM | docs/plugin-lifecycle.md | — | +| 5 | Not knowing max 3 active plugins limit | MEDIUM | packages/devtools/src/utils/get-default-active-plugins.ts | — | +| 6 | Using raw DOM manipulation instead of framework portals | MEDIUM | docs/plugin-lifecycle.md | — | +| 7 | Not keeping devtools packages at latest versions | MEDIUM | maintainer interview | app-setup | ### framework-adapters (4 failure modes) -| # | Mistake | Priority | Source | Cross-skill? | -| --- | --- | --- | --- | --- | -| 1 | Using React JSX pattern in Vue adapter | CRITICAL | packages/devtools-utils/src/vue/plugin.ts | app-setup | -| 2 | Solid render prop not wrapped in function | CRITICAL | docs/framework/solid/adapter.md | — | -| 3 | Ignoring NoOp variant for production | HIGH | docs/devtools-utils.md | production-setup | -| 4 | Not passing theme prop to panel component | MEDIUM | packages/devtools-utils/src/react/plugin.tsx | — | +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ----------------------------------------- | -------- | -------------------------------------------- | ---------------- | +| 1 | Using React JSX pattern in Vue adapter | CRITICAL | packages/devtools-utils/src/vue/plugin.ts | app-setup | +| 2 | Solid render prop not wrapped in function | CRITICAL | docs/framework/solid/adapter.md | — | +| 3 | Ignoring NoOp variant for production | HIGH | docs/devtools-utils.md | production-setup | +| 4 | Not passing theme prop to panel component | MEDIUM | packages/devtools-utils/src/react/plugin.tsx | — | ### bidirectional-communication (3 failure modes) -| # | Mistake | Priority | Source | Cross-skill? | -| --- | --- | --- | --- | --- | -| 1 | Not using structuredClone for snapshots | HIGH | docs/bidirectional-communication.md | — | -| 2 | Not distinguishing observation from command events | MEDIUM | docs/bidirectional-communication.md | — | -| 3 | Non-serializable payloads in cross-tab scenarios | HIGH | packages/event-bus/src/utils/json.ts | event-client-creation | +| # | Mistake | Priority | Source | Cross-skill? | +| --- | -------------------------------------------------- | -------- | ------------------------------------ | --------------------- | +| 1 | Not using structuredClone for snapshots | HIGH | docs/bidirectional-communication.md | — | +| 2 | Not distinguishing observation from command events | MEDIUM | docs/bidirectional-communication.md | — | +| 3 | Non-serializable payloads in cross-tab scenarios | HIGH | packages/event-bus/src/utils/json.ts | event-client-creation | ### production-setup (3 failure modes) -| # | Mistake | Priority | Source | Cross-skill? | -| --- | --- | --- | --- | --- | -| 1 | Keeping devtools in prod without disabling stripping | HIGH | docs/production.md | — | -| 2 | Not using /production sub-export | MEDIUM | docs/production.md | — | -| 3 | Non-Vite projects not excluding devtools manually | HIGH | docs/production.md | — | +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ---------------------------------------------------- | -------- | ------------------ | ------------ | +| 1 | Keeping devtools in prod without disabling stripping | HIGH | docs/production.md | — | +| 2 | Not using /production sub-export | MEDIUM | docs/production.md | — | +| 3 | Non-Vite projects not excluding devtools manually | HIGH | docs/production.md | — | ### marketplace-publishing (3 failure modes) -| # | Mistake | Priority | Source | Cross-skill? | -| --- | --- | --- | --- | --- | -| 1 | Missing pluginImport metadata for auto-install | HIGH | docs/third-party-plugins.md | — | -| 2 | Not specifying requires.minVersion | MEDIUM | docs/third-party-plugins.md | — | -| 3 | Submitting without framework field | MEDIUM | docs/third-party-plugins.md | — | +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ---------------------------------------------- | -------- | --------------------------- | ------------ | +| 1 | Missing pluginImport metadata for auto-install | HIGH | docs/third-party-plugins.md | — | +| 2 | Not specifying requires.minVersion | MEDIUM | docs/third-party-plugins.md | — | +| 3 | Submitting without framework field | MEDIUM | docs/third-party-plugins.md | — | ## Tensions -| Tension | Skills | Agent implication | -| --- | --- | --- | -| Instrumentation completeness vs performance | strategic-instrumentation ↔ event-client-creation | Agent optimizing for debugging coverage emits in hot paths; agent optimizing for performance skips critical points | -| Development convenience vs production safety | app-setup ↔ production-setup | Agent installs as devDep with stripping; later production usage requires undoing those decisions | -| Framework-agnostic core vs framework ergonomics | framework-adapters ↔ plugin-panel-development | Agent trained on React uses render/JSX everywhere; Vue and Solid have different plugin definitions | +| Tension | Skills | Agent implication | +| ----------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Instrumentation completeness vs performance | strategic-instrumentation ↔ event-client-creation | Agent optimizing for debugging coverage emits in hot paths; agent optimizing for performance skips critical points | +| Development convenience vs production safety | app-setup ↔ production-setup | Agent installs as devDep with stripping; later production usage requires undoing those decisions | +| Framework-agnostic core vs framework ergonomics | framework-adapters ↔ plugin-panel-development | Agent trained on React uses render/JSX everywhere; Vue and Solid have different plugin definitions | ## Cross-References -| From | To | Reason | -| --- | --- | --- | -| app-setup | vite-plugin | Vite plugin adds enhanced features after basic setup | -| event-client-creation | strategic-instrumentation | Understanding event system informs where to emit | -| event-client-creation | plugin-panel-development | Client emits, panel listens — same event map | -| plugin-panel-development | framework-adapters | After building panel, wrap in framework adapters | -| framework-adapters | production-setup | NoOp variants are the primary tree-shaking mechanism | -| bidirectional-communication | event-client-creation | Bidirectional extends the base event system | -| vite-plugin | production-setup | Vite plugin handles production stripping defaults | -| plugin-panel-development | marketplace-publishing | After building plugin, publish to marketplace | -| strategic-instrumentation | bidirectional-communication | Emission points benefit from bidirectional patterns | +| From | To | Reason | +| --------------------------- | --------------------------- | ---------------------------------------------------- | +| app-setup | vite-plugin | Vite plugin adds enhanced features after basic setup | +| event-client-creation | strategic-instrumentation | Understanding event system informs where to emit | +| event-client-creation | plugin-panel-development | Client emits, panel listens — same event map | +| plugin-panel-development | framework-adapters | After building panel, wrap in framework adapters | +| framework-adapters | production-setup | NoOp variants are the primary tree-shaking mechanism | +| bidirectional-communication | event-client-creation | Bidirectional extends the base event system | +| vite-plugin | production-setup | Vite plugin handles production stripping defaults | +| plugin-panel-development | marketplace-publishing | After building plugin, publish to marketplace | +| strategic-instrumentation | bidirectional-communication | Emission points benefit from bidirectional patterns | ## Subsystems & Reference Candidates -| Skill | Subsystems | Reference candidates | -| --- | --- | --- | -| vite-plugin | Source injection, Console piping, Enhanced logging, Production stripping, Server event bus, Editor integration | Vite plugin options reference | -| framework-adapters | React adapter, Solid adapter, Vue adapter, Preact adapter | — | -| app-setup | — | Config options (position, hotkeys, theme, eventBus) | -| event-client-creation | — | EventClient constructor options, connection lifecycle states | +| Skill | Subsystems | Reference candidates | +| --------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| vite-plugin | Source injection, Console piping, Enhanced logging, Production stripping, Server event bus, Editor integration | Vite plugin options reference | +| framework-adapters | React adapter, Solid adapter, Vue adapter, Preact adapter | — | +| app-setup | — | Config options (position, hotkeys, theme, eventBus) | +| event-client-creation | — | EventClient constructor options, connection lifecycle states | ## Remaining Gaps All gaps resolved during maintainer interview: + - PiP window mode has zero impact on plugin development - No formal testing patterns exist; EventClient works standalone for same-page communication - No performance benchmarks; guidance is to prototype freely, reduce for production @@ -162,10 +163,10 @@ All gaps resolved during maintainer interview: ## Composition Opportunities -| Library | Integration points | Composition skill needed? | -| --- | --- | --- | -| TanStack Query | Query devtools panel plugin | No — TanStack Query ships its own devtools panel | -| TanStack Router | Router devtools panel plugin | No — TanStack Router ships its own devtools panel | -| TanStack Form | Form devtools panel plugin | No — TanStack Form ships its own devtools panel | -| Vite | Build tooling integration | No — covered by vite-plugin skill | -| Any state/data library | EventClient instrumentation | Yes — strategic-instrumentation skill | +| Library | Integration points | Composition skill needed? | +| ---------------------- | ---------------------------- | ------------------------------------------------- | +| TanStack Query | Query devtools panel plugin | No — TanStack Query ships its own devtools panel | +| TanStack Router | Router devtools panel plugin | No — TanStack Router ships its own devtools panel | +| TanStack Form | Form devtools panel plugin | No — TanStack Form ships its own devtools panel | +| Vite | Build tooling integration | No — covered by vite-plugin skill | +| Any state/data library | EventClient instrumentation | Yes — strategic-instrumentation skill | diff --git a/package.json b/package.json index d3c5ff3d..e3baca1d 100644 --- a/package.json +++ b/package.json @@ -1,100 +1,100 @@ { - "name": "root", - "private": true, - "repository": { - "type": "git", - "url": "git+https://github.com/TanStack/devtools.git" - }, - "packageManager": "pnpm@10.24.0", - "type": "module", - "scripts": { - "build": "nx affected --targets=build --exclude=examples/** && size-limit", - "build:all": "nx run-many --targets=build --exclude=examples/** && size-limit", - "build:core": "nx build @tanstack/devtools && size-limit", - "changeset": "changeset", - "changeset:publish": "changeset publish", - "changeset:version": "changeset version && pnpm install --no-frozen-lockfile && pnpm format", - "clean": "find . -name 'dist' -type d -prune -exec rm -rf {} +", - "clean:node_modules": "find . -name 'node_modules' -type d -prune -exec rm -rf {} +", - "clean:all": "pnpm run clean && pnpm run clean:node_modules", - "dev": "pnpm run watch", - "format": "prettier --experimental-cli --ignore-unknown '**/*' --write", - "generate-docs": "node scripts/generate-docs.ts", - "lint:fix": "nx affected --target=lint:fix --exclude=examples/**", - "lint:fix:all": "pnpm run format && nx run-many --targets=lint --fix", - "preinstall": "node -e \"if(process.env.CI == 'true') {console.log('Skipping preinstall...'); process.exit(1)}\" || npx -y only-allow pnpm", - "size": "size-limit", - "test": "pnpm run test:ci", - "test:build": "nx affected --target=test:build --exclude=examples/**", - "test:ci": "nx run-many --targets=test:eslint,test:sherif,test:knip,test:lib,test:types,test:build,build", - "test:docs": "node scripts/verify-links.ts", - "test:eslint": "nx affected --target=test:eslint --exclude=examples/**", - "test:knip": "knip", - "test:lib": "nx affected --targets=test:lib --exclude=examples/**", - "test:lib:dev": "pnpm test:lib && nx watch --all -- pnpm test:lib", - "test:pr": "nx affected --targets=test:eslint,test:sherif,test:knip,test:lib,test:types,test:build,build", - "test:sherif": "sherif", - "test:types": "nx affected --targets=test:types --exclude=examples/**", - "watch": "pnpm run build:all && nx watch --all -- pnpm run build:all" - }, - "nx": { - "includedScripts": [ - "test:docs", - "test:knip", - "test:sherif" - ] - }, - "size-limit": [ - { - "path": "packages/devtools/dist/index.js", - "limit": "60 KB" - }, - { - "path": "packages/event-bus-client/dist/esm/plugin.js", - "limit": "1.2 KB" - } - ], - "devDependencies": { - "@changesets/cli": "^2.29.7", - "@faker-js/faker": "^9.9.0", - "@size-limit/preset-small-lib": "^11.2.0", - "@svitejs/changesets-changelog-github-compact": "^1.2.0", - "@tanstack/eslint-config": "0.3.2", - "@tanstack/intent": "^0.0.14", - "@tanstack/typedoc-config": "0.2.1", - "@tanstack/vite-config": "0.2.1", - "@testing-library/jest-dom": "^6.8.0", - "@types/node": "^22.15.2", - "eslint": "^9.36.0", - "eslint-plugin-unused-imports": "^4.2.0", - "jsdom": "^27.0.0", - "knip": "^5.64.0", - "markdown-link-extractor": "^4.0.2", - "nx": "22.1.3", - "premove": "^4.0.0", - "prettier": "^3.8.1", - "prettier-plugin-svelte": "^3.4.1", - "publint": "^0.3.13", - "sherif": "^1.7.0", - "size-limit": "^11.2.0", - "tinyglobby": "^0.2.15", - "typescript": "~5.9.2", - "vite": "^7.1.7", - "vitest": "^3.2.4" - }, - "overrides": { - "@tanstack/devtools": "workspace:*", - "@tanstack/react-devtools": "workspace:*", - "@tanstack/preact-devtools": "workspace:*", - "@tanstack/solid-devtools": "workspace:*", - "@tanstack/devtools-vite": "workspace:*" - }, - "files": [ - "skills", - "bin", - "!skills/_artifacts" - ], - "bin": { - "intent": "./bin/intent.js" - } + "name": "root", + "private": true, + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/devtools.git" + }, + "packageManager": "pnpm@10.24.0", + "type": "module", + "scripts": { + "build": "nx affected --targets=build --exclude=examples/** && size-limit", + "build:all": "nx run-many --targets=build --exclude=examples/** && size-limit", + "build:core": "nx build @tanstack/devtools && size-limit", + "changeset": "changeset", + "changeset:publish": "changeset publish", + "changeset:version": "changeset version && pnpm install --no-frozen-lockfile && pnpm format", + "clean": "find . -name 'dist' -type d -prune -exec rm -rf {} +", + "clean:node_modules": "find . -name 'node_modules' -type d -prune -exec rm -rf {} +", + "clean:all": "pnpm run clean && pnpm run clean:node_modules", + "dev": "pnpm run watch", + "format": "prettier --experimental-cli --ignore-unknown '**/*' --write", + "generate-docs": "node scripts/generate-docs.ts", + "lint:fix": "nx affected --target=lint:fix --exclude=examples/**", + "lint:fix:all": "pnpm run format && nx run-many --targets=lint --fix", + "preinstall": "node -e \"if(process.env.CI == 'true') {console.log('Skipping preinstall...'); process.exit(1)}\" || npx -y only-allow pnpm", + "size": "size-limit", + "test": "pnpm run test:ci", + "test:build": "nx affected --target=test:build --exclude=examples/**", + "test:ci": "nx run-many --targets=test:eslint,test:sherif,test:knip,test:lib,test:types,test:build,build", + "test:docs": "node scripts/verify-links.ts", + "test:eslint": "nx affected --target=test:eslint --exclude=examples/**", + "test:knip": "knip", + "test:lib": "nx affected --targets=test:lib --exclude=examples/**", + "test:lib:dev": "pnpm test:lib && nx watch --all -- pnpm test:lib", + "test:pr": "nx affected --targets=test:eslint,test:sherif,test:knip,test:lib,test:types,test:build,build", + "test:sherif": "sherif", + "test:types": "nx affected --targets=test:types --exclude=examples/**", + "watch": "pnpm run build:all && nx watch --all -- pnpm run build:all" + }, + "nx": { + "includedScripts": [ + "test:docs", + "test:knip", + "test:sherif" + ] + }, + "size-limit": [ + { + "path": "packages/devtools/dist/index.js", + "limit": "60 KB" + }, + { + "path": "packages/event-bus-client/dist/esm/plugin.js", + "limit": "1.2 KB" + } + ], + "devDependencies": { + "@changesets/cli": "^2.29.7", + "@faker-js/faker": "^9.9.0", + "@size-limit/preset-small-lib": "^11.2.0", + "@svitejs/changesets-changelog-github-compact": "^1.2.0", + "@tanstack/eslint-config": "0.3.2", + "@tanstack/intent": "^0.0.14", + "@tanstack/typedoc-config": "0.2.1", + "@tanstack/vite-config": "0.2.1", + "@testing-library/jest-dom": "^6.8.0", + "@types/node": "^22.15.2", + "eslint": "^9.36.0", + "eslint-plugin-unused-imports": "^4.2.0", + "jsdom": "^27.0.0", + "knip": "^5.64.0", + "markdown-link-extractor": "^4.0.2", + "nx": "22.1.3", + "premove": "^4.0.0", + "prettier": "^3.8.1", + "prettier-plugin-svelte": "^3.4.1", + "publint": "^0.3.13", + "sherif": "^1.7.0", + "size-limit": "^11.2.0", + "tinyglobby": "^0.2.15", + "typescript": "~5.9.2", + "vite": "^7.1.7", + "vitest": "^3.2.4" + }, + "overrides": { + "@tanstack/devtools": "workspace:*", + "@tanstack/react-devtools": "workspace:*", + "@tanstack/preact-devtools": "workspace:*", + "@tanstack/solid-devtools": "workspace:*", + "@tanstack/devtools-vite": "workspace:*" + }, + "files": [ + "skills", + "bin", + "!skills/_artifacts" + ], + "bin": { + "intent": "./bin/intent.js" + } } diff --git a/packages/devtools-utils/skills/devtools-framework-adapters/SKILL.md b/packages/devtools-utils/skills/devtools-framework-adapters/SKILL.md index 3ff62570..101f6bbb 100644 --- a/packages/devtools-utils/skills/devtools-framework-adapters/SKILL.md +++ b/packages/devtools-utils/skills/devtools-framework-adapters/SKILL.md @@ -8,15 +8,15 @@ description: > Vue uses (name, component) not options object. Solid render must be function. type: framework library: tanstack-devtools -library_version: "0.10.12" +library_version: '0.10.12' requires: - tanstack-devtools/plugin-panel sources: - - "TanStack/devtools:docs/devtools-utils.md" - - "TanStack/devtools:packages/devtools-utils/src/react/plugin.tsx" - - "TanStack/devtools:packages/devtools-utils/src/vue/plugin.ts" - - "TanStack/devtools:packages/devtools-utils/src/solid/plugin.tsx" - - "TanStack/devtools:packages/devtools-utils/src/preact/plugin.tsx" + - 'TanStack/devtools:docs/devtools-utils.md' + - 'TanStack/devtools:packages/devtools-utils/src/react/plugin.tsx' + - 'TanStack/devtools:packages/devtools-utils/src/vue/plugin.ts' + - 'TanStack/devtools:packages/devtools-utils/src/solid/plugin.tsx' + - 'TanStack/devtools:packages/devtools-utils/src/preact/plugin.tsx' --- Use `@tanstack/devtools-utils` factory functions to create per-framework devtools plugin adapters. Each framework has a subpath export (`/react`, `/vue`, `/solid`, `/preact`) with two factories: @@ -43,12 +43,14 @@ All four frameworks follow the same two-factory pattern: ### Plugin Factory Every `createXPlugin` returns `readonly [Plugin, NoOpPlugin]`: + - **Plugin** -- returns a plugin object with metadata and a `render` function that renders your component. - **NoOpPlugin** -- returns a plugin object with the same metadata but renders an empty fragment. ### Panel Factory Every `createXPanel` returns `readonly [Panel, NoOpPanel]`: + - **Panel** -- a framework component that creates a `<div style="height:100%">`, instantiates the core class, calls `core.mount(el, theme)` on mount, and `core.unmount()` on cleanup. - **NoOpPanel** -- renders an empty fragment. @@ -67,6 +69,7 @@ interface DevtoolsPanelProps { ``` Import from the framework subpath: + ```ts import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/react' import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/vue' @@ -91,17 +94,25 @@ const [MyPlugin, NoOpPlugin] = createReactPlugin({ }) // Tree-shaking: use NoOp in production -const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +const ActivePlugin = + process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin ``` ### With Class-Based Panel ```tsx -import { createReactPanel, createReactPlugin } from '@tanstack/devtools-utils/react' +import { + createReactPanel, + createReactPlugin, +} from '@tanstack/devtools-utils/react' class MyDevtoolsCore { - mount(el: HTMLElement, theme: 'light' | 'dark') { /* render into el */ } - unmount() { /* cleanup */ } + mount(el: HTMLElement, theme: 'light' | 'dark') { + /* render into el */ + } + unmount() { + /* cleanup */ + } } const [MyPanel, NoOpPanel] = createReactPanel(MyDevtoolsCore) @@ -165,6 +176,7 @@ const [MyPlugin, NoOpPlugin] = createVuePlugin('My Plugin', MyPanel) ``` Vue plugins also work differently at call time -- you pass props: + ```ts // WRONG -- calling Plugin() with no args (React pattern) const plugin = MyPlugin() @@ -181,7 +193,7 @@ When using Solid components, `Component` must be a function reference, not raw J // WRONG -- evaluates immediately, breaks Solid reactivity createSolidPlugin({ name: 'My Store', - Component: <MyPanel />, // This is JSX.Element, not a component function + Component: <MyPanel />, // This is JSX.Element, not a component function }) // CORRECT -- pass the component function itself @@ -210,7 +222,8 @@ const [MyPlugin, NoOpPlugin] = createReactPlugin({ name: 'Store', Component: MyPanel, }) -const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +const ActivePlugin = + process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin ``` ### MEDIUM: Not Passing Theme Prop to Panel Component @@ -223,15 +236,14 @@ const Component = () => <div>My Panel</div> // CORRECT -- use theme for styling const Component = ({ theme }: DevtoolsPanelProps) => ( - <div className={theme === 'dark' ? 'dark-mode' : 'light-mode'}> - My Panel - </div> + <div className={theme === 'dark' ? 'dark-mode' : 'light-mode'}>My Panel</div> ) ``` ## Design Tension The core architecture is framework-agnostic, but each framework has different idioms: + - React/Preact use an options object with `Component` as a JSX function component. - Vue uses positional arguments with a `DefineComponent` and passes props through. - Solid uses the same options API as React but with Solid's JSX and reactivity model. diff --git a/packages/devtools-utils/skills/devtools-framework-adapters/references/preact.md b/packages/devtools-utils/skills/devtools-framework-adapters/references/preact.md index 6063c377..de20832f 100644 --- a/packages/devtools-utils/skills/devtools-framework-adapters/references/preact.md +++ b/packages/devtools-utils/skills/devtools-framework-adapters/references/preact.md @@ -3,7 +3,10 @@ ## Import ```ts -import { createPreactPlugin, createPreactPanel } from '@tanstack/devtools-utils/preact' +import { + createPreactPlugin, + createPreactPanel, +} from '@tanstack/devtools-utils/preact' import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/preact' ``` @@ -32,12 +35,12 @@ function createPreactPlugin(options: { ### Parameters -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `name` | `string` | Yes | Display name shown in the devtools tab | -| `id` | `string` | No | Unique identifier for the plugin | -| `defaultOpen` | `boolean` | No | Whether the plugin panel starts open | -| `Component` | `(props: DevtoolsPanelProps) => JSX.Element` | Yes | Preact component to render in the panel | +| Parameter | Type | Required | Description | +| ------------- | -------------------------------------------- | -------- | --------------------------------------- | +| `name` | `string` | Yes | Display name shown in the devtools tab | +| `id` | `string` | No | Unique identifier for the plugin | +| `defaultOpen` | `boolean` | No | Whether the plugin panel starts open | +| `Component` | `(props: DevtoolsPanelProps) => JSX.Element` | Yes | Preact component to render in the panel | ### Return Value @@ -121,7 +124,8 @@ const [MyPlugin, NoOpPlugin] = createPreactPlugin({ Component: MyStorePanel, }) -const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +const ActivePlugin = + process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin ``` ## createPreactPanel @@ -142,9 +146,9 @@ function createPreactPanel< ### Parameters -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `CoreClass` | `new () => { mount(el, theme): void; unmount(): void }` | Yes | Class constructor for the devtools core | +| Parameter | Type | Required | Description | +| ----------- | ------------------------------------------------------- | -------- | --------------------------------------- | +| `CoreClass` | `new () => { mount(el, theme): void; unmount(): void }` | Yes | Class constructor for the devtools core | ### Return Value @@ -199,7 +203,10 @@ export function createPreactPanel< ### Usage ```tsx -import { createPreactPanel, createPreactPlugin } from '@tanstack/devtools-utils/preact' +import { + createPreactPanel, + createPreactPlugin, +} from '@tanstack/devtools-utils/preact' class MyDevtoolsCore { mount(el: HTMLElement, theme: 'light' | 'dark') { @@ -224,7 +231,8 @@ const [MyPlugin, NoOpPlugin] = createPreactPlugin({ }) // Step 3: Conditional for production -const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +const ActivePlugin = + process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin ``` ## Preact-Specific Notes @@ -241,14 +249,14 @@ const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlu ## Comparison with React -| Aspect | React | Preact | -|--------|-------|--------| -| Import path | `@tanstack/devtools-utils/react` | `@tanstack/devtools-utils/preact` | -| JSX types | `react` JSX | `preact` JSX | -| Hooks import | `react` | `preact/hooks` | -| API shape | Identical | Identical | -| `createXPlugin` signature | Same | Same | -| `createXPanel` signature | Same | Same | -| `DevtoolsPanelProps` | Same | Same | +| Aspect | React | Preact | +| ------------------------- | -------------------------------- | --------------------------------- | +| Import path | `@tanstack/devtools-utils/react` | `@tanstack/devtools-utils/preact` | +| JSX types | `react` JSX | `preact` JSX | +| Hooks import | `react` | `preact/hooks` | +| API shape | Identical | Identical | +| `createXPlugin` signature | Same | Same | +| `createXPanel` signature | Same | Same | +| `DevtoolsPanelProps` | Same | Same | If you have working React adapter code, converting to Preact is a matter of changing the import paths. diff --git a/packages/devtools-utils/skills/devtools-framework-adapters/references/react.md b/packages/devtools-utils/skills/devtools-framework-adapters/references/react.md index 5255da97..ecff3975 100644 --- a/packages/devtools-utils/skills/devtools-framework-adapters/references/react.md +++ b/packages/devtools-utils/skills/devtools-framework-adapters/references/react.md @@ -3,7 +3,10 @@ ## Import ```ts -import { createReactPlugin, createReactPanel } from '@tanstack/devtools-utils/react' +import { + createReactPlugin, + createReactPanel, +} from '@tanstack/devtools-utils/react' import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/react' ``` @@ -32,12 +35,12 @@ function createReactPlugin(options: { ### Parameters -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `name` | `string` | Yes | Display name shown in the devtools tab | -| `id` | `string` | No | Unique identifier for the plugin | -| `defaultOpen` | `boolean` | No | Whether the plugin panel starts open | -| `Component` | `(props: DevtoolsPanelProps) => JSX.Element` | Yes | React component to render in the panel | +| Parameter | Type | Required | Description | +| ------------- | -------------------------------------------- | -------- | -------------------------------------- | +| `name` | `string` | Yes | Display name shown in the devtools tab | +| `id` | `string` | No | Unique identifier for the plugin | +| `defaultOpen` | `boolean` | No | Whether the plugin panel starts open | +| `Component` | `(props: DevtoolsPanelProps) => JSX.Element` | Yes | React component to render in the panel | ### Return Value @@ -117,7 +120,8 @@ const [MyPlugin, NoOpPlugin] = createReactPlugin({ Component: MyStorePanel, }) -const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +const ActivePlugin = + process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin ``` ## createReactPanel @@ -138,9 +142,9 @@ function createReactPanel< ### Parameters -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `CoreClass` | `new () => { mount(el, theme): void; unmount(): void }` | Yes | Class constructor for the devtools core | +| Parameter | Type | Required | Description | +| ----------- | ------------------------------------------------------- | -------- | --------------------------------------- | +| `CoreClass` | `new () => { mount(el, theme): void; unmount(): void }` | Yes | Class constructor for the devtools core | ### Return Value @@ -196,7 +200,10 @@ export function createReactPanel< #### Composing Panel + Plugin ```tsx -import { createReactPanel, createReactPlugin } from '@tanstack/devtools-utils/react' +import { + createReactPanel, + createReactPlugin, +} from '@tanstack/devtools-utils/react' class MyDevtoolsCore { mount(el: HTMLElement, theme: 'light' | 'dark') { @@ -221,7 +228,8 @@ const [MyPlugin, NoOpPlugin] = createReactPlugin({ }) // Step 3: Use conditionally for production -const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin +const ActivePlugin = + process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlugin ``` ## React-Specific Gotchas diff --git a/packages/devtools-utils/skills/devtools-framework-adapters/references/solid.md b/packages/devtools-utils/skills/devtools-framework-adapters/references/solid.md index 55a6de69..41c987ff 100644 --- a/packages/devtools-utils/skills/devtools-framework-adapters/references/solid.md +++ b/packages/devtools-utils/skills/devtools-framework-adapters/references/solid.md @@ -3,7 +3,10 @@ ## Import ```ts -import { createSolidPlugin, createSolidPanel } from '@tanstack/devtools-utils/solid' +import { + createSolidPlugin, + createSolidPanel, +} from '@tanstack/devtools-utils/solid' import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid' // For class-based lazy loading (separate subpath) @@ -35,12 +38,12 @@ function createSolidPlugin(options: { ### Parameters -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `name` | `string` | Yes | Display name shown in the devtools tab | -| `id` | `string` | No | Unique identifier for the plugin | -| `defaultOpen` | `boolean` | No | Whether the plugin panel starts open | -| `Component` | `(props: DevtoolsPanelProps) => JSX.Element` | Yes | Solid component function | +| Parameter | Type | Required | Description | +| ------------- | -------------------------------------------- | -------- | -------------------------------------- | +| `name` | `string` | Yes | Display name shown in the devtools tab | +| `id` | `string` | No | Unique identifier for the plugin | +| `defaultOpen` | `boolean` | No | Whether the plugin panel starts open | +| `Component` | `(props: DevtoolsPanelProps) => JSX.Element` | Yes | Solid component function | ### Return Value @@ -186,12 +189,15 @@ export function createSolidPanel< ### Usage ```tsx -import { createSolidPanel, createSolidPlugin } from '@tanstack/devtools-utils/solid' +import { + createSolidPanel, + createSolidPlugin, +} from '@tanstack/devtools-utils/solid' import { constructCoreClass } from '@tanstack/devtools-utils/solid/class' // Step 1: Build a core class with lazy loading const [MyDevtoolsCore, NoOpCore] = constructCoreClass( - () => import('./MyDevtoolsUI') + () => import('./MyDevtoolsUI'), ) // Step 2: Create panel from core class @@ -227,7 +233,7 @@ function constructCoreClass( import { constructCoreClass } from '@tanstack/devtools-utils/solid/class' const [DevtoolsCore, NoOpDevtoolsCore] = constructCoreClass( - () => import('./MyDevtoolsPanel') + () => import('./MyDevtoolsPanel'), ) // Use DevtoolsCore with createSolidPanel diff --git a/packages/devtools-utils/skills/devtools-framework-adapters/references/vue.md b/packages/devtools-utils/skills/devtools-framework-adapters/references/vue.md index a3c3760a..12ce80de 100644 --- a/packages/devtools-utils/skills/devtools-framework-adapters/references/vue.md +++ b/packages/devtools-utils/skills/devtools-framework-adapters/references/vue.md @@ -33,10 +33,10 @@ function createVuePlugin<TComponentProps extends Record<string, any>>( ### Parameters -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `name` | `string` | Yes | Display name shown in the devtools tab | -| `component` | `DefineComponent<TComponentProps>` | Yes | Vue component to render in the panel | +| Parameter | Type | Required | Description | +| ----------- | ---------------------------------- | -------- | -------------------------------------- | +| `name` | `string` | Yes | Display name shown in the devtools tab | +| `component` | `DefineComponent<TComponentProps>` | Yes | Vue component to render in the panel | Note: There is **no** `id` or `defaultOpen` parameter. Vue plugins are simpler. @@ -136,14 +136,16 @@ function createVuePanel< mount: (el: HTMLElement, theme?: DevtoolsPanelProps['theme']) => void unmount: () => void }, ->(CoreClass: new (props: TComponentProps) => TCoreDevtoolsClass): readonly [Panel, NoOpPanel] +>( + CoreClass: new (props: TComponentProps) => TCoreDevtoolsClass, +): readonly [Panel, NoOpPanel] ``` ### Parameters -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `CoreClass` | `new (props: TComponentProps) => { mount(el, theme?): void; unmount(): void }` | Yes | Class constructor. **Note:** Vue's constructor takes `props`, unlike React's no-arg constructor. | +| Parameter | Type | Required | Description | +| ----------- | ------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------ | +| `CoreClass` | `new (props: TComponentProps) => { mount(el, theme?): void; unmount(): void }` | Yes | Class constructor. **Note:** Vue's constructor takes `props`, unlike React's no-arg constructor. | ### Return Value @@ -153,6 +155,7 @@ A tuple of two Vue `DefineComponent`s: - **`NoOpPanel`** -- Renders `null`. Both components have the type: + ```ts DefineComponent<{ theme?: 'dark' | 'light' | 'system' @@ -205,8 +208,14 @@ export function createVuePanel< }) return [Panel, NoOpPanel] as unknown as [ - DefineComponent<{ theme?: DevtoolsPanelProps['theme']; devtoolsProps: TComponentProps }>, - DefineComponent<{ theme?: DevtoolsPanelProps['theme']; devtoolsProps: TComponentProps }>, + DefineComponent<{ + theme?: DevtoolsPanelProps['theme'] + devtoolsProps: TComponentProps + }>, + DefineComponent<{ + theme?: DevtoolsPanelProps['theme'] + devtoolsProps: TComponentProps + }>, ] } ``` @@ -237,7 +246,12 @@ const [MyPlugin, NoOpPlugin] = createVuePlugin('My Store', MyPanel) ```vue <template> - <MyPanel theme="dark" :devtools-props="{ /* ... */ }" /> + <MyPanel + theme="dark" + :devtools-props="{ + /* ... */ + }" + /> </template> ``` diff --git a/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md b/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md index c4a4baef..a31b9c65 100644 --- a/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md +++ b/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md @@ -9,11 +9,11 @@ description: > Vite ^6 || ^7 only. type: core library: tanstack-devtools -library_version: "0.10.12" +library_version: '0.10.12' sources: - - "TanStack/devtools:docs/vite-plugin.md" - - "TanStack/devtools:docs/source-inspector.md" - - "TanStack/devtools:packages/devtools-vite/src/plugin.ts" + - 'TanStack/devtools:docs/vite-plugin.md' + - 'TanStack/devtools:docs/source-inspector.md' + - 'TanStack/devtools:packages/devtools-vite/src/plugin.ts' --- Configure @tanstack/devtools-vite -- the Vite plugin that enhances TanStack Devtools with source inspection, console piping, enhanced logging, a server event bus, production stripping, editor integration, and a plugin marketplace. The plugin returns an array of sub-plugins, all using `enforce: 'pre'`, so it must be the FIRST plugin in the Vite config. @@ -33,11 +33,13 @@ export default { ``` Install as a dev dependency: + ```sh pnpm add -D @tanstack/devtools-vite ``` There is also a `defineDevtoolsConfig` helper for type-safe config objects: + ```ts import { devtools, defineDevtoolsConfig } from '@tanstack/devtools-vite' @@ -53,6 +55,7 @@ export default { ## Exports From `packages/devtools-vite/src/index.ts`: + - `devtools` -- main plugin factory, returns `Array<Plugin>` - `defineDevtoolsConfig` -- identity function for type-safe config - `TanStackDevtoolsViteConfig` -- config type (re-exported) @@ -62,17 +65,17 @@ From `packages/devtools-vite/src/index.ts`: `devtools()` returns an array of Vite plugins. Each has `enforce: 'pre'` and only activates when its conditions are met (dev mode, serve command, etc.). -| Sub-plugin name | What it does | When active | -|---|---|---| -| `@tanstack/devtools:inject-source` | Babel transform adding `data-tsd-source` attrs to JSX | dev mode + `injectSource.enabled` | -| `@tanstack/devtools:config` | Reserved for future config modifications | serve command only | -| `@tanstack/devtools:custom-server` | Starts ServerEventBus, registers middleware for open-source/console-pipe endpoints | dev mode | -| `@tanstack/devtools:remove-devtools-on-build` | Strips devtools imports/JSX from production bundles | build command or production mode + `removeDevtoolsOnBuild` | -| `@tanstack/devtools:event-client-setup` | Marketplace: listens for install/add-plugin events via devtoolsEventClient | dev mode + serve + not CI | -| `@tanstack/devtools:console-pipe-transform` | Injects runtime console-pipe code into entry files | dev mode + serve + `consolePiping.enabled` | -| `@tanstack/devtools:better-console-logs` | Babel transform prepending source location to `console.log`/`console.error` | dev mode + `enhancedLogs.enabled` | -| `@tanstack/devtools:inject-plugin` | Detects which file imports TanStackDevtools (for marketplace injection) | dev mode + serve | -| `@tanstack/devtools:connection-injection` | Replaces `__TANSTACK_DEVTOOLS_PORT__`, `__TANSTACK_DEVTOOLS_HOST__`, `__TANSTACK_DEVTOOLS_PROTOCOL__` placeholders | dev mode + serve | +| Sub-plugin name | What it does | When active | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | +| `@tanstack/devtools:inject-source` | Babel transform adding `data-tsd-source` attrs to JSX | dev mode + `injectSource.enabled` | +| `@tanstack/devtools:config` | Reserved for future config modifications | serve command only | +| `@tanstack/devtools:custom-server` | Starts ServerEventBus, registers middleware for open-source/console-pipe endpoints | dev mode | +| `@tanstack/devtools:remove-devtools-on-build` | Strips devtools imports/JSX from production bundles | build command or production mode + `removeDevtoolsOnBuild` | +| `@tanstack/devtools:event-client-setup` | Marketplace: listens for install/add-plugin events via devtoolsEventClient | dev mode + serve + not CI | +| `@tanstack/devtools:console-pipe-transform` | Injects runtime console-pipe code into entry files | dev mode + serve + `consolePiping.enabled` | +| `@tanstack/devtools:better-console-logs` | Babel transform prepending source location to `console.log`/`console.error` | dev mode + `enhancedLogs.enabled` | +| `@tanstack/devtools:inject-plugin` | Detects which file imports TanStackDevtools (for marketplace injection) | dev mode + serve | +| `@tanstack/devtools:connection-injection` | Replaces `__TANSTACK_DEVTOOLS_PORT__`, `__TANSTACK_DEVTOOLS_HOST__`, `__TANSTACK_DEVTOOLS_PROTOCOL__` placeholders | dev mode + serve | ## Subsystem Details @@ -81,6 +84,7 @@ From `packages/devtools-vite/src/index.ts`: Adds `data-tsd-source="<relative-path>:<line>:<column>"` attributes to every JSX opening element via Babel. This powers the "Go to Source" feature -- hold the inspect hotkey (default: Shift+Alt+Ctrl/Meta), hover over elements, click to open in editor. **Key behaviors:** + - Skips `<Fragment>` and `<React.Fragment>` - Skips elements where the component's props parameter is spread (`{...props}`) -- this is because injecting the attribute would be overwritten by the spread - Skips files matching `injectSource.ignore.files` patterns @@ -107,12 +111,14 @@ devtools({ Bidirectional console piping between client and server. Injects runtime code (IIFE) into entry files that: **Client side:** + 1. Wraps `console[level]` to batch and POST entries to `/__tsd/console-pipe` 2. Opens an EventSource on `/__tsd/console-pipe/sse` to receive server logs 3. Server logs appear in browser console with a purple `[Server]` prefix 4. Client logs appear in terminal with a cyan `[Client]` prefix **Server side (SSR/Nitro):** + 1. Wraps `console[level]` to batch and POST entries to `<viteServerUrl>/__tsd/console-pipe/server` 2. These are then broadcast to all SSE clients @@ -148,6 +154,7 @@ devtools({ ### Production Stripping Removes all devtools code from production builds. The transform: + 1. Finds files importing from these packages: `@tanstack/react-devtools`, `@tanstack/preact-devtools`, `@tanstack/solid-devtools`, `@tanstack/vue-devtools`, `@tanstack/devtools` 2. Removes the import declarations 3. Removes the JSX elements that use the imported components @@ -168,6 +175,7 @@ devtools({ A WebSocket + SSE server for devtools-to-client communication. Managed by `@tanstack/devtools-event-bus/server`. **Key behaviors:** + - Default port: 4206 - On EADDRINUSE: falls back to OS-assigned port (port 0) - When Vite uses HTTPS: piggybacks on Vite's httpServer instead of creating a standalone one (shares TLS certificate) @@ -179,9 +187,9 @@ A WebSocket + SSE server for devtools-to-client communication. Managed by `@tans ```ts devtools({ eventBusConfig: { - port: 4206, // default + port: 4206, // default enabled: true, // default; set false for storybook/vitest - debug: false, // default; logs internal bus activity + debug: false, // default; logs internal bus activity }, }) ``` @@ -212,6 +220,7 @@ devtools({ ### Plugin Marketplace When the dev server is running, listens for events via `devtoolsEventClient`: + - `install-devtools` -- runs package manager install, then auto-injects plugin into devtools setup file - `add-plugin-to-devtools` -- injects plugin import and JSX/function call into the file containing `<TanStackDevtools>` - `bump-package-version` -- updates a package to a minimum version @@ -238,10 +247,7 @@ export default { // CORRECT export default { - plugins: [ - devtools(), - react(), - ], + plugins: [devtools(), react()], } ``` @@ -280,12 +286,12 @@ devtools({ eventBusConfig: { port: 4207 } }) These are registered on the Vite dev server (not the event bus server): -| Endpoint | Method | Purpose | -|---|---|---| -| `/__tsd/open-source?source=<path:line:col>` | GET | Opens file in editor, returns HTML that closes the window | -| `/__tsd/console-pipe` | POST | Receives client console entries (batched JSON) | -| `/__tsd/console-pipe/server` | POST | Receives server-side console entries | -| `/__tsd/console-pipe/sse` | GET | SSE stream for broadcasting server logs to browser | +| Endpoint | Method | Purpose | +| ------------------------------------------- | ------ | --------------------------------------------------------- | +| `/__tsd/open-source?source=<path:line:col>` | GET | Opens file in editor, returns HTML that closes the window | +| `/__tsd/console-pipe` | POST | Receives client console entries (batched JSON) | +| `/__tsd/console-pipe/server` | POST | Receives server-side console entries | +| `/__tsd/console-pipe/sse` | GET | SSE stream for broadcasting server logs to browser | ## Cross-References diff --git a/packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md b/packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md index ae13905e..09fd7062 100644 --- a/packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md +++ b/packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md @@ -45,14 +45,15 @@ declare function defineDevtoolsConfig( Controls source injection -- the Babel transform that adds `data-tsd-source` attributes to JSX elements for the "Go to Source" feature. -| Field | Type | Default | Description | -|---|---|---|---| -| `enabled` | `boolean` | `true` | Whether to inject `data-tsd-source` attributes into JSX elements during development. | -| `ignore` | `object` | `undefined` | Patterns to exclude from injection. | -| `ignore.files` | `Array<string \| RegExp>` | `[]` | File paths to skip. Strings are matched via picomatch glob syntax. RegExp patterns are tested directly. Matched against the file's path relative to `process.cwd()`. | -| `ignore.components` | `Array<string \| RegExp>` | `[]` | Component/element names to skip. Strings are matched via picomatch. RegExp patterns are tested directly. Matched against the JSX element name (e.g., `"div"`, `"MyComponent"`, `"Namespace.Component"`). | +| Field | Type | Default | Description | +| ------------------- | ------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `enabled` | `boolean` | `true` | Whether to inject `data-tsd-source` attributes into JSX elements during development. | +| `ignore` | `object` | `undefined` | Patterns to exclude from injection. | +| `ignore.files` | `Array<string \| RegExp>` | `[]` | File paths to skip. Strings are matched via picomatch glob syntax. RegExp patterns are tested directly. Matched against the file's path relative to `process.cwd()`. | +| `ignore.components` | `Array<string \| RegExp>` | `[]` | Component/element names to skip. Strings are matched via picomatch. RegExp patterns are tested directly. Matched against the JSX element name (e.g., `"div"`, `"MyComponent"`, `"Namespace.Component"`). | **Built-in exclusions (hardcoded in transform filter, not configurable):** + - `node_modules` - `?raw` imports - `/dist/` paths @@ -67,16 +68,8 @@ devtools({ injectSource: { enabled: true, ignore: { - files: [ - 'node_modules', - /.*\.test\.(js|ts|jsx|tsx)$/, - '**/generated/**', - ], - components: [ - 'InternalComponent', - /.*Provider$/, - /^Styled/, - ], + files: ['node_modules', /.*\.test\.(js|ts|jsx|tsx)$/, '**/generated/**'], + components: ['InternalComponent', /.*Provider$/, /^Styled/], }, }, }) @@ -88,12 +81,13 @@ devtools({ Controls bidirectional console log piping between client (browser) and server (terminal/SSR runtime). -| Field | Type | Default | Description | -|---|---|---|---| -| `enabled` | `boolean` | `true` | Whether to enable console piping. When enabled, client `console.*` calls are forwarded to the terminal, and server `console.*` calls are forwarded to the browser console. | -| `levels` | `Array<ConsoleLevel>` | `['log', 'warn', 'error', 'info', 'debug']` | Which console methods to intercept and pipe. `ConsoleLevel` is `'log' \| 'warn' \| 'error' \| 'info' \| 'debug'`. | +| Field | Type | Default | Description | +| --------- | --------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `enabled` | `boolean` | `true` | Whether to enable console piping. When enabled, client `console.*` calls are forwarded to the terminal, and server `console.*` calls are forwarded to the browser console. | +| `levels` | `Array<ConsoleLevel>` | `['log', 'warn', 'error', 'info', 'debug']` | Which console methods to intercept and pipe. `ConsoleLevel` is `'log' \| 'warn' \| 'error' \| 'info' \| 'debug'`. | **Runtime behavior:** + - Client batches entries (max 50, flush after 100ms) and POSTs to `/__tsd/console-pipe` - Server batches entries (max 20, flush after 50ms) and POSTs to `<viteServerUrl>/__tsd/console-pipe/server` - Browser subscribes to server logs via `EventSource` at `/__tsd/console-pipe/sse` @@ -125,22 +119,25 @@ devtools({ Controls the Babel transform that prepends source location information to `console.log()` and `console.error()` calls. -| Field | Type | Default | Description | -|---|---|---|---| -| `enabled` | `boolean` | `true` | Whether to enhance console.log and console.error with source location. When enabled, each log call gets a clickable "Go to Source" link in the browser and a file:line:column prefix in the terminal. | +| Field | Type | Default | Description | +| --------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `enabled` | `boolean` | `true` | Whether to enhance console.log and console.error with source location. When enabled, each log call gets a clickable "Go to Source" link in the browser and a file:line:column prefix in the terminal. | **What gets transformed:** + - Only `console.log(...)` and `console.error(...)` calls (not `warn`, `info`, `debug`) - Skips `node_modules`, `?raw`, `/dist/`, `/build/` - Skips files that don't contain the string `console.` **Browser output format:** + ``` %cLOG%c %cGo to Source: http://localhost:5173/__tsd/open-source?source=...%c -> <original args> ``` **Server output format (chalk):** + ``` LOG /src/components/Header.tsx:26:13 -> <original args> @@ -162,11 +159,12 @@ devtools({ Controls whether devtools code is stripped from production builds. -| Field | Type | Default | Description | -|---|---|---|---| -| `removeDevtoolsOnBuild` | `boolean` | `true` | When true, removes all devtools imports and JSX usage from production builds. | +| Field | Type | Default | Description | +| ----------------------- | --------- | ------- | ----------------------------------------------------------------------------- | +| `removeDevtoolsOnBuild` | `boolean` | `true` | When true, removes all devtools imports and JSX usage from production builds. | **Packages stripped:** + - `@tanstack/react-devtools` - `@tanstack/preact-devtools` - `@tanstack/solid-devtools` @@ -176,6 +174,7 @@ Controls whether devtools code is stripped from production builds. **Activation condition:** Active when `command !== 'serve'` OR `config.mode === 'production'`. This dual check supports hosting providers (Cloudflare, Netlify, Heroku) that may not use the `build` command but always set mode to `production`. **What gets removed:** + 1. Import declarations from the listed packages 2. JSX elements using the imported component names 3. Leftover imports that were only referenced inside the removed JSX (e.g., plugin panel components referenced in the `plugins` prop) @@ -195,9 +194,9 @@ devtools({ Controls the plugin's own console output. -| Field | Type | Default | Description | -|---|---|---|---| -| `logging` | `boolean` | `true` | Whether the devtools plugin logs status messages to the terminal (e.g., "Removed devtools code from: ..."). | +| Field | Type | Default | Description | +| --------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------- | +| `logging` | `boolean` | `true` | Whether the devtools plugin logs status messages to the terminal (e.g., "Removed devtools code from: ..."). | **Example:** @@ -213,24 +212,32 @@ devtools({ Configuration for the server event bus that handles devtools-to-client communication via WebSocket and SSE. -| Field | Type | Default | Description | -|---|---|---|---| -| `enabled` | `boolean` | `true` | Whether to start the server event bus. Set to `false` when running devtools in environments that don't need it (e.g., Storybook, Vitest). This field is specific to the Vite plugin wrapper; it is not part of `ServerEventBusConfig` from `@tanstack/devtools-event-bus/server`. | -| `port` | `number` | `4206` | Preferred port for the event bus server. If the port is in use (EADDRINUSE), the bus falls back to an OS-assigned port (port 0). | -| `host` | `string` | Derived from `server.host` in Vite config, or `'localhost'` | Hostname to bind the event bus server to. | -| `debug` | `boolean` | `false` | When true, logs internal event bus activity (connections, dispatches, etc.) to the console. | -| `httpServer` | `HttpServerLike` | `undefined` | An external HTTP server to attach to instead of creating a standalone one. The Vite plugin automatically sets this when HTTPS is enabled (uses `server.httpServer` from Vite) so WebSocket/SSE connections share the same TLS certificate. You generally do not need to set this manually. | +| Field | Type | Default | Description | +| ------------ | ---------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `enabled` | `boolean` | `true` | Whether to start the server event bus. Set to `false` when running devtools in environments that don't need it (e.g., Storybook, Vitest). This field is specific to the Vite plugin wrapper; it is not part of `ServerEventBusConfig` from `@tanstack/devtools-event-bus/server`. | +| `port` | `number` | `4206` | Preferred port for the event bus server. If the port is in use (EADDRINUSE), the bus falls back to an OS-assigned port (port 0). | +| `host` | `string` | Derived from `server.host` in Vite config, or `'localhost'` | Hostname to bind the event bus server to. | +| `debug` | `boolean` | `false` | When true, logs internal event bus activity (connections, dispatches, etc.) to the console. | +| `httpServer` | `HttpServerLike` | `undefined` | An external HTTP server to attach to instead of creating a standalone one. The Vite plugin automatically sets this when HTTPS is enabled (uses `server.httpServer` from Vite) so WebSocket/SSE connections share the same TLS certificate. You generally do not need to set this manually. | **`HttpServerLike` interface:** + ```ts interface HttpServerLike { on: (event: string, listener: (...args: Array<any>) => void) => this - removeListener: (event: string, listener: (...args: Array<any>) => void) => this - address: () => { port: number; family: string; address: string } | string | null + removeListener: ( + event: string, + listener: (...args: Array<any>) => void, + ) => this + address: () => + | { port: number; family: string; address: string } + | string + | null } ``` **`ServerEventBusConfig` type (from `@tanstack/devtools-event-bus/server`):** + ```ts interface ServerEventBusConfig { port?: number | undefined @@ -269,12 +276,13 @@ devtools({ Configuration for the "open in editor" functionality used by the source inspector. -| Field | Type | Default | Description | -|---|---|---|---| -| `name` | `string` | `'VSCode'` | Name of the editor, used for debugging/logging purposes. | +| Field | Type | Default | Description | +| ------ | ----------------------------------------------------------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | `string` | `'VSCode'` | Name of the editor, used for debugging/logging purposes. | | `open` | `(path: string, lineNumber: string \| undefined, columnNumber?: string) => Promise<void>` | Uses `launch-editor` to open VS Code | Callback function that opens a file in the editor. The `path` is an absolute file path. `lineNumber` and `columnNumber` are strings (not numbers) or undefined. | **`EditorConfig` type (from `packages/devtools-vite/src/editor.ts`):** + ```ts type EditorConfig = { name: string @@ -287,6 +295,7 @@ type EditorConfig = { ``` **Default implementation:** + ```ts const DEFAULT_EDITOR_CONFIG: EditorConfig = { name: 'VSCode', @@ -326,11 +335,11 @@ devtools({ These are not user-facing config options but are relevant if you work on `@tanstack/devtools` internals. The `connection-injection` sub-plugin replaces these string literals in `@tanstack/devtools*` and `@tanstack/event-bus` source code during dev: -| Placeholder | Replaced with | Fallback | -|---|---|---| -| `__TANSTACK_DEVTOOLS_PORT__` | Actual event bus port (number) | `4206` | -| `__TANSTACK_DEVTOOLS_HOST__` | Event bus hostname (JSON string) | `"localhost"` | -| `__TANSTACK_DEVTOOLS_PROTOCOL__` | `"http"` or `"https"` (JSON string) | `"http"` | +| Placeholder | Replaced with | Fallback | +| -------------------------------- | ----------------------------------- | ------------- | +| `__TANSTACK_DEVTOOLS_PORT__` | Actual event bus port (number) | `4206` | +| `__TANSTACK_DEVTOOLS_HOST__` | Event bus hostname (JSON string) | `"localhost"` | +| `__TANSTACK_DEVTOOLS_PROTOCOL__` | `"http"` or `"https"` (JSON string) | `"http"` | --- @@ -387,18 +396,18 @@ export default { ## Defaults Summary -| Option | Default Value | -|---|---| -| `injectSource.enabled` | `true` | -| `injectSource.ignore` | `undefined` (no ignores) | -| `consolePiping.enabled` | `true` | -| `consolePiping.levels` | `['log', 'warn', 'error', 'info', 'debug']` | -| `enhancedLogs.enabled` | `true` | -| `removeDevtoolsOnBuild` | `true` | -| `logging` | `true` | -| `eventBusConfig.enabled` | `true` | -| `eventBusConfig.port` | `4206` | -| `eventBusConfig.host` | Vite's `server.host` or `'localhost'` | -| `eventBusConfig.debug` | `false` | -| `editor.name` | `'VSCode'` | -| `editor.open` | Uses `launch-editor` | +| Option | Default Value | +| ------------------------ | ------------------------------------------- | +| `injectSource.enabled` | `true` | +| `injectSource.ignore` | `undefined` (no ignores) | +| `consolePiping.enabled` | `true` | +| `consolePiping.levels` | `['log', 'warn', 'error', 'info', 'debug']` | +| `enhancedLogs.enabled` | `true` | +| `removeDevtoolsOnBuild` | `true` | +| `logging` | `true` | +| `eventBusConfig.enabled` | `true` | +| `eventBusConfig.port` | `4206` | +| `eventBusConfig.host` | Vite's `server.host` or `'localhost'` | +| `eventBusConfig.debug` | `false` | +| `editor.name` | `'VSCode'` | +| `editor.open` | Uses `launch-editor` | diff --git a/packages/devtools/skills/devtools-app-setup/SKILL.md b/packages/devtools/skills/devtools-app-setup/SKILL.md index a9fa86a5..95ecf36e 100644 --- a/packages/devtools/skills/devtools-app-setup/SKILL.md +++ b/packages/devtools/skills/devtools-app-setup/SKILL.md @@ -6,8 +6,8 @@ description: > hideUntilHover, requireUrlFlag, eventBusConfig). TanStackDevtools component, defaultOpen, localStorage persistence. type: core -library: "@tanstack/devtools" -library_version: "0.10.12" +library: '@tanstack/devtools' +library_version: '0.10.12' sources: - docs/quick-start.md - docs/installation.md @@ -53,7 +53,7 @@ import { TanStackDevtools } from '@tanstack/react-devtools' import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' -<TanStackDevtools +;<TanStackDevtools plugins={[ { name: 'TanStack Query', @@ -106,19 +106,22 @@ import { TanStackDevtools } from '@tanstack/solid-devtools' import { SolidQueryDevtoolsPanel } from '@tanstack/solid-query-devtools' import App from './App' -render(() => ( - <> - <App /> - <TanStackDevtools - plugins={[ - { - name: 'TanStack Query', - render: <SolidQueryDevtoolsPanel />, - }, - ]} - /> - </> -), document.getElementById('root')!) +render( + () => ( + <> + <App /> + <TanStackDevtools + plugins={[ + { + name: 'TanStack Query', + render: <SolidQueryDevtoolsPanel />, + }, + ]} + /> + </> + ), + document.getElementById('root')!, +) ``` ### Preact @@ -155,6 +158,7 @@ render( Pass a `config` prop to `TanStackDevtools` to set initial shell behavior. These values are persisted to `localStorage` after first load and can be changed through the settings panel at runtime. Storage keys used internally: + - `tanstack_devtools_settings` -- persisted settings - `tanstack_devtools_state` -- persisted UI state (active tab, panel height, active plugins, persistOpen) @@ -163,16 +167,16 @@ All config properties are optional. Defaults shown below: ```tsx <TanStackDevtools config={{ - defaultOpen: false, // open panel on mount - hideUntilHover: false, // hide trigger until mouse hover - position: 'bottom-right', // trigger position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'middle-left' | 'middle-right' - panelLocation: 'bottom', // panel position: 'top' | 'bottom' + defaultOpen: false, // open panel on mount + hideUntilHover: false, // hide trigger until mouse hover + position: 'bottom-right', // trigger position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'middle-left' | 'middle-right' + panelLocation: 'bottom', // panel position: 'top' | 'bottom' openHotkey: ['Control', '~'], inspectHotkey: ['Shift', 'Alt', 'CtrlOrMeta'], - requireUrlFlag: false, // require URL param to show devtools + requireUrlFlag: false, // require URL param to show devtools urlFlag: 'tanstack-devtools', // the URL param name when requireUrlFlag is true - theme: 'dark', // 'light' | 'dark' (defaults to system preference) - triggerHidden: false, // completely hide trigger (hotkey still works) + theme: 'dark', // 'light' | 'dark' (defaults to system preference) + triggerHidden: false, // completely hide trigger (hotkey still works) }} /> ``` @@ -184,9 +188,9 @@ The `eventBusConfig` prop configures the client-side event bus that plugins use ```tsx <TanStackDevtools eventBusConfig={{ - debug: false, // enable debug logging for the event bus + debug: false, // enable debug logging for the event bus connectToServerBus: false, // connect to the Vite plugin server event bus - port: 3000, // port for server event bus connection + port: 3000, // port for server event bus connection }} /> ``` @@ -201,7 +205,7 @@ Each plugin entry can include a `defaultOpen` flag to control whether that plugi import { TanStackDevtools } from '@tanstack/react-devtools' import { FormDevtools } from '@tanstack/react-form' -<TanStackDevtools +;<TanStackDevtools config={{ hideUntilHover: true }} eventBusConfig={{ debug: true }} plugins={[ @@ -222,7 +226,7 @@ Use `requireUrlFlag` to hide devtools unless a specific URL parameter is present <TanStackDevtools config={{ requireUrlFlag: true, - urlFlag: 'tanstack-devtools', // visit ?tanstack-devtools to enable + urlFlag: 'tanstack-devtools', // visit ?tanstack-devtools to enable }} /> ``` @@ -238,9 +242,7 @@ Wrong: ```vue <!-- This silently fails - render is ignored in Vue adapter --> <script setup lang="ts"> -const plugins = [ - { name: 'My Plugin', render: MyComponent }, -] +const plugins = [{ name: 'My Plugin', render: MyComponent }] </script> ``` diff --git a/packages/devtools/skills/devtools-marketplace/SKILL.md b/packages/devtools/skills/devtools-marketplace/SKILL.md index 2fba60d4..90144339 100644 --- a/packages/devtools/skills/devtools-marketplace/SKILL.md +++ b/packages/devtools/skills/devtools-marketplace/SKILL.md @@ -6,8 +6,8 @@ description: > requires (packageName, minVersion), framework tagging, multi-framework submissions, featured plugins. type: lifecycle -library: "@tanstack/devtools" -library_version: "0.10.12" +library: '@tanstack/devtools' +library_version: '0.10.12' requires: - devtools-plugin-panel sources: @@ -131,6 +131,7 @@ A function-based plugin exports a factory function that returns a plugin object. ``` When a user clicks "Install" in the marketplace, the Vite plugin: + 1. Runs the package manager to install `@acme/react-analytics-devtools` 2. Finds the file containing `<TanStackDevtools />` 3. Adds `import { AnalyticsDevtoolsPlugin } from '@acme/react-analytics-devtools'` @@ -164,7 +165,7 @@ The injected code looks like: ```tsx import { AcmeStateDevtoolsPanel } from '@acme/react-state-devtools' -<TanStackDevtools +;<TanStackDevtools plugins={[ { name: 'Acme State Inspector', render: <AcmeStateDevtoolsPanel /> }, ]} @@ -257,13 +258,13 @@ If `pluginImport` is missing, step 3-5 are skipped entirely. The package gets in The marketplace determines the user's current framework by scanning their `package.json` dependencies for known framework packages: -| Framework | Detected packages | -|-----------|------------------| -| react | `react`, `react-dom` | -| solid | `solid-js` | -| vue | `vue`, `@vue/core` | -| svelte | `svelte` | -| angular | `@angular/core` | +| Framework | Detected packages | +| --------- | -------------------- | +| react | `react`, `react-dom` | +| solid | `solid-js` | +| vue | `vue`, `@vue/core` | +| svelte | `svelte` | +| angular | `@angular/core` | Plugins with `framework: 'other'` are shown regardless of the detected framework. @@ -325,6 +326,7 @@ Correct -- pluginImport provided: ``` The `importName` must be the exact named export from your package. The `type` must match how the export is consumed: + - `'function'` if your export is a factory like `export function AnalyticsDevtoolsPlugin() { return { name: '...', ... } }` - `'jsx'` if your export is a component like `export function AnalyticsDevtoolsPanel() { return <div>...</div> }` diff --git a/packages/devtools/skills/devtools-plugin-panel/SKILL.md b/packages/devtools/skills/devtools-plugin-panel/SKILL.md index 471c02cc..60dc63fd 100644 --- a/packages/devtools/skills/devtools-plugin-panel/SKILL.md +++ b/packages/devtools/skills/devtools-plugin-panel/SKILL.md @@ -8,14 +8,14 @@ description: > with devtools-ui for multi-framework support, or framework-specific panels. type: core library: tanstack-devtools -library_version: "0.10.12" +library_version: '0.10.12' requires: - devtools-event-client sources: - - "TanStack/devtools:docs/building-custom-plugins.md" - - "TanStack/devtools:docs/plugin-lifecycle.md" - - "TanStack/devtools:docs/plugin-configuration.md" - - "TanStack/devtools:packages/devtools/src/context/devtools-context.tsx" + - 'TanStack/devtools:docs/building-custom-plugins.md' + - 'TanStack/devtools:docs/plugin-lifecycle.md' + - 'TanStack/devtools:docs/plugin-configuration.md' + - 'TanStack/devtools:packages/devtools/src/context/devtools-context.tsx' --- ## TanStackDevtoolsPlugin Interface @@ -64,7 +64,7 @@ import { EventClient } from '@tanstack/devtools-event-client' type StoreEvents = { 'state-changed': { storeName: string; state: unknown; timestamp: number } 'action-dispatched': { storeName: string; action: string; payload: unknown } - 'reset': void + reset: void } class StoreInspectorClient extends EventClient<StoreEvents> { @@ -84,21 +84,33 @@ Event names are suffixes only. The `pluginId` is prepended automatically: `'stor /** @jsxImportSource solid-js */ import { createSignal, onCleanup, For } from 'solid-js' import { - MainPanel, Header, HeaderLogo, Section, SectionTitle, - JsonTree, Button, Tag, useTheme, + MainPanel, + Header, + HeaderLogo, + Section, + SectionTitle, + JsonTree, + Button, + Tag, + useTheme, } from '@tanstack/devtools-ui' import { storeInspector } from './event-client' export default function StoreInspectorPanel() { const { theme } = useTheme() const [state, setState] = createSignal<Record<string, unknown>>({}) - const [actions, setActions] = createSignal<Array<{ action: string; payload: unknown }>>([]) + const [actions, setActions] = createSignal< + Array<{ action: string; payload: unknown }> + >([]) 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 }]) + setActions((prev) => [ + ...prev, + { action: e.payload.action, payload: e.payload.payload }, + ]) }) onCleanup(() => { @@ -130,7 +142,9 @@ export default function StoreInspectorPanel() { </div> )} </For> - <Button variant="danger" onClick={() => setActions([])}>Clear Log</Button> + <Button variant="danger" onClick={() => setActions([])}> + Clear Log + </Button> </Section> </MainPanel> ) @@ -162,12 +176,13 @@ export const [StoreInspectorPanel, NoOpStoreInspectorPanel] = import { createReactPlugin } from '@tanstack/devtools-utils/react' import { StoreInspectorPanel } from './react' -export const [StoreInspectorPlugin, NoOpStoreInspectorPlugin] = createReactPlugin({ - name: 'Store Inspector', - id: 'store-inspector', - defaultOpen: true, - Component: StoreInspectorPanel, -}) +export const [StoreInspectorPlugin, NoOpStoreInspectorPlugin] = + createReactPlugin({ + name: 'Store Inspector', + id: 'store-inspector', + defaultOpen: true, + Component: StoreInspectorPanel, + }) ``` ### Step 4: Register @@ -222,7 +237,9 @@ function MyPluginPanel({ theme }: { theme?: 'light' | 'dark' }) { <h3>My Plugin</h3> <ul> {items.map((item) => ( - <li key={item.id}>{item.id}: {item.value}</li> + <li key={item.id}> + {item.id}: {item.value} + </li> ))} </ul> </div> @@ -292,10 +309,16 @@ Wrong: ```ts function ComponentA() { - useEffect(() => { const c = client.on('state', cb1); return c }, []) + useEffect(() => { + const c = client.on('state', cb1) + return c + }, []) } function ComponentB() { - useEffect(() => { const c = client.on('state', cb2); return c }, []) + useEffect(() => { + const c = client.on('state', cb2) + return c + }, []) } ``` diff --git a/packages/devtools/skills/devtools-plugin-panel/references/panel-api.md b/packages/devtools/skills/devtools-plugin-panel/references/panel-api.md index 2eb82db8..25eeda23 100644 --- a/packages/devtools/skills/devtools-plugin-panel/references/panel-api.md +++ b/packages/devtools/skills/devtools-plugin-panel/references/panel-api.md @@ -4,15 +4,15 @@ All factories return `[Plugin, NoOpPlugin]` tuples for production tree-shaking. -| Factory | Import Path | Framework | -|---|---|---| -| `createReactPlugin` | `@tanstack/devtools-utils/react` | React | -| `createSolidPlugin` | `@tanstack/devtools-utils/solid` | Solid.js | -| `createVuePlugin` | `@tanstack/devtools-utils/vue` | Vue 3 | -| `createPreactPlugin` | `@tanstack/devtools-utils/preact` | Preact | -| `createReactPanel` | `@tanstack/devtools-utils/react` | React (wraps Solid core) | -| `createSolidPanel` | `@tanstack/devtools-utils/solid` | Solid (wraps Solid core) | -| `constructCoreClass` | `@tanstack/devtools-utils/solid/class` | Core class construction | +| Factory | Import Path | Framework | +| -------------------- | -------------------------------------- | ------------------------ | +| `createReactPlugin` | `@tanstack/devtools-utils/react` | React | +| `createSolidPlugin` | `@tanstack/devtools-utils/solid` | Solid.js | +| `createVuePlugin` | `@tanstack/devtools-utils/vue` | Vue 3 | +| `createPreactPlugin` | `@tanstack/devtools-utils/preact` | Preact | +| `createReactPanel` | `@tanstack/devtools-utils/react` | React (wraps Solid core) | +| `createSolidPanel` | `@tanstack/devtools-utils/solid` | Solid (wraps Solid core) | +| `constructCoreClass` | `@tanstack/devtools-utils/solid/class` | Core class construction | ### createReactPlugin / createSolidPlugin / createPreactPlugin @@ -32,8 +32,16 @@ function createVuePlugin<TComponentProps extends Record<string, any>>( name: string, component: DefineComponent<TComponentProps, {}, unknown>, ): readonly [ - (props: TComponentProps) => { name: string; component: DefineComponent; props: TComponentProps }, - (props: TComponentProps) => { name: string; component: Fragment; props: TComponentProps }, + (props: TComponentProps) => { + name: string + component: DefineComponent + props: TComponentProps + }, + (props: TComponentProps) => { + name: string + component: Fragment + props: TComponentProps + }, ] ``` @@ -45,24 +53,24 @@ Vue uses positional `(name, component)` args, not an options object. All components are Solid.js. Use in Path 1 (Solid core) panels only. -| Component | Purpose | -|---|---| -| `MainPanel` | Root container with optional padding | -| `Header` | Top header bar | -| `HeaderLogo` | Logo section; accepts `flavor` colors | -| `Section` | Content section wrapper | -| `SectionTitle` | `<h3>` section heading | -| `SectionDescription` | `<p>` description text | -| `SectionIcon` | Icon wrapper in sections | -| `JsonTree` | Expandable JSON tree viewer with copy support | -| `Button` | Variants: primary, secondary, danger, success, info, warning; supports `outline` and `ghost` | -| `Tag` | Colored label tag with optional count badge | -| `Select` | Dropdown select with label and description | -| `Input` | Text input | -| `Checkbox` | Checkbox input | -| `TanStackLogo` | TanStack logo SVG | -| `ThemeContextProvider` | Wraps children with theme context | -| `useTheme` | Returns `{ theme: Accessor<Theme>, setTheme }` -- must be inside ThemeContextProvider | +| Component | Purpose | +| ---------------------- | -------------------------------------------------------------------------------------------- | +| `MainPanel` | Root container with optional padding | +| `Header` | Top header bar | +| `HeaderLogo` | Logo section; accepts `flavor` colors | +| `Section` | Content section wrapper | +| `SectionTitle` | `<h3>` section heading | +| `SectionDescription` | `<p>` description text | +| `SectionIcon` | Icon wrapper in sections | +| `JsonTree` | Expandable JSON tree viewer with copy support | +| `Button` | Variants: primary, secondary, danger, success, info, warning; supports `outline` and `ghost` | +| `Tag` | Colored label tag with optional count badge | +| `Select` | Dropdown select with label and description | +| `Input` | Text input | +| `Checkbox` | Checkbox input | +| `TanStackLogo` | TanStack logo SVG | +| `ThemeContextProvider` | Wraps children with theme context | +| `useTheme` | Returns `{ theme: Accessor<Theme>, setTheme }` -- must be inside ThemeContextProvider | ### JsonTree Props @@ -70,7 +78,7 @@ All components are Solid.js. Use in Path 1 (Solid core) panels only. function JsonTree<TData>(props: { value: TData copyable?: boolean - defaultExpansionDepth?: number // default: 1 + defaultExpansionDepth?: number // default: 1 collapsePaths?: Array<string> config?: { dateFormat?: string } }): JSX.Element @@ -84,9 +92,9 @@ function JsonTree<TData>(props: { class EventClient<TEventMap extends Record<string, any>> { constructor(config: { pluginId: string - debug?: boolean // default: false - enabled?: boolean // default: true - reconnectEveryMs?: number // default: 300 + debug?: boolean // default: false + enabled?: boolean // default: true + reconnectEveryMs?: number // default: 300 }) emit<TEvent extends keyof TEventMap & string>( @@ -96,12 +104,18 @@ class EventClient<TEventMap extends Record<string, any>> { on<TEvent extends keyof TEventMap & string>( eventSuffix: TEvent, - cb: (event: { type: TEvent; payload: TEventMap[TEvent]; pluginId?: string }) => void, + cb: (event: { + type: TEvent + payload: TEventMap[TEvent] + pluginId?: string + }) => void, options?: { withEventTarget?: boolean }, ): () => void onAll(cb: (event: { type: string; payload: any }) => void): () => void - onAllPluginEvents(cb: (event: AllDevtoolsEvents<TEventMap>) => void): () => void + onAllPluginEvents( + cb: (event: AllDevtoolsEvents<TEventMap>) => void, + ): () => void getPluginId(): string } ``` @@ -110,13 +124,13 @@ class EventClient<TEventMap extends Record<string, any>> { ## Key Source Files -| File | Purpose | -|---|---| -| `packages/devtools/src/context/devtools-context.tsx` | `TanStackDevtoolsPlugin` interface, plugin ID generation | -| `packages/devtools/src/core.ts` | `TanStackDevtoolsCore` class | -| `packages/devtools/src/utils/constants.ts` | `MAX_ACTIVE_PLUGINS = 3` | -| `packages/devtools/src/utils/get-default-active-plugins.ts` | defaultOpen resolution logic | -| `packages/event-bus-client/src/plugin.ts` | `EventClient` class | -| `packages/devtools-utils/src/solid/class.ts` | `constructCoreClass` | -| `packages/devtools-ui/src/index.ts` | All UI component exports | -| `packages/devtools-ui/src/components/theme.tsx` | `ThemeContextProvider`, `useTheme` | +| File | Purpose | +| ----------------------------------------------------------- | -------------------------------------------------------- | +| `packages/devtools/src/context/devtools-context.tsx` | `TanStackDevtoolsPlugin` interface, plugin ID generation | +| `packages/devtools/src/core.ts` | `TanStackDevtoolsCore` class | +| `packages/devtools/src/utils/constants.ts` | `MAX_ACTIVE_PLUGINS = 3` | +| `packages/devtools/src/utils/get-default-active-plugins.ts` | defaultOpen resolution logic | +| `packages/event-bus-client/src/plugin.ts` | `EventClient` class | +| `packages/devtools-utils/src/solid/class.ts` | `constructCoreClass` | +| `packages/devtools-ui/src/index.ts` | All UI component exports | +| `packages/devtools-ui/src/components/theme.tsx` | `ThemeContextProvider`, `useTheme` | diff --git a/packages/devtools/skills/devtools-production/SKILL.md b/packages/devtools/skills/devtools-production/SKILL.md index 83a08791..43f6e3c6 100644 --- a/packages/devtools/skills/devtools-production/SKILL.md +++ b/packages/devtools/skills/devtools-production/SKILL.md @@ -5,8 +5,8 @@ description: > devDependency vs regular dependency, conditional imports, NoOp plugin variants for tree-shaking, non-Vite production exclusion patterns. type: lifecycle -library: "@tanstack/devtools" -library_version: "0.10.12" +library: '@tanstack/devtools' +library_version: '0.10.12' requires: devtools-app-setup sources: - docs/production.md @@ -52,7 +52,13 @@ function App() { return ( <> <YourApp /> - <TanStackDevtools plugins={[/* ... */]} /> + <TanStackDevtools + plugins={ + [ + /* ... */ + ] + } + /> </> ) } @@ -76,6 +82,7 @@ The `@tanstack/devtools` core package uses Node.js conditional exports to serve ``` Key points: + - `browser` + `development` condition resolves to `dev.js` (dev-only extras). - `browser` without `development` resolves to `index.js` (production build). - `node` and `workerd` resolve to `server.js` (server-safe, no DOM). @@ -102,7 +109,7 @@ import react from '@vitejs/plugin-react' export default { plugins: [ - devtools(), // removeDevtoolsOnBuild defaults to true + devtools(), // removeDevtoolsOnBuild defaults to true react(), ], } @@ -117,7 +124,13 @@ function App() { return ( <> <YourApp /> - <TanStackDevtools plugins={[/* ... */]} /> + <TanStackDevtools + plugins={ + [ + /* ... */ + ] + } + /> </> ) } @@ -162,7 +175,13 @@ function App() { return ( <> <YourApp /> - <TanStackDevtools plugins={[/* ... */]} /> + <TanStackDevtools + plugins={ + [ + /* ... */ + ] + } + /> </> ) } @@ -176,9 +195,13 @@ You can combine this with `requireUrlFlag` from the shell config to hide the dev <TanStackDevtools config={{ requireUrlFlag: true, - urlFlag: 'debug', // visit ?debug to show devtools + urlFlag: 'debug', // visit ?debug to show devtools }} - plugins={[/* ... */]} + plugins={ + [ + /* ... */ + ] + } /> ``` @@ -197,9 +220,11 @@ import { TanStackDevtools } from '@tanstack/react-devtools' export default function Devtools() { return ( <TanStackDevtools - plugins={[ - // your plugins - ]} + plugins={ + [ + // your plugins + ] + } /> ) } @@ -234,7 +259,15 @@ let DevtoolsComponent: React.ComponentType = () => null if (__DEV__) { const { TanStackDevtools } = await import('@tanstack/react-devtools') - DevtoolsComponent = () => <TanStackDevtools plugins={[/* ... */]} /> + DevtoolsComponent = () => ( + <TanStackDevtools + plugins={ + [ + /* ... */ + ] + } + /> + ) } function App() { @@ -274,23 +307,19 @@ const ActivePlugin = process.env.NODE_ENV === 'development' ? QueryPlugin : QueryNoOpPlugin function App() { - return ( - <TanStackDevtools - plugins={[ActivePlugin()]} - /> - ) + return <TanStackDevtools plugins={[ActivePlugin()]} /> } ``` The NoOp pattern exists for every framework adapter: -| Framework | Factory | Source | -|-----------|---------|--------| -| React | `createReactPlugin` | `packages/devtools-utils/src/react/plugin.tsx` | -| React (panel) | `createReactPanel` | `packages/devtools-utils/src/react/panel.tsx` | -| Preact | `createPreactPlugin` | `packages/devtools-utils/src/preact/plugin.tsx` | -| Solid | `createSolidPlugin` | `packages/devtools-utils/src/solid/plugin.tsx` | -| Vue | `createVuePlugin` | `packages/devtools-utils/src/vue/plugin.ts` | +| Framework | Factory | Source | +| ------------- | -------------------- | ----------------------------------------------- | +| React | `createReactPlugin` | `packages/devtools-utils/src/react/plugin.tsx` | +| React (panel) | `createReactPanel` | `packages/devtools-utils/src/react/panel.tsx` | +| Preact | `createPreactPlugin` | `packages/devtools-utils/src/preact/plugin.tsx` | +| Solid | `createSolidPlugin` | `packages/devtools-utils/src/solid/plugin.tsx` | +| Vue | `createVuePlugin` | `packages/devtools-utils/src/vue/plugin.ts` | All return `readonly [Plugin, NoOpPlugin]`. The `NoOpPlugin` always has the same metadata (`name`, `id`, `defaultOpen`) but its render function produces an empty fragment, so the bundler can tree-shake the real panel component and all its dependencies. @@ -308,7 +337,7 @@ The Vite plugin's `removeDevtoolsOnBuild` defaults to `true`. If you want devtoo // vite.config.ts export default { plugins: [ - devtools(), // removeDevtoolsOnBuild defaults to true -- code is stripped + devtools(), // removeDevtoolsOnBuild defaults to true -- code is stripped react(), ], } @@ -324,10 +353,7 @@ npm install -D @tanstack/react-devtools ```ts // vite.config.ts export default { - plugins: [ - devtools({ removeDevtoolsOnBuild: false }), - react(), - ], + plugins: [devtools({ removeDevtoolsOnBuild: false }), react()], } ``` @@ -351,7 +377,13 @@ function App() { return ( <> <YourApp /> - <TanStackDevtools plugins={[/* ... */]} /> + <TanStackDevtools + plugins={ + [ + /* ... */ + ] + } + /> </> ) } diff --git a/packages/event-bus-client/skills/devtools-bidirectional/SKILL.md b/packages/event-bus-client/skills/devtools-bidirectional/SKILL.md index 6c0737ff..4015949d 100644 --- a/packages/event-bus-client/skills/devtools-bidirectional/SKILL.md +++ b/packages/event-bus-client/skills/devtools-bidirectional/SKILL.md @@ -2,8 +2,8 @@ name: devtools-bidirectional description: Two-way event patterns between devtools panel and application. App-to-devtools observation, devtools-to-app commands, time-travel debugging with snapshots and revert. structuredClone for snapshot safety, distinct event suffixes for observation vs commands, serializable payloads only. type: core -library: "@tanstack/devtools-event-client" -library_version: "0.10.12" +library: '@tanstack/devtools-event-client' +library_version: '0.10.12' requires: devtools-event-client sources: - packages/event-bus-client/src/plugin.ts @@ -93,7 +93,7 @@ type CounterEvents = { // Observation: app -> panel 'state-update': { count: number; updatedAt: number } // Commands: panel -> app - 'reset': void + reset: void 'set-count': { count: number } } ``` @@ -146,9 +146,9 @@ Combine observation (snapshots) with commands (revert) to build a time-travel sl ```ts type TimeTravelEvents = { // Observation: app -> panel - 'snapshot': { state: unknown; timestamp: number; label: string } + snapshot: { state: unknown; timestamp: number; label: string } // Command: panel -> app - 'revert': { state: unknown } + revert: { state: unknown } } class TimeTravelClient extends EventClient<TimeTravelEvents> { @@ -260,12 +260,13 @@ type StoreInspectorEvents = { // Commands: panel -> app (describe what to do) 'set-state': { storeName: string; state: unknown } 'dispatch-action': { storeName: string; action: string; payload: unknown } - 'reset': void - 'revert': { state: unknown } + reset: void + revert: { state: unknown } } ``` Naming convention: + - **Observation events** describe what happened: `state-update`, `action-dispatched`, `error-caught`, `snapshot` - **Command events** describe what to do: `set-state`, `dispatch-action`, `reset`, `revert` @@ -279,7 +280,7 @@ import { EventClient } from '@tanstack/devtools-event-client' type StoreInspectorEvents = { 'state-update': { storeName: string; state: unknown; timestamp: number } 'set-state': { storeName: string; state: unknown } - 'reset': void + reset: void } class StoreInspectorClient extends EventClient<StoreInspectorEvents> { @@ -417,8 +418,8 @@ Wrong: storeInspector.emit('set-state', { storeName: 'main', state: { - items: new Map([['a', 1]]), // Map -- lost on serialization - onClick: () => alert('hi'), // Function -- lost on serialization + items: new Map([['a', 1]]), // Map -- lost on serialization + onClick: () => alert('hi'), // Function -- lost on serialization ref: document.getElementById('x'), // DOM node -- lost on serialization }, }) @@ -446,9 +447,9 @@ Wrong: ```ts type MyEvents = { - 'state': unknown // Is this observation or command? - 'update': unknown // Who emits this? - 'count': number // Unclear direction + state: unknown // Is this observation or command? + update: unknown // Who emits this? + count: number // Unclear direction } ``` @@ -456,10 +457,10 @@ Correct: ```ts type MyEvents = { - 'state-update': unknown // Observation: describes what happened - 'set-state': unknown // Command: describes what to do - 'count-changed': number // Observation: past tense / descriptive - 'reset': void // Command: imperative + 'state-update': unknown // Observation: describes what happened + 'set-state': unknown // Command: describes what to do + 'count-changed': number // Observation: past tense / descriptive + reset: void // Command: imperative } ``` diff --git a/packages/event-bus-client/skills/devtools-event-client/SKILL.md b/packages/event-bus-client/skills/devtools-event-client/SKILL.md index da51871f..0973fbda 100644 --- a/packages/event-bus-client/skills/devtools-event-client/SKILL.md +++ b/packages/event-bus-client/skills/devtools-event-client/SKILL.md @@ -2,8 +2,8 @@ name: devtools-event-client description: Create typed EventClient for a library. Define event maps with typed payloads, pluginId auto-prepend namespacing, emit()/on()/onAll()/onAllPluginEvents() API. Connection lifecycle (5 retries, 300ms), event queuing, enabled/disabled state, SSR fallbacks, singleton pattern. Unique pluginId requirement to avoid event collisions. type: core -library: "@tanstack/devtools-event-client" -library_version: "0.10.12" +library: '@tanstack/devtools-event-client' +library_version: '0.10.12' sources: - packages/event-bus-client/src/plugin.ts - docs/event-system.md @@ -30,12 +30,12 @@ import { EventClient } from '@tanstack/devtools-event-client' ### Constructor Options -| Option | Type | Required | Default | Description | -| ------------------ | --------- | -------- | ------- | ------------------------------------------------ | -| `pluginId` | `string` | Yes | -- | Identifies this plugin in the event system. Must be unique across all plugins. | +| Option | Type | Required | Default | Description | +| ------------------ | --------- | -------- | ------- | ------------------------------------------------------------------------------------- | +| `pluginId` | `string` | Yes | -- | Identifies this plugin in the event system. Must be unique across all plugins. | | `debug` | `boolean` | No | `false` | Enable verbose console logging prefixed with `[tanstack-devtools:{pluginId}-plugin]`. | -| `enabled` | `boolean` | No | `true` | When `false`, `emit()` is a no-op and `on()` returns a no-op cleanup function. | -| `reconnectEveryMs` | `number` | No | `300` | Interval in ms between connection retry attempts (max 5 retries). | +| `enabled` | `boolean` | No | `true` | When `false`, `emit()` is a no-op and `on()` returns a no-op cleanup function. | +| `reconnectEveryMs` | `number` | No | `300` | Interval in ms between connection retry attempts (max 5 retries). | ## Core Patterns @@ -49,7 +49,7 @@ import { EventClient } from '@tanstack/devtools-event-client' type StoreEvents = { 'state-changed': { storeName: string; state: unknown; timestamp: number } 'action-dispatched': { storeName: string; action: string; payload: unknown } - 'reset': void + reset: void } class StoreInspectorClient extends EventClient<StoreEvents> { @@ -110,9 +110,13 @@ cleanup() **`on(suffix, callback, { withEventTarget: true })`** -- also register on an internal EventTarget so events emitted and listened to on the same client instance are delivered immediately without going through the global bus: ```ts -const cleanup = storeInspector.on('state-changed', (event) => { - console.log(event.payload.state) -}, { withEventTarget: true }) +const cleanup = storeInspector.on( + 'state-changed', + (event) => { + console.log(event.payload.state) + }, + { withEventTarget: true }, +) ``` **`onAll(callback)`** -- listen to all events from all plugins: @@ -238,8 +242,8 @@ Wrong: storeInspector.emit('state-changed', { storeName: 'main', state, - callback: () => {}, // Function -- not serializable - element: document.body, // DOM node -- not serializable + callback: () => {}, // Function -- not serializable + element: document.body, // DOM node -- not serializable }) ``` diff --git a/packages/event-bus-client/skills/devtools-instrumentation/SKILL.md b/packages/event-bus-client/skills/devtools-instrumentation/SKILL.md index ac105b99..756cc1a1 100644 --- a/packages/event-bus-client/skills/devtools-instrumentation/SKILL.md +++ b/packages/event-bus-client/skills/devtools-instrumentation/SKILL.md @@ -2,8 +2,8 @@ name: devtools-instrumentation description: Analyze library codebase for critical architecture and debugging points, add strategic event emissions. Identify middleware boundaries, state transitions, lifecycle hooks. Consolidate events (1 not 15), debounce high-frequency updates, DRY shared payload fields, guard emit() for production. Transparent server/client event bridging. type: core -library: "@tanstack/devtools-event-client" -library_version: "0.10.12" +library: '@tanstack/devtools-event-client' +library_version: '0.10.12' requires: devtools-event-client sources: - packages/event-bus-client/src/plugin.ts @@ -72,7 +72,10 @@ export const routerDevtools = new RouterDevtoolsClient() ``` ```ts -async function runMiddlewarePipeline(req: Request, middlewares: Middleware[]): Promise<Response> { +async function runMiddlewarePipeline( + req: Request, + middlewares: Middleware[], +): Promise<Response> { const requestId = crypto.randomUUID() const pipelineStart = performance.now() const chain: Array<{ name: string; durationMs: number }> = [] @@ -127,7 +130,10 @@ type QueryEvents = { class QueryDevtoolsClient extends EventClient<QueryEvents> { constructor() { - super({ pluginId: 'my-query-lib', enabled: process.env.NODE_ENV !== 'production' }) + super({ + pluginId: 'my-query-lib', + enabled: process.env.NODE_ENV !== 'production', + }) } } @@ -138,11 +144,20 @@ export const queryDevtools = new QueryDevtoolsClient() class Query { #state: QueryState = 'idle' - private transition(to: QueryState, extra?: Partial<QueryEvents['query-lifecycle']>) { + private transition( + to: QueryState, + extra?: Partial<QueryEvents['query-lifecycle']>, + ) { const from = this.#state if (from === to) return // No transition, no event this.#state = to - queryDevtools.emit('query-lifecycle', { queryKey: this.key, from, to, timestamp: Date.now(), ...extra }) + queryDevtools.emit('query-lifecycle', { + queryKey: this.key, + from, + to, + timestamp: Date.now(), + ...extra, + }) } async fetch() { @@ -150,9 +165,15 @@ class Query { const start = performance.now() try { const data = await this.fetcher() - this.transition('success', { data: structuredClone(data), fetchDuration: performance.now() - start }) + this.transition('success', { + data: structuredClone(data), + fetchDuration: performance.now() - start, + }) } catch (e) { - this.transition('error', { error: e instanceof Error ? e.message : String(e), fetchDuration: performance.now() - start }) + this.transition('error', { + error: e instanceof Error ? e.message : String(e), + fetchDuration: performance.now() - start, + }) } } } @@ -165,14 +186,27 @@ When multiple events share fields, build a shared base and spread it. ```ts class Store { private basePayload() { - return { storeName: this.#name, version: this.#version, sessionId: this.#sessionId, timestamp: Date.now() } + return { + storeName: this.#name, + version: this.#version, + sessionId: this.#sessionId, + timestamp: Date.now(), + } } - dispatch(action: string, updater: (s: Record<string, unknown>) => Record<string, unknown>) { + dispatch( + action: string, + updater: (s: Record<string, unknown>) => Record<string, unknown>, + ) { const prevState = structuredClone(this.#state) this.#state = updater(this.#state) this.#version++ - storeDevtools.emit('store-updated', { ...this.basePayload(), action, prevState, nextState: structuredClone(this.#state) }) + storeDevtools.emit('store-updated', { + ...this.basePayload(), + action, + prevState, + nextState: structuredClone(this.#state), + }) } reset(initial: Record<string, unknown>) { @@ -193,10 +227,19 @@ function createDebouncedEmitter<TEvents extends Record<string, any>>( delayMs: number, ) { const timers = new Map<string, ReturnType<typeof setTimeout>>() - return function debouncedEmit<K extends keyof TEvents & string>(event: K, payload: TEvents[K]) { + return function debouncedEmit<K extends keyof TEvents & string>( + event: K, + payload: TEvents[K], + ) { const existing = timers.get(event) if (existing) clearTimeout(existing) - timers.set(event, setTimeout(() => { client.emit(event, payload); timers.delete(event) }, delayMs)) + timers.set( + event, + setTimeout(() => { + client.emit(event, payload) + timers.delete(event) + }, delayMs), + ) } } @@ -215,7 +258,10 @@ For leading+trailing (throttle), use the same pattern with a `lastEmit` timestam ```ts class MyLibDevtools extends EventClient<MyEvents> { constructor() { - super({ pluginId: 'my-lib', enabled: process.env.NODE_ENV !== 'production' }) + super({ + pluginId: 'my-lib', + enabled: process.env.NODE_ENV !== 'production', + }) } } ``` @@ -224,7 +270,10 @@ For expensive payload construction (e.g., `structuredClone` of large state), gua ```ts if (process.env.NODE_ENV !== 'production') { - myDevtools.emit('state-snapshot', { state: structuredClone(largeState), timestamp: Date.now() }) + myDevtools.emit('state-snapshot', { + state: structuredClone(largeState), + timestamp: Date.now(), + }) } ``` @@ -278,8 +327,14 @@ Correct -- 1 event with all data: ```ts routerDevtools.emit('request-processed', { - id, method, path, duration: 50, - middlewareChain: [{ name: 'auth', durationMs: 5 }, { name: 'cors', durationMs: 1 }], + id, + method, + path, + duration: 50, + middlewareChain: [ + { name: 'auth', durationMs: 5 }, + { name: 'cors', durationMs: 1 }, + ], status: 200, }) ``` @@ -325,7 +380,12 @@ Correct -- instrumented at the handler boundary: function handleRequest(req: Request) { const params = parseQueryString(req.url) const result = processRequest(params) - devtools.emit('request-processed', { path: req.url, params: Object.fromEntries(params), result: result.summary, duration: performance.now() - start }) + devtools.emit('request-processed', { + path: req.url, + params: Object.fromEntries(params), + result: result.summary, + duration: performance.now() - start, + }) } ``` @@ -336,8 +396,20 @@ Source: maintainer interview Wrong: ```ts -devtools.emit('action-a', { storeName: this.name, version: this.version, sessionId: this.sessionId, timestamp: Date.now(), data }) -devtools.emit('action-b', { storeName: this.name, version: this.version, sessionId: this.sessionId, timestamp: Date.now(), other }) +devtools.emit('action-a', { + storeName: this.name, + version: this.version, + sessionId: this.sessionId, + timestamp: Date.now(), + data, +}) +devtools.emit('action-b', { + storeName: this.name, + version: this.version, + sessionId: this.sessionId, + timestamp: Date.now(), + other, +}) ``` Correct: From 3265b8234833dbc2e681dff2aabafe57fa396042 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:53:12 +0000 Subject: [PATCH 3/5] ci: apply automated fixes (attempt 2/3) --- packages/devtools/skills/devtools-app-setup/SKILL.md | 2 -- packages/devtools/skills/devtools-marketplace/SKILL.md | 1 - 2 files changed, 3 deletions(-) diff --git a/packages/devtools/skills/devtools-app-setup/SKILL.md b/packages/devtools/skills/devtools-app-setup/SKILL.md index 95ecf36e..0097c8ef 100644 --- a/packages/devtools/skills/devtools-app-setup/SKILL.md +++ b/packages/devtools/skills/devtools-app-setup/SKILL.md @@ -52,7 +52,6 @@ Add plugins via the `plugins` prop. Each plugin needs `name` (string) and `rende import { TanStackDevtools } from '@tanstack/react-devtools' import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' - ;<TanStackDevtools plugins={[ { @@ -204,7 +203,6 @@ Each plugin entry can include a `defaultOpen` flag to control whether that plugi ```tsx import { TanStackDevtools } from '@tanstack/react-devtools' import { FormDevtools } from '@tanstack/react-form' - ;<TanStackDevtools config={{ hideUntilHover: true }} eventBusConfig={{ debug: true }} diff --git a/packages/devtools/skills/devtools-marketplace/SKILL.md b/packages/devtools/skills/devtools-marketplace/SKILL.md index 90144339..50a55000 100644 --- a/packages/devtools/skills/devtools-marketplace/SKILL.md +++ b/packages/devtools/skills/devtools-marketplace/SKILL.md @@ -164,7 +164,6 @@ The injected code looks like: ```tsx import { AcmeStateDevtoolsPanel } from '@acme/react-state-devtools' - ;<TanStackDevtools plugins={[ { name: 'Acme State Inspector', render: <AcmeStateDevtoolsPanel /> }, From aee3b5b9f5ce03ac4db608e566f12495dabfe075 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak <t.zlak@hotmail.com> Date: Wed, 11 Mar 2026 11:53:49 +0100 Subject: [PATCH 4/5] chore: add changeset for intent skills --- .changeset/add-intent-skills.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/add-intent-skills.md diff --git a/.changeset/add-intent-skills.md b/.changeset/add-intent-skills.md new file mode 100644 index 00000000..33589396 --- /dev/null +++ b/.changeset/add-intent-skills.md @@ -0,0 +1,8 @@ +--- +"@tanstack/devtools": patch +"@tanstack/devtools-event-client": patch +"@tanstack/devtools-vite": patch +"@tanstack/devtools-utils": patch +--- + +Add @tanstack/intent agent skills for AI coding agents From 6971444846491862dc69f6bfc95fd82f7de55d77 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:55:04 +0000 Subject: [PATCH 5/5] ci: apply automated fixes --- .changeset/add-intent-skills.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/add-intent-skills.md b/.changeset/add-intent-skills.md index 33589396..2f64fa77 100644 --- a/.changeset/add-intent-skills.md +++ b/.changeset/add-intent-skills.md @@ -1,8 +1,8 @@ --- -"@tanstack/devtools": patch -"@tanstack/devtools-event-client": patch -"@tanstack/devtools-vite": patch -"@tanstack/devtools-utils": patch +'@tanstack/devtools': patch +'@tanstack/devtools-event-client': patch +'@tanstack/devtools-vite': patch +'@tanstack/devtools-utils': patch --- Add @tanstack/intent agent skills for AI coding agents