From 6c331b7b97d3903c775a848728458bf386d5880a Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Mon, 22 Sep 2025 13:16:23 +0200 Subject: [PATCH 1/7] add devtools client for other libs to listen to --- examples/react/basic/package.json | 2 +- .../react/basic/src/package-json-panel.tsx | 166 ++++++++++++++++++ examples/react/basic/src/setup.tsx | 5 + examples/react/basic/vite.config.ts | 1 + packages/devtools-vite/package.json | 9 +- packages/devtools-vite/src/client.ts | 1 + packages/devtools-vite/src/event.ts | 28 +++ packages/devtools-vite/src/plugin.ts | 36 +++- packages/devtools-vite/src/utils.ts | 15 +- packages/devtools-vite/vite.config.ts | 2 +- pnpm-lock.yaml | 5 + 11 files changed, 259 insertions(+), 11 deletions(-) create mode 100644 examples/react/basic/src/package-json-panel.tsx create mode 100644 packages/devtools-vite/src/client.ts create mode 100644 packages/devtools-vite/src/event.ts diff --git a/examples/react/basic/package.json b/examples/react/basic/package.json index bf8a0e1f..2b5ad0f0 100644 --- a/examples/react/basic/package.json +++ b/examples/react/basic/package.json @@ -41,4 +41,4 @@ "last 1 safari version" ] } -} +} \ No newline at end of file diff --git a/examples/react/basic/src/package-json-panel.tsx b/examples/react/basic/src/package-json-panel.tsx new file mode 100644 index 00000000..2952bf0b --- /dev/null +++ b/examples/react/basic/src/package-json-panel.tsx @@ -0,0 +1,166 @@ +import { devtoolsEventClient } from "@tanstack/devtools-vite/client" +import { useEffect, useState } from "react" +import type { CSSProperties } from "react" + +export const PackageJsonPanel = () => { + const [packageJson, setPackageJson] = useState(null) + const [outdatedDeps, setOutdatedDeps] = useState>({}) + + useEffect(() => { + devtoolsEventClient.emit("mounted", undefined as any) + const off = devtoolsEventClient.on("ready", (event) => { + setPackageJson(event.payload.packageJson) + setOutdatedDeps(event.payload.outdatedDeps || {}) + }) + return () => { off?.() } + }, []) + + const hasOutdated = Object.keys(outdatedDeps || {}).length > 0 + + // Helpers + const stripRange = (v?: string) => (v ?? '').replace(/^[~^><=v\s]*/, '') + const parseSemver = (v?: string) => { + const s = stripRange(v) + const m = s.match(/^(\d+)\.(\d+)\.(\d+)/) + if (!m) return null + return { major: +m[1], minor: +m[2], patch: +m[3] } + } + const diffType = (current?: string, latest?: string): 'major' | 'minor' | 'patch' | null => { + const c = parseSemver(current) + const l = parseSemver(latest) + if (!c || !l) return null + if (l.major > c.major) return 'major' + if (l.major === c.major && l.minor > c.minor) return 'minor' + if (l.major === c.major && l.minor === c.minor && l.patch > c.patch) return 'patch' + return null + } + const diffColor: Record<'major' | 'minor' | 'patch', string> = { + major: '#ef4444', + minor: '#f59e0b', + patch: '#10b981', + } + + const containerStyle: CSSProperties = { padding: 10 } + const metaStyle: CSSProperties = { display: 'grid', gridTemplateColumns: 'auto 1fr', gap: 6, marginBottom: 8 } + const sectionStyle: CSSProperties = { margin: '8px 0', padding: '8px', border: '1px solid #444', borderRadius: 6 } + const tableStyle: CSSProperties = { width: '100%', borderCollapse: 'collapse' } + const thtd: CSSProperties = { borderBottom: '1px solid #333', padding: '4px 6px', textAlign: 'left' } + const badge = (text: string, color: string) => {text} + const btn = (label: string, onClick: () => void, variant: 'primary' | 'ghost' = 'primary') => ( + + ) + + const VersionCell = ({ dep, specified }: { dep: string, specified: string }) => { + const info = outdatedDeps[dep] + const current = info?.current ?? specified + const latest = info?.latest + const dt = info ? diffType(current, latest) : null + return ( +
+ {current} + {dt && latest ? + + {badge(`latest ${latest}`, diffColor[dt])} + : null} +
+ ) + } + + const UpgradeRowActions = ({ name }: { name: string }) => { + const info = outdatedDeps[name] + if (!info) return null + return ( +
+ {btn('Wanted', () => (devtoolsEventClient as any).emit('upgrade-dependency', { name, target: info.wanted } as any))} + {btn('Latest', () => (devtoolsEventClient as any).emit('upgrade-dependency', { name, target: info.latest } as any), 'ghost')} +
+ ) + } + + const makeLists = (names?: string[]) => { + const entries = Object.entries(outdatedDeps).filter(([n]) => !names || names.includes(n)) + const wantedList = entries.map(([name, info]) => ({ name, target: info.wanted })) + const latestList = entries.map(([name, info]) => ({ name, target: info.latest })) + return { wantedList, latestList } + } + + const BulkActions = ({ names }: { names?: string[] }) => { + const { wantedList, latestList } = makeLists(names) + if (wantedList.length === 0 && latestList.length === 0) return null + return ( +
+ {btn('All → wanted', () => (devtoolsEventClient as any).emit('upgrade-dependencies-bulk', { list: wantedList } as any))} + {btn('All → latest', () => (devtoolsEventClient as any).emit('upgrade-dependencies-bulk', { list: latestList } as any), 'ghost')} +
+ ) + } + + const renderDeps = (title: string, deps?: Record) => { + const names = Object.keys(deps || {}) + const someOutdatedInSection = names.some((n) => !!outdatedDeps[n]) + return ( +
+
+

{title}

+ {someOutdatedInSection ? : null} +
+ + + + + + + + + + + {Object.entries(deps || {}).map(([dep, version]) => { + const info = outdatedDeps[dep] + const isOutdated = !!info && info.current !== info.latest + return ( + + + + + + + ) + })} + +
PackageVersionStatusActions
{dep}{isOutdated ? badge('Outdated', '#e11d48') : badge('OK', '#10b981')}{isOutdated ? : null}
+
+ ) + } + + return ( +
+

Package.json

+ {packageJson ? ( +
+
+

Package info

+
+
Name
{packageJson.name}
+
Version
v{packageJson.version}
+
Description
{packageJson.description}
+
Author
{packageJson.author}
+
License
{packageJson.license}
+
Repository
{packageJson.repository?.url || packageJson.repository}
+
+
+ {renderDeps('Dependencies', packageJson.dependencies)} + {renderDeps('Dev Dependencies', packageJson.devDependencies)} +
+

Outdated (All)

+ {hasOutdated ? :

All dependencies are up to date.

} +
+
+ ) : ( +

No package.json data available

+ )} +
+ ) +} \ No newline at end of file diff --git a/examples/react/basic/src/setup.tsx b/examples/react/basic/src/setup.tsx index 610f65f7..0c2da836 100644 --- a/examples/react/basic/src/setup.tsx +++ b/examples/react/basic/src/setup.tsx @@ -9,6 +9,7 @@ import { createRouter, } from '@tanstack/react-router' import { TanStackDevtools } from '@tanstack/react-devtools' +import { PackageJsonPanel } from './package-json-panel' const rootRoute = createRootRoute({ component: () => ( @@ -72,6 +73,10 @@ export default function DevtoolsExample() { name: 'TanStack Router', render: , }, + { + name: "Package.json", + render: () => , + } /* { name: "The actual app", render: