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..46f80904 --- /dev/null +++ b/examples/react/basic/src/package-json-panel.tsx @@ -0,0 +1,309 @@ +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< + Record< + string, + { + current: string + wanted: string + latest: string + type?: 'dependencies' | 'devDependencies' + } + > + >({}) + + 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

+ )} +
+ ) +} diff --git a/examples/react/basic/src/setup.tsx b/examples/react/basic/src/setup.tsx index 610f65f7..191f158c 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: