@tanstack/devtools 0.6.19 → 0.6.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,346 @@
1
+ import { For, Show, createSignal, onCleanup, onMount } from 'solid-js'
2
+ import { devtoolsEventClient } from '@tanstack/devtools-client'
3
+ import { usePlugins } from '../context/use-devtools-context'
4
+ import { useStyles } from '../styles/use-styles'
5
+ import { PluginSectionComponent } from './marketplace/plugin-section'
6
+ import { SettingsPanel } from './marketplace/settings-panel'
7
+ import { MarketplaceHeader } from './marketplace/marketplace-header'
8
+ import { FRAMEWORKS } from './marketplace/types'
9
+ import {
10
+ buildPluginCards,
11
+ detectFramework,
12
+ groupIntoSections,
13
+ } from './marketplace/plugin-utils'
14
+ import type { PackageJson } from '@tanstack/devtools-client'
15
+ import type { PluginCard, PluginSection } from './marketplace/types'
16
+
17
+ export const PluginMarketplace = () => {
18
+ const styles = useStyles()
19
+ const { plugins } = usePlugins()
20
+ const [pluginSections, setPluginSections] = createSignal<
21
+ Array<PluginSection>
22
+ >([])
23
+ const [currentPackageJson, setCurrentPackageJson] =
24
+ createSignal<PackageJson | null>(null)
25
+ const [searchInput, setSearchInput] = createSignal('')
26
+ const [searchQuery, setSearchQuery] = createSignal('')
27
+ const [collapsedSections, setCollapsedSections] = createSignal<Set<string>>(
28
+ new Set(),
29
+ )
30
+ const [showActivePlugins, setShowActivePlugins] = createSignal(true)
31
+ const [selectedTags, setSelectedTags] = createSignal<Set<string>>(new Set())
32
+ const [isSettingsOpen, setIsSettingsOpen] = createSignal(false)
33
+
34
+ let debounceTimeout: ReturnType<typeof setTimeout> | undefined
35
+
36
+ // Debounce search input
37
+ const handleSearchInput = (value: string) => {
38
+ setSearchInput(value)
39
+
40
+ if (debounceTimeout) {
41
+ clearTimeout(debounceTimeout)
42
+ }
43
+
44
+ debounceTimeout = setTimeout(() => {
45
+ setSearchQuery(value)
46
+ }, 300)
47
+ }
48
+
49
+ const toggleSection = (framework: string) => {
50
+ setCollapsedSections((prev) => {
51
+ const newSet = new Set(prev)
52
+ if (newSet.has(framework)) {
53
+ newSet.delete(framework)
54
+ } else {
55
+ newSet.add(framework)
56
+ }
57
+ return newSet
58
+ })
59
+ }
60
+
61
+ const matchesSearch = (card: PluginCard, query: string): boolean => {
62
+ if (!query) return true
63
+
64
+ const lowerQuery = query.toLowerCase()
65
+ return (
66
+ card.devtoolsPackage.toLowerCase().includes(lowerQuery) ||
67
+ card.requiredPackageName.toLowerCase().includes(lowerQuery) ||
68
+ card.framework.toLowerCase().includes(lowerQuery)
69
+ )
70
+ }
71
+
72
+ const getFilteredSections = () => {
73
+ const query = searchQuery()
74
+ const showActive = showActivePlugins()
75
+ const tags = selectedTags()
76
+ const pkg = currentPackageJson()
77
+
78
+ // Regenerate sections from current plugin state
79
+ if (!pkg) return []
80
+
81
+ const currentFramework = detectFramework(pkg, FRAMEWORKS)
82
+ const registeredPlugins = new Set(plugins()?.map((p) => p.id || '') || [])
83
+
84
+ // Build fresh cards from current state
85
+ const allCards = buildPluginCards(
86
+ pkg,
87
+ currentFramework,
88
+ registeredPlugins,
89
+ pluginSections().flatMap((s) => s.cards), // Preserve status from existing cards
90
+ )
91
+
92
+ // Generate sections from cards
93
+ let sections = groupIntoSections(allCards)
94
+
95
+ // Filter out active plugins section if hidden
96
+ if (!showActive) {
97
+ sections = sections.filter((section) => section.id !== 'active')
98
+ }
99
+
100
+ // Filter by tags if any are selected
101
+ if (tags.size > 0) {
102
+ sections = sections
103
+ .map((section) => ({
104
+ ...section,
105
+ cards: section.cards.filter((card) => {
106
+ if (!card.metadata?.tags) return false
107
+ return card.metadata.tags.some((tag) => tags.has(tag))
108
+ }),
109
+ }))
110
+ .filter((section) => section.cards.length > 0)
111
+ }
112
+
113
+ // Apply search filter
114
+ if (!query) return sections
115
+
116
+ return sections
117
+ .map((section) => ({
118
+ ...section,
119
+ cards: section.cards.filter((card) => matchesSearch(card, query)),
120
+ }))
121
+ .filter((section) => section.cards.length > 0)
122
+ }
123
+
124
+ onMount(() => {
125
+ // Listen for package.json updates
126
+ const cleanupJsonRead = devtoolsEventClient.on(
127
+ 'package-json-read',
128
+ (event) => {
129
+ setCurrentPackageJson(event.payload.packageJson)
130
+ updatePluginCards(event.payload.packageJson)
131
+ },
132
+ )
133
+
134
+ const cleanupJsonUpdated = devtoolsEventClient.on(
135
+ 'package-json-updated',
136
+ (event) => {
137
+ setCurrentPackageJson(event.payload.packageJson)
138
+ updatePluginCards(event.payload.packageJson)
139
+ },
140
+ )
141
+
142
+ // Listen for installation results
143
+ const cleanupDevtoolsInstalled = devtoolsEventClient.on(
144
+ 'devtools-installed',
145
+ (event) => {
146
+ setPluginSections((prevSections) =>
147
+ prevSections.map((section) => ({
148
+ ...section,
149
+ cards: section.cards.map((card) =>
150
+ card.devtoolsPackage === event.payload.packageName
151
+ ? {
152
+ ...card,
153
+ status: event.payload.success ? 'success' : 'error',
154
+ error: event.payload.error,
155
+ }
156
+ : card,
157
+ ),
158
+ })),
159
+ )
160
+ },
161
+ )
162
+
163
+ // Listen for plugin added results
164
+ const cleanupPluginAdded = devtoolsEventClient.on(
165
+ 'plugin-added',
166
+ (event) => {
167
+ setPluginSections((prevSections) =>
168
+ prevSections.map((section) => ({
169
+ ...section,
170
+ cards: section.cards.map((card) =>
171
+ card.devtoolsPackage === event.payload.packageName
172
+ ? {
173
+ ...card,
174
+ status: event.payload.success ? 'success' : 'error',
175
+ error: event.payload.error,
176
+ }
177
+ : card,
178
+ ),
179
+ })),
180
+ )
181
+
182
+ // When plugin is successfully added, recalculate to move it to active section
183
+ if (event.payload.success) {
184
+ const pkg = currentPackageJson()
185
+ if (pkg) {
186
+ updatePluginCards(pkg)
187
+ }
188
+ }
189
+ },
190
+ )
191
+
192
+ onCleanup(() => {
193
+ cleanupJsonRead()
194
+ cleanupJsonUpdated()
195
+ cleanupDevtoolsInstalled()
196
+ cleanupPluginAdded()
197
+ })
198
+ // Emit mounted event to trigger package.json read
199
+ devtoolsEventClient.emit('mounted', undefined)
200
+ })
201
+
202
+ const updatePluginCards = (pkg: PackageJson | null) => {
203
+ if (!pkg) return
204
+
205
+ const currentFramework = detectFramework(pkg, FRAMEWORKS)
206
+
207
+ // Get list of registered plugin names
208
+ const registeredPlugins = new Set(plugins()?.map((p) => p.id || '') || [])
209
+
210
+ const allCards = buildPluginCards(
211
+ pkg,
212
+ currentFramework,
213
+ registeredPlugins,
214
+ pluginSections().flatMap((s) => s.cards),
215
+ )
216
+
217
+ const sections = groupIntoSections(allCards)
218
+ setPluginSections(sections)
219
+ }
220
+
221
+ const handleAction = (card: PluginCard) => {
222
+ if (
223
+ card.actionType === 'requires-package' ||
224
+ card.actionType === 'wrong-framework' ||
225
+ card.actionType === 'already-installed' ||
226
+ card.actionType === 'version-mismatch'
227
+ ) {
228
+ // Can't install devtools without the base package, wrong framework, already installed, or version mismatch
229
+ return
230
+ }
231
+
232
+ // change state to installing of the plugin user clicked
233
+ setPluginSections((prevSections) =>
234
+ prevSections.map((section) => ({
235
+ ...section,
236
+ cards: section.cards.map((c) =>
237
+ c.devtoolsPackage === card.devtoolsPackage
238
+ ? { ...c, status: 'installing' }
239
+ : c,
240
+ ),
241
+ })),
242
+ )
243
+
244
+ // Bump the version of the required package and then add to devtools
245
+ if (card.actionType === 'bump-version') {
246
+ // emits the event to vite plugin to bump the package version, this will add it to devtools after
247
+ devtoolsEventClient.emit('bump-package-version', {
248
+ packageName: card.requiredPackageName,
249
+ devtoolsPackage: card.devtoolsPackage,
250
+ pluginName: card.metadata?.title || card.devtoolsPackage,
251
+ minVersion: card.metadata?.requires?.minVersion,
252
+ pluginImport: card.metadata?.pluginImport,
253
+ })
254
+ return
255
+ }
256
+
257
+ if (card.actionType === 'add-to-devtools') {
258
+ // emits the event to vite plugin to add the plugin
259
+ devtoolsEventClient.emit('add-plugin-to-devtools', {
260
+ packageName: card.devtoolsPackage,
261
+ // should always be defined
262
+ pluginName: card.metadata?.title ?? card.devtoolsPackage,
263
+ pluginImport: card.metadata?.pluginImport,
264
+ })
265
+ return
266
+ }
267
+ devtoolsEventClient.emit('install-devtools', {
268
+ packageName: card.devtoolsPackage,
269
+ // should always be defined
270
+ pluginName: card.metadata?.title ?? card.devtoolsPackage,
271
+ pluginImport: card.metadata?.pluginImport,
272
+ })
273
+ }
274
+
275
+ // Get all available tags from plugins (excluding active plugins)
276
+ const getAllTags = () => {
277
+ const tags = new Set<string>()
278
+ pluginSections().forEach((section) => {
279
+ // Only get tags from featured and available sections, not active plugins
280
+ if (section.id === 'featured' || section.id === 'available') {
281
+ section.cards.forEach((card) => {
282
+ if (card.metadata?.tags) {
283
+ card.metadata.tags.forEach((tag) => tags.add(tag))
284
+ }
285
+ })
286
+ }
287
+ })
288
+ return Array.from(tags).sort()
289
+ }
290
+
291
+ const toggleTag = (tag: string) => {
292
+ setSelectedTags((prev) => {
293
+ const newTags = new Set(prev)
294
+ if (newTags.has(tag)) {
295
+ newTags.delete(tag)
296
+ } else {
297
+ newTags.add(tag)
298
+ }
299
+ return newTags
300
+ })
301
+ }
302
+
303
+ return (
304
+ <div class={styles().pluginMarketplace}>
305
+ {/* Settings Panel */}
306
+ <SettingsPanel
307
+ isOpen={isSettingsOpen}
308
+ onClose={() => setIsSettingsOpen(false)}
309
+ showActivePlugins={showActivePlugins}
310
+ setShowActivePlugins={setShowActivePlugins}
311
+ />
312
+
313
+ <MarketplaceHeader
314
+ searchInput={searchInput}
315
+ onSearchInput={handleSearchInput}
316
+ onSettingsClick={() => setIsSettingsOpen(!isSettingsOpen())}
317
+ tags={getAllTags}
318
+ selectedTags={selectedTags}
319
+ onToggleTag={toggleTag}
320
+ />
321
+
322
+ <Show when={getFilteredSections().length > 0}>
323
+ <For each={getFilteredSections()}>
324
+ {(section) => (
325
+ <PluginSectionComponent
326
+ section={section}
327
+ isCollapsed={() => collapsedSections().has(section.id)}
328
+ onToggleCollapse={() => toggleSection(section.id)}
329
+ onCardAction={handleAction}
330
+ />
331
+ )}
332
+ </For>
333
+ </Show>
334
+
335
+ <Show when={getFilteredSections().length === 0}>
336
+ <div class={styles().pluginMarketplaceEmpty}>
337
+ <p class={styles().pluginMarketplaceEmptyText}>
338
+ {searchQuery()
339
+ ? `No plugins found matching "${searchQuery()}"`
340
+ : 'No additional plugins available. You have all compatible devtools installed and registered!'}
341
+ </p>
342
+ </div>
343
+ </Show>
344
+ </div>
345
+ )
346
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Plugin Registry - Metadata for TanStack and third-party devtools plugins
3
+ *
4
+ * This registry allows plugin authors to customize their marketplace cards with:
5
+ * - Custom titles and descriptions
6
+ * - Logo/image URLs
7
+ * - Minimum version requirements (semver)
8
+ * - External links
9
+ *
10
+ * External companies can open PRs to add their plugins here!
11
+ */
12
+
13
+ export interface PluginMetadata {
14
+ /** Package name (e.g., '@tanstack/react-query-devtools') */
15
+ packageName: string
16
+
17
+ /** Display title shown in the marketplace card */
18
+ title: string
19
+
20
+ /** Short description of what the plugin does */
21
+ description?: string
22
+
23
+ /** URL to a logo image (SVG, PNG, etc.) */
24
+ logoUrl?: string
25
+
26
+ /** Required package dependency */
27
+ requires?: {
28
+ /** Required package name (e.g., '@tanstack/react-query') */
29
+ packageName: string
30
+ /** Minimum required version (semver) */
31
+ minVersion: string
32
+ /** Maximum version (if there's a known breaking change) */
33
+ maxVersion?: string
34
+ }
35
+
36
+ /** Plugin import configuration */
37
+ pluginImport?: {
38
+ /** The exact name to import from the package (e.g., 'FormDevtoolsPlugin' or 'ReactQueryDevtoolsPanel') */
39
+ importName: string
40
+ /** Whether this is a JSX component or a function that returns a plugin */
41
+ type: 'jsx' | 'function'
42
+ }
43
+
44
+ /** Custom plugin ID pattern for matching against registered plugins in devtools.
45
+ * If specified, this will be used to match plugin IDs that may have suffixes.
46
+ * For example, pluginId: "tanstack-form" will match "tanstack-form-4" in devtools.
47
+ * The default behavior of devtools is to lowercase the package name and replace non-alphanumeric characters with -
48
+ * so if the name of the plugin is "My awesome Plugin", the default pluginId would be "my-awesome-plugin".
49
+ */
50
+ pluginId?: string
51
+
52
+ /** Official documentation URL / URL to the docs on how to implement devtools */
53
+ docsUrl?: string
54
+
55
+ /** Plugin author/maintainer */
56
+ author?: string
57
+
58
+ /** Repository URL */
59
+ repoUrl?: string
60
+
61
+ /** Framework this plugin supports */
62
+ framework: 'react' | 'solid' | 'vue' | 'svelte' | 'angular' | 'other'
63
+
64
+ /** Mark as featured to appear in the Featured section with animated border */
65
+ featured?: boolean
66
+
67
+ /** Mark as new to show a "New" banner on the card */
68
+ isNew?: boolean
69
+
70
+ /** Tags for filtering and categorization */
71
+ tags?: Array<string>
72
+ }
73
+
74
+ /**
75
+ * Registry of all known devtools plugins
76
+ * External contributors: Add your plugin metadata here!
77
+ */
78
+ const PLUGIN_REGISTRY: Record<string, PluginMetadata> = {
79
+ // TanStack Query
80
+ '@tanstack/react-query-devtools': {
81
+ packageName: '@tanstack/react-query-devtools',
82
+ title: 'TanStack Query Devtools',
83
+ description:
84
+ 'Powerful devtools for TanStack Query - inspect queries, mutations, and cache',
85
+ requires: {
86
+ packageName: '@tanstack/react-query',
87
+ minVersion: '5.0.0',
88
+ },
89
+ pluginId: 'tanstack-query',
90
+ docsUrl: 'https://tanstack.com/query/latest/docs/devtools',
91
+ author: 'TanStack',
92
+ framework: 'react',
93
+ featured: true, // Featured plugin
94
+ tags: ['TanStack', 'data-fetching', 'caching', 'state-management'],
95
+ },
96
+ '@tanstack/solid-query-devtools': {
97
+ packageName: '@tanstack/solid-query-devtools',
98
+ title: 'TanStack Query Devtools',
99
+ description:
100
+ 'Powerful devtools for TanStack Query - inspect queries, mutations, and cache',
101
+ requires: {
102
+ packageName: '@tanstack/solid-query',
103
+ minVersion: '5.0.0',
104
+ },
105
+ pluginId: 'tanstack-query',
106
+ docsUrl: 'https://tanstack.com/query/latest/docs/devtools',
107
+ author: 'TanStack',
108
+ framework: 'solid',
109
+ tags: ['TanStack', 'data-fetching', 'caching', 'state-management'],
110
+ },
111
+
112
+ // TanStack Router
113
+ '@tanstack/react-router-devtools': {
114
+ packageName: '@tanstack/react-router-devtools',
115
+ title: 'TanStack Router Devtools',
116
+ description: 'Inspect routes, navigation, and router state in real-time',
117
+ requires: {
118
+ packageName: '@tanstack/react-router',
119
+ minVersion: '1.0.0',
120
+ },
121
+ pluginId: 'tanstack-router',
122
+ docsUrl: 'https://tanstack.com/router/latest/docs/devtools',
123
+ author: 'TanStack',
124
+ framework: 'react',
125
+ featured: true, // Featured plugin
126
+ tags: ['TanStack', 'routing', 'navigation'],
127
+ },
128
+ '@tanstack/solid-router-devtools': {
129
+ packageName: '@tanstack/solid-router-devtools',
130
+ title: 'TanStack Router Devtools',
131
+ description: 'Inspect routes, navigation, and router state in real-time',
132
+ requires: {
133
+ packageName: '@tanstack/solid-router',
134
+ minVersion: '1.0.0',
135
+ },
136
+ pluginId: 'tanstack-router',
137
+ docsUrl: 'https://tanstack.com/router/latest/docs/devtools',
138
+ author: 'TanStack',
139
+ framework: 'solid',
140
+ tags: ['TanStack', 'routing', 'navigation'],
141
+ },
142
+
143
+ // TanStack Form
144
+ '@tanstack/react-form-devtools': {
145
+ packageName: '@tanstack/react-form-devtools',
146
+ title: 'TanStack Form Devtools',
147
+ description: 'Debug form state, validation, and field values',
148
+ requires: {
149
+ packageName: '@tanstack/react-form',
150
+ minVersion: '1.23.0',
151
+ },
152
+ pluginImport: {
153
+ importName: 'FormDevtoolsPlugin',
154
+ type: 'function',
155
+ },
156
+ pluginId: 'tanstack-form',
157
+ docsUrl: 'https://tanstack.com/form/latest/docs/devtools',
158
+ author: 'TanStack',
159
+ framework: 'react',
160
+ isNew: true,
161
+ tags: ['TanStack', 'forms', 'validation'],
162
+ },
163
+ '@tanstack/solid-form-devtools': {
164
+ packageName: '@tanstack/solid-form-devtools',
165
+ title: 'TanStack Form Devtools',
166
+ description: 'Debug form state, validation, and field values',
167
+ requires: {
168
+ packageName: '@tanstack/solid-form',
169
+ minVersion: '1.23.0',
170
+ },
171
+ pluginImport: {
172
+ importName: 'FormDevtoolsPlugin',
173
+ type: 'function',
174
+ },
175
+ pluginId: 'tanstack-form',
176
+ docsUrl: 'https://tanstack.com/form/latest/docs/devtools',
177
+ author: 'TanStack',
178
+ isNew: true,
179
+ framework: 'solid',
180
+ tags: ['TanStack', 'forms', 'validation'],
181
+ },
182
+
183
+ // TanStack Pacer (Example - adjust as needed)
184
+ '@tanstack/react-pacer-devtools': {
185
+ packageName: '@tanstack/react-pacer-devtools',
186
+ title: 'Pacer Devtools',
187
+ description: 'Monitor and debug your Pacer animations and transitions',
188
+ requires: {
189
+ packageName: '@tanstack/react-pacer',
190
+ minVersion: '0.16.4',
191
+ },
192
+ author: 'TanStack',
193
+ framework: 'react',
194
+ isNew: true, // New plugin banner
195
+ tags: ['TanStack'],
196
+ },
197
+ '@tanstack/solid-pacer-devtools': {
198
+ packageName: '@tanstack/solid-pacer-devtools',
199
+ title: 'Pacer Devtools',
200
+ description: 'Monitor and debug your Pacer animations and transitions',
201
+ requires: {
202
+ packageName: '@tanstack/solid-pacer',
203
+ minVersion: '0.14.4',
204
+ },
205
+ author: 'TanStack',
206
+ framework: 'solid',
207
+ isNew: true, // New plugin banner
208
+ tags: ['TanStack'],
209
+ },
210
+
211
+ // ==========================================
212
+ // THIRD-PARTY PLUGINS - Examples
213
+ // ==========================================
214
+ // External contributors can add their plugins below!
215
+ }
216
+
217
+ /**
218
+ * Get all registered plugin metadata
219
+ */
220
+ export function getAllPluginMetadata(): Array<PluginMetadata> {
221
+ return Object.values(PLUGIN_REGISTRY)
222
+ }