diff --git a/.changeset/curly-bikes-play.md b/.changeset/curly-bikes-play.md new file mode 100644 index 00000000..fa0a684f --- /dev/null +++ b/.changeset/curly-bikes-play.md @@ -0,0 +1,13 @@ +--- +'@tanstack/devtools-event-client': patch +'@tanstack/devtools-client': patch +'@tanstack/devtools-vite': patch +'@tanstack/devtools': patch +--- + +Number of improvements to various parts of the DevTools: + +- Update event client to allow users to disable it +- Allow trigger to be completely hidden +- Add a new package `@tanstack/devtools-client` to allow users to listen to events we emit from Vite. +- Fix bugs inside of the DevTools like plugins being nuked on page refresh. diff --git a/examples/angular/ssr/package.json b/examples/angular/ssr/package.json new file mode 100644 index 00000000..9d0250c7 --- /dev/null +++ b/examples/angular/ssr/package.json @@ -0,0 +1,5 @@ +{ + "name": "ssr", + "version": "0.0.0", + "private": true +} diff --git a/examples/react/basic/package.json b/examples/react/basic/package.json index affa8c08..a17cd8b7 100644 --- a/examples/react/basic/package.json +++ b/examples/react/basic/package.json @@ -9,6 +9,7 @@ "test:types": "tsc" }, "dependencies": { + "@tanstack/devtools-client": "0.0.1", "@tanstack/devtools-event-client": "0.3.2", "@tanstack/react-devtools": "^0.7.4", "@tanstack/react-query": "^5.90.1", 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..c5d26b4f --- /dev/null +++ b/examples/react/basic/src/package-json-panel.tsx @@ -0,0 +1,341 @@ +import { devtoolsEventClient } from '@tanstack/devtools-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 cleanupOutdated = devtoolsEventClient.on( + 'outdated-deps-read', + (event) => { + setOutdatedDeps(event.payload.outdatedDeps || {}) + }, + ) + const cleanupPackageJson = devtoolsEventClient.on( + 'package-json-read', + (event) => { + console.log('package-json-read', event) + setPackageJson(event.payload.packageJson) + }, + ) + return () => { + cleanupOutdated() + cleanupPackageJson() + } + }, []) + + 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] as + | { + current: string + wanted: string + latest: string + type?: 'dependencies' | 'devDependencies' + } + | undefined + 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] as + | { + current: string + wanted: string + latest: string + type?: 'dependencies' | 'devDependencies' + } + | undefined + 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?: Array) => { + 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?: Array }) => { + 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 || {}) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const someOutdatedInSection = names.some((n) => !!outdatedDeps[n]) + return ( +
+
+

{title}

+ {someOutdatedInSection ? : null} +
+ + + + + + + + + + + {Object.entries(deps || {}).map(([dep, version]) => { + const info = outdatedDeps[dep] as + | { + current: string + wanted: string + latest: string + type?: 'dependencies' | 'devDependencies' + } + | undefined + 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: