From 790269cabde0fb16c0f720b34909a66cf642d4ae Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Wed, 15 Oct 2025 14:23:57 +0200 Subject: [PATCH 1/7] feat: add plugin marketplace and no plugins fallback --- packages/devtools-client/src/index.ts | 11 + packages/devtools-vite/src/plugin.ts | 61 ++ packages/devtools-vite/src/utils.ts | 14 + packages/devtools/package.json | 3 +- packages/devtools/src/styles/use-styles.ts | 580 +++++++++++++++ .../devtools/src/tabs/no-plugins-fallback.tsx | 300 ++++++++ .../devtools/src/tabs/plugin-marketplace.tsx | 665 ++++++++++++++++++ packages/devtools/src/tabs/plugins-tab.tsx | 170 +++-- pnpm-lock.yaml | 5 + 9 files changed, 1744 insertions(+), 65 deletions(-) create mode 100644 packages/devtools/src/tabs/no-plugins-fallback.tsx create mode 100644 packages/devtools/src/tabs/plugin-marketplace.tsx diff --git a/packages/devtools-client/src/index.ts b/packages/devtools-client/src/index.ts index 939132e8..652a13a5 100644 --- a/packages/devtools-client/src/index.ts +++ b/packages/devtools-client/src/index.ts @@ -55,6 +55,17 @@ interface EventMap { packageJson: PackageJson | null } 'tanstack-devtools-core:mounted': void + 'tanstack-devtools-core:install-devtools': { + packageName: string + } + 'tanstack-devtools-core:devtools-installed': { + packageName: string + success: boolean + error?: string + } + 'tanstack-devtools-core:package-json-updated': { + packageJson: PackageJson | null + } } export class DevtoolsEventClient extends EventClient { diff --git a/packages/devtools-vite/src/plugin.ts b/packages/devtools-vite/src/plugin.ts index 1110afe2..e707941b 100644 --- a/packages/devtools-vite/src/plugin.ts +++ b/packages/devtools-vite/src/plugin.ts @@ -86,6 +86,57 @@ const emitOutdatedDeps = async () => { }) } +const installPackage = async ( + packageName: string, +): Promise<{ + success: boolean + error?: string +}> => { + return new Promise((resolve) => { + // Detect package manager + exec('npm --version', (error) => { + const packageManager = error ? 'pnpm' : 'npm' + const installCommand = `${packageManager} install -D ${packageName}` + + console.log( + chalk.blueBright( + `[@tanstack/devtools-vite] Installing ${packageName}...`, + ), + ) + + exec(installCommand, async (installError) => { + if (installError) { + console.error( + chalk.redBright( + `[@tanstack/devtools-vite] Failed to install ${packageName}:`, + ), + installError.message, + ) + resolve({ + success: false, + error: installError.message, + }) + return + } + + console.log( + chalk.greenBright( + `[@tanstack/devtools-vite] Successfully installed ${packageName}`, + ), + ) + + // Read the updated package.json and emit the event + const updatedPackageJson = await readPackageJson() + devtoolsEventClient.emit('package-json-updated', { + packageJson: updatedPackageJson, + }) + + resolve({ success: true }) + }) + }) + }) +} + export const devtools = (args?: TanStackDevtoolsViteConfig): Array => { let port = 5173 const logging = args?.logging ?? true @@ -231,6 +282,16 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array => { const packageJson = await readPackageJson() const outdatedDeps = emitOutdatedDeps().then((deps) => deps) + // Listen for package installation requests + devtoolsEventClient.on('install-devtools', async (event) => { + const result = await installPackage(event.payload.packageName) + devtoolsEventClient.emit('devtools-installed', { + packageName: event.payload.packageName, + success: result.success, + error: result.error, + }) + }) + // whenever a client mounts we send all the current info to the subscribers devtoolsEventClient.on('mounted', async () => { devtoolsEventClient.emit('outdated-deps-read', { diff --git a/packages/devtools-vite/src/utils.ts b/packages/devtools-vite/src/utils.ts index bc11747c..0fa90db0 100644 --- a/packages/devtools-vite/src/utils.ts +++ b/packages/devtools-vite/src/utils.ts @@ -92,3 +92,17 @@ export const tryParseJson = ( export const readPackageJson = async () => tryParseJson(await tryReadFile(process.cwd() + '/package.json')) + +export const writePackageJson = async (packageJson: PackageJson) => { + try { + await fs.writeFile( + process.cwd() + '/package.json', + JSON.stringify(packageJson, null, 2) + '\n', + 'utf-8', + ) + return true + } catch (error) { + console.error('Failed to write package.json:', error) + return false + } +} diff --git a/packages/devtools/package.json b/packages/devtools/package.json index 5408efcd..a5f1a416 100644 --- a/packages/devtools/package.json +++ b/packages/devtools/package.json @@ -60,6 +60,7 @@ }, "dependencies": { "@solid-primitives/keyboard": "^1.3.3", + "@tanstack/devtools-client": "workspace:*", "@tanstack/devtools-event-bus": "workspace:*", "@tanstack/devtools-ui": "workspace:*", "clsx": "^2.1.1", @@ -74,4 +75,4 @@ "tsup-preset-solid": "^2.2.0", "vite-plugin-solid": "^2.11.8" } -} +} \ No newline at end of file diff --git a/packages/devtools/src/styles/use-styles.ts b/packages/devtools/src/styles/use-styles.ts index e9262562..20f2f16d 100644 --- a/packages/devtools/src/styles/use-styles.ts +++ b/packages/devtools/src/styles/use-styles.ts @@ -390,6 +390,8 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { width: ${size[48]}; overflow-y: auto; transform: ${isExpanded ? 'translateX(0)' : 'translateX(-100%)'}; + display: flex; + flex-direction: column; `, pluginsTabSidebarTransition: (mSeconds: number) => { @@ -398,6 +400,11 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { ` }, + pluginsList: css` + flex: 1; + overflow-y: auto; + `, + pluginName: css` font-size: ${fontSize.xs}; font-family: ${fontFamily.sans}; @@ -459,6 +466,579 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { display: flex; gap: 0.5rem; `, + + // No Plugins Fallback Styles + noPluginsFallback: css` + display: flex; + align-items: center; + justify-content: center; + min-height: 400px; + padding: 2rem; + background: ${t(colors.gray[50], colors.darkGray[700])}; + width: 100%; + height: 100%; + `, + noPluginsFallbackContent: css` + max-width: 600px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + `, + noPluginsFallbackIcon: css` + width: 64px; + height: 64px; + color: ${t(colors.gray[400], colors.gray[600])}; + margin-bottom: 0.5rem; + + svg { + width: 100%; + height: 100%; + } + `, + noPluginsFallbackTitle: css` + font-size: 1.5rem; + font-weight: 600; + color: ${t(colors.gray[900], colors.gray[100])}; + margin: 0; + `, + noPluginsFallbackDescription: css` + font-size: 0.95rem; + color: ${t(colors.gray[600], colors.gray[400])}; + line-height: 1.5; + margin: 0; + `, + noPluginsSuggestions: css` + width: 100%; + margin-top: 1.5rem; + padding: 1.5rem; + background: ${t(colors.white, colors.darkGray[800])}; + border: 1px solid ${t(colors.gray[200], colors.gray[700])}; + border-radius: 0.5rem; + `, + noPluginsSuggestionsTitle: css` + font-size: 1.125rem; + font-weight: 600; + color: ${t(colors.gray[900], colors.gray[100])}; + margin: 0 0 0.5rem 0; + `, + noPluginsSuggestionsDesc: css` + font-size: 0.875rem; + color: ${t(colors.gray[600], colors.gray[400])}; + margin: 0 0 1rem 0; + `, + noPluginsSuggestionsList: css` + display: flex; + flex-direction: column; + gap: 0.75rem; + `, + noPluginsSuggestionCard: css` + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + background: ${t(colors.gray[50], colors.darkGray[900])}; + border: 1px solid ${t(colors.gray[200], colors.gray[700])}; + border-radius: 0.375rem; + transition: all 0.15s ease; + + &:hover { + border-color: ${t(colors.gray[300], colors.gray[600])}; + background: ${t(colors.gray[100], colors.darkGray[800])}; + } + `, + noPluginsSuggestionInfo: css` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + flex: 1; + `, + noPluginsSuggestionPackage: css` + font-size: 0.95rem; + font-weight: 600; + color: ${t(colors.gray[900], colors.gray[100])}; + margin: 0; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + `, + noPluginsSuggestionSource: css` + font-size: 0.8rem; + color: ${t(colors.gray[500], colors.gray[500])}; + margin: 0; + `, + noPluginsSuggestionStatus: css` + display: flex; + align-items: center; + gap: 0.5rem; + color: ${t(colors.green[600], colors.green[400])}; + + svg { + width: 18px; + height: 18px; + } + `, + noPluginsSuggestionStatusText: css` + font-size: 0.875rem; + font-weight: 500; + `, + noPluginsSuggestionStatusTextError: css` + font-size: 0.875rem; + font-weight: 500; + color: ${t(colors.red[600], colors.red[400])}; + `, + noPluginsEmptyState: css` + margin-top: 1.5rem; + padding: 1.5rem; + background: ${t(colors.white, colors.darkGray[800])}; + border: 1px solid ${t(colors.gray[200], colors.gray[700])}; + border-radius: 0.5rem; + `, + noPluginsEmptyStateText: css` + font-size: 0.875rem; + color: ${t(colors.gray[600], colors.gray[400])}; + margin: 0; + line-height: 1.5; + `, + noPluginsFallbackLinks: css` + display: flex; + align-items: center; + gap: 0.75rem; + margin-top: 1.5rem; + `, + noPluginsFallbackLink: css` + font-size: 0.875rem; + color: ${t(colors.gray[700], colors.gray[300])}; + text-decoration: none; + transition: color 0.15s ease; + + &:hover { + color: ${t(colors.gray[900], colors.gray[100])}; + text-decoration: underline; + } + `, + noPluginsFallbackLinkSeparator: css` + color: ${t(colors.gray[400], colors.gray[600])}; + `, + + // Plugin Marketplace Styles (for "Add More" tab) + pluginMarketplace: css` + width: 100%; + height: 100%; + overflow-y: auto; + padding: 2rem; + background: ${t( + 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)', + 'linear-gradient(135deg, #1a1d23 0%, #13161a 100%)', + )}; + animation: fadeIn 0.3s ease; + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + `, + pluginMarketplaceHeader: css` + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 2px solid ${t(colors.gray[200], colors.gray[700])}; + `, + pluginMarketplaceTitle: css` + font-size: 1.5rem; + font-weight: 700; + color: ${t(colors.gray[900], colors.gray[100])}; + margin: 0 0 0.5rem 0; + letter-spacing: -0.02em; + `, + pluginMarketplaceDescription: css` + font-size: 0.95rem; + color: ${t(colors.gray[600], colors.gray[400])}; + margin: 0 0 1rem 0; + line-height: 1.5; + `, + pluginMarketplaceSearchWrapper: css` + position: relative; + display: flex; + align-items: center; + margin-top: 1rem; + + svg { + position: absolute; + left: 1rem; + color: ${t(colors.gray[400], colors.gray[500])}; + pointer-events: none; + } + `, + pluginMarketplaceSearch: css` + width: 100%; + padding: 0.75rem 1rem 0.75rem 2.75rem; + background: ${t(colors.gray[50], colors.darkGray[900])}; + border: 2px solid ${t(colors.gray[200], colors.gray[700])}; + border-radius: 0.5rem; + color: ${t(colors.gray[900], colors.gray[100])}; + font-size: 0.95rem; + font-family: ${fontFamily.sans}; + transition: all 0.2s ease; + + &::placeholder { + color: ${t(colors.gray[400], colors.gray[500])}; + } + + &:focus { + outline: none; + border-color: ${t(colors.blue[500], colors.blue[400])}; + background: ${t(colors.white, colors.darkGray[800])}; + box-shadow: 0 0 0 3px + ${t('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.1)')}; + } + `, + pluginMarketplaceGrid: css` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.25rem; + animation: slideUp 0.4s ease; + + @keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + `, + pluginMarketplaceCard: css` + background: ${t(colors.white, colors.darkGray[800])}; + border: 2px solid ${t(colors.gray[200], colors.gray[700])}; + border-radius: 0.75rem; + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: ${t( + 'linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%)', + 'linear-gradient(90deg, #60a5fa 0%, #a78bfa 100%)', + )}; + transform: scaleX(0); + transform-origin: left; + transition: transform 0.25s ease; + } + + &:hover { + border-color: ${t(colors.gray[400], colors.gray[500])}; + box-shadow: 0 8px 24px ${t('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.4)')}; + transform: translateY(-4px); + + &::before { + transform: scaleX(1); + } + } + `, + pluginMarketplaceCardIcon: css` + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: ${t( + 'linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)', + 'linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%)', + )}; + border-radius: 0.5rem; + color: white; + transition: transform 0.25s ease; + + svg { + width: 20px; + height: 20px; + } + `, + pluginMarketplaceCardHeader: css` + flex: 1; + `, + pluginMarketplaceCardTitle: css` + font-size: 0.95rem; + font-weight: 600; + color: ${t(colors.gray[900], colors.gray[100])}; + margin: 0 0 0.5rem 0; + line-height: 1.4; + `, + pluginMarketplaceCardDescription: css` + font-size: 0.8rem; + color: ${t(colors.gray[500], colors.gray[500])}; + margin: 0; + padding: 0.375rem 0.75rem; + background: ${t(colors.gray[100], colors.gray[900])}; + border-radius: 0.375rem; + display: inline-block; + font-weight: 500; + `, + pluginMarketplaceCardStatus: css` + display: flex; + align-items: center; + gap: 0.5rem; + color: ${t(colors.green[600], colors.green[400])}; + animation: statusFadeIn 0.3s ease; + + @keyframes statusFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + svg { + width: 18px; + height: 18px; + animation: iconPop 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); + } + + @keyframes iconPop { + 0% { + transform: scale(0); + } + 50% { + transform: scale(1.2); + } + 100% { + transform: scale(1); + } + } + `, + pluginMarketplaceCardSpinner: css` + width: 18px; + height: 18px; + border: 2px solid ${t(colors.gray[200], colors.gray[700])}; + border-top-color: ${t(colors.blue[600], colors.blue[400])}; + border-radius: 50%; + animation: spin 0.8s linear infinite; + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + `, + pluginMarketplaceCardStatusText: css` + font-size: 0.875rem; + font-weight: 600; + `, + pluginMarketplaceCardStatusTextError: css` + font-size: 0.875rem; + font-weight: 600; + color: ${t(colors.red[600], colors.red[400])}; + `, + pluginMarketplaceEmpty: css` + padding: 3rem 2rem; + text-align: center; + background: ${t(colors.white, colors.darkGray[800])}; + border: 2px dashed ${t(colors.gray[300], colors.gray[700])}; + border-radius: 0.75rem; + animation: fadeIn 0.3s ease; + `, + pluginMarketplaceEmptyText: css` + font-size: 0.95rem; + color: ${t(colors.gray[600], colors.gray[400])}; + margin: 0; + line-height: 1.6; + `, + + // Framework sections + pluginMarketplaceSection: css` + margin-bottom: 2.5rem; + + &:last-child { + margin-bottom: 0; + } + `, + pluginMarketplaceSectionHeader: css` + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; + user-select: none; + + &:hover { + opacity: 0.8; + } + `, + pluginMarketplaceSectionHeaderLeft: css` + display: flex; + align-items: center; + gap: 0.5rem; + `, + pluginMarketplaceSectionChevron: css` + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + color: ${t(colors.gray[600], colors.gray[400])}; + transition: transform 0.2s ease; + `, + pluginMarketplaceSectionChevronCollapsed: css` + transform: rotate(-90deg); + `, + pluginMarketplaceSectionTitle: css` + font-size: 1.125rem; + font-weight: 600; + color: ${t(colors.gray[900], colors.gray[100])}; + margin: 0; + display: flex; + align-items: center; + gap: 0.5rem; + `, + pluginMarketplaceSectionBadge: css` + font-size: 0.75rem; + font-weight: 600; + padding: 0.25rem 0.5rem; + background: ${t( + 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', + 'linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%)', + )}; + color: white; + border-radius: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.05em; + `, + pluginMarketplaceCardDisabled: css` + opacity: 0.6; + filter: grayscale(0.3); + cursor: not-allowed; + + &:hover { + transform: none; + box-shadow: none; + } + `, + + // Card state badges + pluginMarketplaceCardBadge: css` + position: absolute; + top: 1rem; + right: 1rem; + padding: 0.25rem 0.5rem; + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; + border-radius: 0.25rem; + letter-spacing: 0.05em; + `, + pluginMarketplaceCardBadgeInstall: css` + background: ${t(colors.green[100], colors.green[900])}; + color: ${t(colors.green[700], colors.green[300])}; + `, + pluginMarketplaceCardBadgeAdd: css` + background: ${t(colors.blue[100], colors.blue[900])}; + color: ${t(colors.blue[700], colors.blue[300])}; + `, + pluginMarketplaceCardBadgeRequires: css` + background: ${t(colors.gray[100], colors.gray[800])}; + color: ${t(colors.gray[600], colors.gray[400])}; + `, + + // Add More Tab Style (visually distinct from regular plugins) + pluginNameAddMore: css` + font-size: ${fontSize.xs}; + font-family: ${fontFamily.sans}; + color: ${t(colors.gray[600], colors.gray[400])}; + padding: ${size[3]} ${size[2]}; + cursor: pointer; + text-align: center; + border-top: 2px solid ${t(colors.gray[200], colors.gray[700])}; + border-left: 2px solid transparent; + background: ${t( + 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)', + 'linear-gradient(135deg, #1f2937 0%, #111827 100%)', + )}; + font-weight: 600; + position: relative; + margin-top: auto; + + h3 { + margin: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + + &::before { + content: '✨'; + font-size: 0.875rem; + animation: sparkle 2s ease-in-out infinite; + } + } + + @keyframes sparkle { + 0%, + 100% { + opacity: 1; + transform: scale(1) rotate(0deg); + } + 50% { + opacity: 0.6; + transform: scale(1.1) rotate(10deg); + } + } + + &:hover { + background: ${t( + 'linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%)', + 'linear-gradient(135deg, #374151 0%, #1f2937 100%)', + )}; + color: ${t(colors.gray[900], colors.gray[100])}; + border-left-color: ${t(colors.blue[500], colors.blue[400])}; + + h3::before { + animation: sparkle 0.5s ease-in-out infinite; + } + } + + &.active { + background: ${t( + 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', + 'linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%)', + )}; + color: ${t(colors.white, colors.white)}; + border-left: 2px solid ${t(colors.blue[600], colors.blue[300])}; + box-shadow: 0 4px 12px + ${t('rgba(59, 130, 246, 0.3)', 'rgba(96, 165, 250, 0.3)')}; + + h3::before { + filter: brightness(0) invert(1); + } + } + + &.active:hover { + background: ${t( + 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)', + 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', + )}; + transform: translateX(4px) scale(1.02); + } + `, } } diff --git a/packages/devtools/src/tabs/no-plugins-fallback.tsx b/packages/devtools/src/tabs/no-plugins-fallback.tsx new file mode 100644 index 00000000..02970d51 --- /dev/null +++ b/packages/devtools/src/tabs/no-plugins-fallback.tsx @@ -0,0 +1,300 @@ +import { For, Show, createSignal, onMount } from 'solid-js' +import { devtoolsEventClient } from '@tanstack/devtools-client' +import { Button } from '@tanstack/devtools-ui' +import { useStyles } from '../styles/use-styles' +import type { PackageJson } from '@tanstack/devtools-client' + +// Map of TanStack packages to their devtools packages +const TANSTACK_DEVTOOLS_MAP: Record = { + '@tanstack/react-query': '@tanstack/react-query-devtools', + '@tanstack/solid-query': '@tanstack/solid-query-devtools', + '@tanstack/vue-query': '@tanstack/vue-query-devtools', + '@tanstack/svelte-query': '@tanstack/svelte-query-devtools', + '@tanstack/angular-query-experimental': + '@tanstack/angular-query-devtools-experimental', + '@tanstack/react-router': '@tanstack/router-devtools', + '@tanstack/react-table': '@tanstack/react-table-devtools', + '@tanstack/solid-table': '@tanstack/solid-table-devtools', + '@tanstack/vue-table': '@tanstack/vue-table-devtools', + '@tanstack/svelte-table': '@tanstack/svelte-table-devtools', +} + +type InstallStatus = 'idle' | 'installing' | 'success' | 'error' + +interface SuggestedPlugin { + packageName: string + devtoolsPackage: string + status: InstallStatus + error?: string +} + +// Simple icon components +const PackageIcon = () => ( + + + +) + +const CheckCircleIcon = () => ( + + + +) + +const XCircleIcon = () => ( + + + +) + +export const NoPluginsFallback = () => { + const styles = useStyles() + const [packageJson, setPackageJson] = createSignal(null) + const [suggestedPlugins, setSuggestedPlugins] = createSignal< + Array + >([]) + + onMount(() => { + // Listen for package.json updates + devtoolsEventClient.on('package-json-read', (event) => { + setPackageJson(event.payload.packageJson) + updateSuggestedPlugins(event.payload.packageJson) + }) + + devtoolsEventClient.on('package-json-updated', (event) => { + setPackageJson(event.payload.packageJson) + updateSuggestedPlugins(event.payload.packageJson) + }) + + // Listen for installation results + devtoolsEventClient.on('devtools-installed', (event) => { + setSuggestedPlugins((prev) => + prev.map((plugin) => + plugin.devtoolsPackage === event.payload.packageName + ? { + ...plugin, + status: event.payload.success ? 'success' : 'error', + error: event.payload.error, + } + : plugin, + ), + ) + + // Remove successfully installed packages after a delay + if (event.payload.success) { + setTimeout(() => { + setSuggestedPlugins((prev) => + prev.filter( + (plugin) => plugin.devtoolsPackage !== event.payload.packageName, + ), + ) + }, 3000) + } + }) + + // Emit mounted event to trigger package.json read + devtoolsEventClient.emit('mounted', undefined) + }) + + const updateSuggestedPlugins = (pkg: PackageJson | null) => { + if (!pkg) return + + const allDeps = { + ...pkg.dependencies, + ...pkg.devDependencies, + } + + const suggestions: Array = [] + + Object.entries(TANSTACK_DEVTOOLS_MAP).forEach( + ([sourcePackage, devtoolsPackage]) => { + // Check if the source package is installed but devtools is not + if (allDeps[sourcePackage] && !allDeps[devtoolsPackage]) { + // Only add if not already in the list + const existing = suggestedPlugins().find( + (p) => p.devtoolsPackage === devtoolsPackage, + ) + if (!existing) { + suggestions.push({ + packageName: sourcePackage, + devtoolsPackage: devtoolsPackage, + status: 'idle', + }) + } else { + // Preserve existing status + suggestions.push(existing) + } + } + }, + ) + + setSuggestedPlugins(suggestions) + } + + const handleInstall = (plugin: SuggestedPlugin) => { + setSuggestedPlugins((prev) => + prev.map((p) => + p.devtoolsPackage === plugin.devtoolsPackage + ? { ...p, status: 'installing' } + : p, + ), + ) + + devtoolsEventClient.emit('install-devtools', { + packageName: plugin.devtoolsPackage, + }) + } + + return ( +
+
+
+ +
+ +

No Plugins Registered

+ +

+ You don't have any devtools plugins registered yet. TanStack Devtools + can enhance your development experience with specialized tools for + TanStack libraries. +

+ + 0}> +
+

+ Suggested Plugins +

+

+ We detected the following TanStack packages in your project. Would + you like to install their devtools? +

+ +
+ + {(plugin) => ( +
+
+

+ {plugin.devtoolsPackage} +

+

+ For {plugin.packageName} +

+
+ + + + + Installing... + + + + + + Installed! + + + + + + {plugin.error || 'Failed to install'} + + +
+ } + > + + +
+ )} + +
+
+ + + +
+

+ No TanStack packages detected in your project. Install a TanStack + library and its corresponding devtools package to get started. +

+
+
+ + +
+ + ) +} diff --git a/packages/devtools/src/tabs/plugin-marketplace.tsx b/packages/devtools/src/tabs/plugin-marketplace.tsx new file mode 100644 index 00000000..528d8956 --- /dev/null +++ b/packages/devtools/src/tabs/plugin-marketplace.tsx @@ -0,0 +1,665 @@ +import { For, Show, createSignal, onMount } from 'solid-js' +import { devtoolsEventClient } from '@tanstack/devtools-client' +import { Button } from '@tanstack/devtools-ui' +import { useStyles } from '../styles/use-styles' +import { usePlugins } from '../context/use-devtools-context' +import type { PackageJson } from '@tanstack/devtools-client' + +// Comprehensive list of all TanStack devtools packages +// Pattern: @tanstack/- => @tanstack/--devtools +const TANSTACK_FRAMEWORKS = ['react', 'solid', 'vue', 'svelte', 'angular'] as const +const TANSTACK_PACKAGES = ['query', 'router', 'form', 'pacer'] as const +const FRAMEWORK_AGNOSTIC_PACKAGES = ['table'] as const + +type InstallStatus = 'idle' | 'installing' | 'success' | 'error' +type ActionType = 'install' | 'add-to-devtools' | 'requires-package' | 'wrong-framework' + +interface PluginCard { + packageName: string + devtoolsPackage: string + framework: string + package: string + hasPackage: boolean + hasDevtools: boolean + isRegistered: boolean + actionType: ActionType + status: InstallStatus + error?: string + isCurrentFramework: boolean +} + +interface FrameworkSection { + framework: string + displayName: string + cards: Array + isActive: boolean +} + +// Simple icon components +const PackageIcon = () => ( + + + +) + +const CheckCircleIcon = () => ( + + + +) + +const XCircleIcon = () => ( + + + +) + +const ChevronDownIcon = () => ( + + + +) + +const SearchIcon = () => ( + + + +) + +export const PluginMarketplace = () => { + const styles = useStyles() + const { plugins } = usePlugins() + const [frameworkSections, setFrameworkSections] = createSignal>([]) + const [searchInput, setSearchInput] = createSignal('') + const [searchQuery, setSearchQuery] = createSignal('') + const [collapsedSections, setCollapsedSections] = createSignal>(new Set()) + + let debounceTimeout: ReturnType | undefined + + // Debounce search input + const handleSearchInput = (value: string) => { + setSearchInput(value) + + if (debounceTimeout) { + clearTimeout(debounceTimeout) + } + + debounceTimeout = setTimeout(() => { + setSearchQuery(value) + }, 300) + } + + const toggleSection = (framework: string) => { + setCollapsedSections((prev) => { + const newSet = new Set(prev) + if (newSet.has(framework)) { + newSet.delete(framework) + } else { + newSet.add(framework) + } + return newSet + }) + } + + const matchesSearch = (card: PluginCard, query: string): boolean => { + if (!query) return true + + const lowerQuery = query.toLowerCase() + return ( + card.devtoolsPackage.toLowerCase().includes(lowerQuery) || + card.packageName.toLowerCase().includes(lowerQuery) || + card.framework.toLowerCase().includes(lowerQuery) || + card.package.toLowerCase().includes(lowerQuery) + ) + } + + const getFilteredSections = () => { + const query = searchQuery() + if (!query) return frameworkSections() + + return frameworkSections() + .map((section) => ({ + ...section, + cards: section.cards.filter((card) => matchesSearch(card, query)), + })) + .filter((section) => section.cards.length > 0) + } + + onMount(() => { + // Listen for package.json updates + devtoolsEventClient.on('package-json-read', (event) => { + updatePluginCards(event.payload.packageJson) + }) + + devtoolsEventClient.on('package-json-updated', (event) => { + updatePluginCards(event.payload.packageJson) + }) + + // Listen for installation results + devtoolsEventClient.on('devtools-installed', (event) => { + setFrameworkSections((prevSections) => + prevSections.map((section) => ({ + ...section, + cards: section.cards.map((card) => + card.devtoolsPackage === event.payload.packageName + ? { + ...card, + status: event.payload.success ? 'success' : 'error', + error: event.payload.error, + } + : card, + ), + })), + ) + + // Refresh the list after successful installation + if (event.payload.success) { + setTimeout(() => { + devtoolsEventClient.emit('mounted', undefined) + }, 1000) + } + }) + + // Emit mounted event to trigger package.json read + devtoolsEventClient.emit('mounted', undefined) + }) + + const detectFramework = (pkg: PackageJson): string => { + const allDeps = { + ...pkg.dependencies, + ...pkg.devDependencies, + } + + // Check for framework-specific packages + for (const framework of TANSTACK_FRAMEWORKS) { + const frameworkPackages = [ + `${framework}`, + `@${framework}/core`, + `@tanstack/${framework}-query`, + `@tanstack/${framework}-router`, + `@tanstack/${framework}-form`, + `@tanstack/${framework}-table`, + ] + + if (frameworkPackages.some((pkg) => allDeps[pkg])) { + return framework + } + } + + return 'unknown' + } + + const updatePluginCards = (pkg: PackageJson | null) => { + if (!pkg) return + + const allDeps = { + ...pkg.dependencies, + ...pkg.devDependencies, + } + + const currentFramework = detectFramework(pkg) + + // Get list of registered plugin names + const registeredPlugins = new Set( + plugins()?.map((p) => { + return p.id || '' + }) || [], + ) + + const allCards: Array = [] + + // Generate all possible combinations + TANSTACK_FRAMEWORKS.forEach((framework) => { + TANSTACK_PACKAGES.forEach((pkg) => { + // Special handling for router (it's just @tanstack/router-devtools for react) + const packageName = + pkg === 'router' && framework === 'react' + ? '@tanstack/react-router' + : `@tanstack/${framework}-${pkg}` + + const devtoolsPackage = + pkg === 'router' && framework === 'react' + ? '@tanstack/router-devtools' + : `@tanstack/${framework}-${pkg}-devtools` + + const hasPackage = !!allDeps[packageName] + const hasDevtools = !!allDeps[devtoolsPackage] + const isCurrentFramework = framework === currentFramework + + // Check if plugin is registered + const isRegistered = + registeredPlugins.has(devtoolsPackage) || + registeredPlugins.has(packageName) || + Array.from(registeredPlugins).some( + (id) => id.includes(pkg) && id.includes(framework), + ) + + // Determine action type + let actionType: ActionType + if (!isCurrentFramework) { + actionType = 'wrong-framework' + } else if (!hasPackage) { + actionType = 'requires-package' + } else if (hasDevtools && !isRegistered) { + actionType = 'add-to-devtools' + } else if (!hasDevtools) { + actionType = 'install' + } else { + // Has package, has devtools, and is registered - skip + return + } + + // Find existing card to preserve status + const existing = frameworkSections() + .flatMap((s) => s.cards) + .find((c) => c.devtoolsPackage === devtoolsPackage) + + allCards.push({ + packageName, + devtoolsPackage, + framework, + package: pkg, + hasPackage, + hasDevtools, + isRegistered, + actionType, + status: existing?.status || 'idle', + error: existing?.error, + isCurrentFramework, + }) + }) + }) + + // Also add framework-agnostic packages (table) + FRAMEWORK_AGNOSTIC_PACKAGES.forEach((pkg) => { + TANSTACK_FRAMEWORKS.forEach((framework) => { + const packageName = `@tanstack/${framework}-${pkg}` + const devtoolsPackage = `@tanstack/${framework}-${pkg}-devtools` + + const hasPackage = !!allDeps[packageName] + const hasDevtools = !!allDeps[devtoolsPackage] + const isCurrentFramework = framework === currentFramework + + const isRegistered = + registeredPlugins.has(devtoolsPackage) || + registeredPlugins.has(packageName) || + Array.from(registeredPlugins).some( + (id) => id.includes(pkg) && id.includes(framework), + ) + + let actionType: ActionType + if (!isCurrentFramework) { + actionType = 'wrong-framework' + } else if (!hasPackage) { + actionType = 'requires-package' + } else if (hasDevtools && !isRegistered) { + actionType = 'add-to-devtools' + } else if (!hasDevtools) { + actionType = 'install' + } else { + return + } + + const existing = frameworkSections() + .flatMap((s) => s.cards) + .find((c) => c.devtoolsPackage === devtoolsPackage) + + allCards.push({ + packageName, + devtoolsPackage, + framework, + package: pkg, + hasPackage, + hasDevtools, + isRegistered, + actionType, + status: existing?.status || 'idle', + error: existing?.error, + isCurrentFramework, + }) + }) + }) + + // Group cards by framework + const sections: Array = [] + + TANSTACK_FRAMEWORKS.forEach((framework) => { + const frameworkCards = allCards.filter((c) => c.framework === framework) + + if (frameworkCards.length > 0) { + sections.push({ + framework, + displayName: framework.charAt(0).toUpperCase() + framework.slice(1), + cards: frameworkCards, + isActive: framework === currentFramework, + }) + } + }) + + // Sort sections: current framework first, then others + sections.sort((a, b) => { + if (a.isActive) return -1 + if (b.isActive) return 1 + return a.displayName.localeCompare(b.displayName) + }) + + setFrameworkSections(sections) + + // Collapse non-active sections by default + const newCollapsed = new Set() + sections.forEach((section) => { + if (!section.isActive) { + newCollapsed.add(section.framework) + } + }) + setCollapsedSections(newCollapsed) + } + + const handleAction = (card: PluginCard) => { + if (card.actionType === 'requires-package' || card.actionType === 'wrong-framework') { + // Can't install devtools without the base package or wrong framework + return + } + + if (card.actionType === 'add-to-devtools') { + // Show instructions to add to devtools manually + alert( + `To add ${card.devtoolsPackage} to your devtools:\n\n` + + `1. Import the package in your code\n` + + `2. Add it to the plugins array in \n\n` + + `Example:\nimport { ${card.package}Devtools } from '${card.devtoolsPackage}'\n\n` + + `plugins={[\n { name: '${card.package}', render: <${card.package}Devtools /> }\n]}`, + ) + return + } + + // Install the devtools package + setFrameworkSections((prevSections) => + prevSections.map((section) => ({ + ...section, + cards: section.cards.map((c) => + c.devtoolsPackage === card.devtoolsPackage + ? { ...c, status: 'installing' } + : c, + ), + })), + ) + + devtoolsEventClient.emit('install-devtools', { + packageName: card.devtoolsPackage, + }) + } + + const getButtonText = (card: PluginCard) => { + if (card.status === 'installing') return 'Installing...' + if (card.status === 'success') return 'Installed!' + if (card.status === 'error') return 'Error' + + switch (card.actionType) { + case 'install': + return 'Install' + case 'add-to-devtools': + return 'Add to Devtools' + case 'requires-package': + return `Requires ${card.package}` + case 'wrong-framework': + return 'Different Framework' + default: + return 'Install' + } + } + + const getButtonVariant = (card: PluginCard) => { + if ( + card.actionType === 'requires-package' || + card.actionType === 'wrong-framework' + ) + return 'secondary' + return 'primary' + } + + const getBadgeClass = (card: PluginCard) => { + const s = styles() + const base = s.pluginMarketplaceCardBadge + switch (card.actionType) { + case 'install': + return `${base} ${s.pluginMarketplaceCardBadgeInstall}` + case 'add-to-devtools': + return `${base} ${s.pluginMarketplaceCardBadgeAdd}` + case 'requires-package': + case 'wrong-framework': + return `${base} ${s.pluginMarketplaceCardBadgeRequires}` + default: + return base + } + } + + const getBadgeText = (card: PluginCard) => { + switch (card.actionType) { + case 'install': + return 'Available' + case 'add-to-devtools': + return 'Installed' + case 'requires-package': + return 'Unavailable' + case 'wrong-framework': + return 'Other Framework' + default: + return '' + } + } + + return ( +
+
+

Plugin Marketplace

+

+ Discover and install devtools for TanStack Query, Router, Form, and + Pacer +

+ +
+ + handleSearchInput(e.currentTarget.value)} + /> +
+
+ + 0}> + + {(section) => ( +
+
toggleSection(section.framework)} + > +
+
+ +
+

+ {section.displayName} + {section.isActive && ( + + Your Framework + + )} +

+
+
+ + +
+ + {(card) => ( +
+ + {getBadgeText(card)} + +
+ +
+
+

+ {card.devtoolsPackage} +

+

+ {card.actionType === 'requires-package' + ? `Requires ${card.packageName}` + : card.actionType === 'wrong-framework' + ? `For ${section.displayName} projects` + : `For ${card.packageName}`} +

+
+ + + +
+ + Installing... + + + + + + Installed! + + + + + + {card.error || 'Failed to install'} + + +
+ } + > + +
+
+ )} +
+
+
+
+ )} +
+
+ + +
+

+ {searchQuery() + ? `No plugins found matching "${searchQuery()}"` + : 'No additional plugins available. You have all compatible devtools installed and registered!'} +

+
+
+
+ ) +} diff --git a/packages/devtools/src/tabs/plugins-tab.tsx b/packages/devtools/src/tabs/plugins-tab.tsx index d88f2fec..7f589b70 100644 --- a/packages/devtools/src/tabs/plugins-tab.tsx +++ b/packages/devtools/src/tabs/plugins-tab.tsx @@ -1,9 +1,11 @@ -import { For, createEffect, createMemo, createSignal } from 'solid-js' +import { For, Show, createEffect, createMemo, createSignal } from 'solid-js' import clsx from 'clsx' import { useDrawContext } from '../context/draw-context' import { usePlugins, useTheme } from '../context/use-devtools-context' import { useStyles } from '../styles/use-styles' import { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from '../constants' +import { NoPluginsFallback } from './no-plugins-fallback' +import { PluginMarketplace } from './plugin-marketplace' export const PluginsTab = () => { const { plugins, activePlugins, toggleActivePlugins } = usePlugins() @@ -12,10 +14,19 @@ export const PluginsTab = () => { const [pluginRefs, setPluginRefs] = createSignal( new Map(), ) + const [showMarketplace, setShowMarketplace] = createSignal(false) const styles = useStyles() const { theme } = useTheme() + + const hasPlugins = createMemo( + () => plugins()?.length && plugins()!.length > 0, + ) + + // Marketplace ID for managing active state + const MARKETPLACE_ID = '__marketplace__' + createEffect(() => { const currentActivePlugins = plugins()?.filter((plugin) => activePlugins().includes(plugin.id!), @@ -30,76 +41,107 @@ export const PluginsTab = () => { }) }) + const handleMarketplaceClick = () => { + setShowMarketplace(true) + // Clear other active plugins when marketplace is selected + toggleActivePlugins(MARKETPLACE_ID) + } + + const handlePluginClick = (pluginId: string) => { + setShowMarketplace(false) + toggleActivePlugins(pluginId) + } + return ( -
-
hoverUtils.enter()} - onMouseLeave={() => hoverUtils.leave()} - > + }> +
hoverUtils.enter()} + onMouseLeave={() => hoverUtils.leave()} > - - {(plugin) => { - let pluginHeading: HTMLHeadingElement | undefined - - createEffect(() => { - if (pluginHeading) { - typeof plugin.name === 'string' - ? (pluginHeading.textContent = plugin.name) - : plugin.name(pluginHeading, theme()) - } - }) - - const isActive = createMemo(() => - activePlugins().includes(plugin.id!), - ) - - return ( +
+
+ + {(plugin) => { + let pluginHeading: HTMLHeadingElement | undefined + + createEffect(() => { + if (pluginHeading) { + typeof plugin.name === 'string' + ? (pluginHeading.textContent = plugin.name) + : plugin.name(pluginHeading, theme()) + } + }) + + const isActive = createMemo(() => + activePlugins().includes(plugin.id!), + ) + + return ( +
handlePluginClick(plugin.id!)} + class={clsx(styles().pluginName, { + active: isActive(), + })} + > +

+

+ ) + }} +
+
+ + {/* Add More Tab - visually distinct */} +
+

+ Add More

+
+
+
+ + {/* Show marketplace if active, otherwise show plugin panels */} + + {(pluginId) => (
{ - toggleActivePlugins(plugin.id!) + id={`${PLUGIN_CONTAINER_ID}-${pluginId}`} + ref={(el) => { + setPluginRefs((prev) => { + const updated = new Map(prev) + updated.set(pluginId, el) + return updated + }) }} - class={clsx(styles().pluginName, { - active: isActive(), - })} - > -

-

- ) - }} - -
+ class={styles().pluginsTabContent} + /> + )} + + } + > + +
- - - {(pluginId) => ( -
{ - setPluginRefs((prev) => { - const updated = new Map(prev) - updated.set(pluginId, el) - return updated - }) - }} - class={styles().pluginsTabContent} - /> - )} - -
+ ) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97854d96..c71e54be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -382,6 +382,8 @@ importers: specifier: ^4.2.4 version: 4.2.4 + examples/react/start/generated/prisma: {} + examples/react/time-travel: dependencies: '@tanstack/devtools-event-client': @@ -482,6 +484,9 @@ importers: '@solid-primitives/keyboard': specifier: ^1.3.3 version: 1.3.3(solid-js@1.9.9) + '@tanstack/devtools-client': + specifier: workspace:* + version: link:../devtools-client '@tanstack/devtools-event-bus': specifier: workspace:* version: link:../event-bus From 73722115c07ad0afa6cda65ee98cb1f49498e418 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Oct 2025 10:50:06 +0200 Subject: [PATCH 2/7] Plugins marketplace --- examples/react/basic/package.json | 2 + examples/react/basic/src/setup.tsx | 130 +- package.json | 4 +- packages/devtools-client/src/index.ts | 21 +- packages/devtools-ui/src/components/icons.tsx | 167 +++ packages/devtools-ui/src/index.ts | 11 + .../devtools-vite/src/inject-plugin.test.ts | 1086 +++++++++++++++++ packages/devtools-vite/src/inject-plugin.ts | 343 ++++++ packages/devtools-vite/src/package-manager.ts | 181 +++ packages/devtools-vite/src/plugin.ts | 261 ++-- packages/devtools-vite/src/utils.ts | 14 - .../devtools/src/context/devtools-context.tsx | 33 +- packages/devtools/src/core.tsx | 16 +- packages/devtools/src/styles/use-styles.ts | 279 ++++- .../src/tabs/marketplace/card-utils.test.ts | 219 ++++ .../src/tabs/marketplace/card-utils.ts | 85 ++ .../tabs/marketplace/marketplace-header.tsx | 54 + .../src/tabs/marketplace/plugin-card.tsx | 164 +++ .../src/tabs/marketplace/plugin-section.tsx | 53 + .../src/tabs/marketplace/plugin-utils.test.ts | 518 ++++++++ .../src/tabs/marketplace/plugin-utils.ts | 248 ++++ .../src/tabs/marketplace/settings-panel.tsx | 41 + .../src/tabs/marketplace/tag-filters.tsx | 34 + .../devtools/src/tabs/marketplace/types.ts | 47 + .../devtools/src/tabs/no-plugins-fallback.tsx | 300 ----- .../devtools/src/tabs/plugin-marketplace.tsx | 706 +++-------- packages/devtools/src/tabs/plugin-registry.ts | 222 ++++ packages/devtools/src/tabs/plugins-tab.tsx | 33 +- .../devtools/src/tabs/semver-utils.test.ts | 218 ++++ packages/devtools/src/tabs/semver-utils.ts | 114 ++ packages/react-devtools/src/devtools.tsx | 86 +- packages/solid-devtools/src/core.tsx | 44 +- pnpm-lock.yaml | 64 + 33 files changed, 4723 insertions(+), 1075 deletions(-) create mode 100644 packages/devtools-vite/src/inject-plugin.test.ts create mode 100644 packages/devtools-vite/src/inject-plugin.ts create mode 100644 packages/devtools-vite/src/package-manager.ts create mode 100644 packages/devtools/src/tabs/marketplace/card-utils.test.ts create mode 100644 packages/devtools/src/tabs/marketplace/card-utils.ts create mode 100644 packages/devtools/src/tabs/marketplace/marketplace-header.tsx create mode 100644 packages/devtools/src/tabs/marketplace/plugin-card.tsx create mode 100644 packages/devtools/src/tabs/marketplace/plugin-section.tsx create mode 100644 packages/devtools/src/tabs/marketplace/plugin-utils.test.ts create mode 100644 packages/devtools/src/tabs/marketplace/plugin-utils.ts create mode 100644 packages/devtools/src/tabs/marketplace/settings-panel.tsx create mode 100644 packages/devtools/src/tabs/marketplace/tag-filters.tsx create mode 100644 packages/devtools/src/tabs/marketplace/types.ts delete mode 100644 packages/devtools/src/tabs/no-plugins-fallback.tsx create mode 100644 packages/devtools/src/tabs/plugin-registry.ts create mode 100644 packages/devtools/src/tabs/semver-utils.test.ts create mode 100644 packages/devtools/src/tabs/semver-utils.ts diff --git a/examples/react/basic/package.json b/examples/react/basic/package.json index 50ded764..1f100aa3 100644 --- a/examples/react/basic/package.json +++ b/examples/react/basic/package.json @@ -12,6 +12,7 @@ "@tanstack/devtools-client": "0.0.2", "@tanstack/devtools-event-client": "0.3.3", "@tanstack/react-devtools": "^0.7.6", + "@tanstack/react-form": "^1.23.7", "@tanstack/react-query": "^5.90.1", "@tanstack/react-query-devtools": "^5.90.1", "@tanstack/react-router": "^1.131.50", @@ -23,6 +24,7 @@ "devDependencies": { "@tanstack/devtools-ui": "0.4.2", "@tanstack/devtools-vite": "0.3.6", + "@tanstack/react-form-devtools": "^0.1.7", "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", "@vitejs/plugin-react": "^4.7.0", diff --git a/examples/react/basic/src/setup.tsx b/examples/react/basic/src/setup.tsx index 191f158c..3f94e519 100644 --- a/examples/react/basic/src/setup.tsx +++ b/examples/react/basic/src/setup.tsx @@ -1,89 +1,65 @@ -import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' -import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' -import { - Link, - Outlet, - RouterProvider, - createRootRoute, - createRoute, - createRouter, -} from '@tanstack/react-router' -import { TanStackDevtools } from '@tanstack/react-devtools' -import { PackageJsonPanel } from './package-json-panel' +import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'; +import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'; +import { Link, Outlet, RouterProvider, createRootRoute, createRoute, createRouter } from '@tanstack/react-router'; +import { TanStackDevtools } from '@tanstack/react-devtools'; +import { PackageJsonPanel } from './package-json-panel'; const rootRoute = createRootRoute({ - component: () => ( - <> -
- - Home - {' '} - - About - -
-
- - - ), -}) - + component: () => <> +
+ + Home + {' '} + + About + +
+
+ + +}); const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: '/', component: function Index() { - return ( -
-

Welcome Home!

-
- ) - }, -}) + return
+

Welcome Home!

+
; + } +}); function About() { - return ( -
-

Hello from About!

-
- ) + return
+

Hello from About!

+
; } - const aboutRoute = createRoute({ getParentRoute: () => rootRoute, path: '/about', - component: About, -}) - -const routeTree = rootRoute.addChildren([indexRoute, aboutRoute]) - -const router = createRouter({ routeTree }) - + component: About +}); +const routeTree = rootRoute.addChildren([indexRoute, aboutRoute]); +const router = createRouter({ + routeTree +}); export default function DevtoolsExample() { - return ( - <> - , - }, - { - name: 'TanStack Router', - render: , - }, - { - name: 'Package.json', - render: () => , - }, - /* { - name: "The actual app", - render: