Skip to content

Commit 5362ab5

Browse files
add devtools client for other libs to listen to (#162)
* add devtools client for other libs to listen to * ci: apply automated fixes * ci: apply automated fixes * separate event client * ci: apply automated fixes * fix tests * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 288cdcc commit 5362ab5

File tree

25 files changed

+702
-25
lines changed

25 files changed

+702
-25
lines changed

.changeset/curly-bikes-play.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@tanstack/devtools-event-client': patch
3+
'@tanstack/devtools-client': patch
4+
'@tanstack/devtools-vite': patch
5+
'@tanstack/devtools': patch
6+
---
7+
8+
Number of improvements to various parts of the DevTools:
9+
10+
- Update event client to allow users to disable it
11+
- Allow trigger to be completely hidden
12+
- Add a new package `@tanstack/devtools-client` to allow users to listen to events we emit from Vite.
13+
- Fix bugs inside of the DevTools like plugins being nuked on page refresh.

examples/angular/ssr/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "ssr",
3+
"version": "0.0.0",
4+
"private": true
5+
}

examples/react/basic/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"test:types": "tsc"
1010
},
1111
"dependencies": {
12+
"@tanstack/devtools-client": "0.0.1",
1213
"@tanstack/devtools-event-client": "0.3.2",
1314
"@tanstack/react-devtools": "^0.7.4",
1415
"@tanstack/react-query": "^5.90.1",
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
import { devtoolsEventClient } from '@tanstack/devtools-client'
2+
import { useEffect, useState } from 'react'
3+
import type { CSSProperties } from 'react'
4+
5+
export const PackageJsonPanel = () => {
6+
const [packageJson, setPackageJson] = useState<any>(null)
7+
const [outdatedDeps, setOutdatedDeps] = useState<
8+
Record<
9+
string,
10+
{
11+
current: string
12+
wanted: string
13+
latest: string
14+
type?: 'dependencies' | 'devDependencies'
15+
}
16+
>
17+
>({})
18+
19+
useEffect(() => {
20+
devtoolsEventClient.emit('mounted', undefined as any)
21+
const cleanupOutdated = devtoolsEventClient.on(
22+
'outdated-deps-read',
23+
(event) => {
24+
setOutdatedDeps(event.payload.outdatedDeps || {})
25+
},
26+
)
27+
const cleanupPackageJson = devtoolsEventClient.on(
28+
'package-json-read',
29+
(event) => {
30+
console.log('package-json-read', event)
31+
setPackageJson(event.payload.packageJson)
32+
},
33+
)
34+
return () => {
35+
cleanupOutdated()
36+
cleanupPackageJson()
37+
}
38+
}, [])
39+
40+
const hasOutdated = Object.keys(outdatedDeps).length > 0
41+
42+
// Helpers
43+
const stripRange = (v?: string) => (v ?? '').replace(/^[~^><=v\s]*/, '')
44+
const parseSemver = (v?: string) => {
45+
const s = stripRange(v)
46+
const m = s.match(/^(\d+)\.(\d+)\.(\d+)/)
47+
if (!m) return null
48+
return { major: +m[1], minor: +m[2], patch: +m[3] }
49+
}
50+
const diffType = (
51+
current?: string,
52+
latest?: string,
53+
): 'major' | 'minor' | 'patch' | null => {
54+
const c = parseSemver(current)
55+
const l = parseSemver(latest)
56+
if (!c || !l) return null
57+
if (l.major > c.major) return 'major'
58+
if (l.major === c.major && l.minor > c.minor) return 'minor'
59+
if (l.major === c.major && l.minor === c.minor && l.patch > c.patch)
60+
return 'patch'
61+
return null
62+
}
63+
const diffColor: Record<'major' | 'minor' | 'patch', string> = {
64+
major: '#ef4444',
65+
minor: '#f59e0b',
66+
patch: '#10b981',
67+
}
68+
69+
const containerStyle: CSSProperties = { padding: 10 }
70+
const metaStyle: CSSProperties = {
71+
display: 'grid',
72+
gridTemplateColumns: 'auto 1fr',
73+
gap: 6,
74+
marginBottom: 8,
75+
}
76+
const sectionStyle: CSSProperties = {
77+
margin: '8px 0',
78+
padding: '8px',
79+
border: '1px solid #444',
80+
borderRadius: 6,
81+
}
82+
const tableStyle: CSSProperties = {
83+
width: '100%',
84+
borderCollapse: 'collapse',
85+
}
86+
const thtd: CSSProperties = {
87+
borderBottom: '1px solid #333',
88+
padding: '4px 6px',
89+
textAlign: 'left',
90+
}
91+
const badge = (text: string, color: string) => (
92+
<span
93+
style={{
94+
background: color,
95+
color: '#fff',
96+
borderRadius: 4,
97+
padding: '1px 4px',
98+
fontSize: 11,
99+
}}
100+
>
101+
{text}
102+
</span>
103+
)
104+
const btn = (
105+
label: string,
106+
onClick: () => void,
107+
variant: 'primary' | 'ghost' = 'primary',
108+
) => (
109+
<button
110+
onClick={onClick}
111+
style={{
112+
padding: '2px 6px',
113+
borderRadius: 5,
114+
border:
115+
variant === 'primary' ? '1px solid #6d28d9' : '1px solid transparent',
116+
cursor: 'pointer',
117+
background: variant === 'primary' ? '#7c3aed' : 'transparent',
118+
color: variant === 'primary' ? '#fff' : '#7c3aed',
119+
fontSize: 12,
120+
}}
121+
>
122+
{label}
123+
</button>
124+
)
125+
126+
const VersionCell = ({
127+
dep,
128+
specified,
129+
}: {
130+
dep: string
131+
specified: string
132+
}) => {
133+
const info = outdatedDeps[dep] as
134+
| {
135+
current: string
136+
wanted: string
137+
latest: string
138+
type?: 'dependencies' | 'devDependencies'
139+
}
140+
| undefined
141+
const current = info?.current ?? specified
142+
const latest = info?.latest
143+
const dt = info ? diffType(current, latest) : null
144+
return (
145+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
146+
<span>{current}</span>
147+
{dt && latest ? (
148+
<span
149+
style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
150+
>
151+
<span style={{ opacity: 0.6 }}></span>
152+
{badge(`latest ${latest}`, diffColor[dt])}
153+
</span>
154+
) : null}
155+
</div>
156+
)
157+
}
158+
159+
const UpgradeRowActions = ({ name }: { name: string }) => {
160+
const info = outdatedDeps[name] as
161+
| {
162+
current: string
163+
wanted: string
164+
latest: string
165+
type?: 'dependencies' | 'devDependencies'
166+
}
167+
| undefined
168+
if (!info) return null
169+
return (
170+
<div style={{ display: 'flex', gap: 6 }}>
171+
{btn('Wanted', () =>
172+
(devtoolsEventClient as any).emit('upgrade-dependency', {
173+
name,
174+
target: info.wanted,
175+
} as any),
176+
)}
177+
{btn(
178+
'Latest',
179+
() =>
180+
(devtoolsEventClient as any).emit('upgrade-dependency', {
181+
name,
182+
target: info.latest,
183+
} as any),
184+
'ghost',
185+
)}
186+
</div>
187+
)
188+
}
189+
190+
const makeLists = (names?: Array<string>) => {
191+
const entries = Object.entries(outdatedDeps).filter(
192+
([n]) => !names || names.includes(n),
193+
)
194+
const wantedList = entries.map(([name, info]) => ({
195+
name,
196+
target: info.wanted,
197+
}))
198+
const latestList = entries.map(([name, info]) => ({
199+
name,
200+
target: info.latest,
201+
}))
202+
return { wantedList, latestList }
203+
}
204+
205+
const BulkActions = ({ names }: { names?: Array<string> }) => {
206+
const { wantedList, latestList } = makeLists(names)
207+
if (wantedList.length === 0 && latestList.length === 0) return null
208+
return (
209+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
210+
{btn('All → wanted', () =>
211+
(devtoolsEventClient as any).emit('upgrade-dependencies-bulk', {
212+
list: wantedList,
213+
} as any),
214+
)}
215+
{btn(
216+
'All → latest',
217+
() =>
218+
(devtoolsEventClient as any).emit('upgrade-dependencies-bulk', {
219+
list: latestList,
220+
} as any),
221+
'ghost',
222+
)}
223+
</div>
224+
)
225+
}
226+
227+
const renderDeps = (title: string, deps?: Record<string, string>) => {
228+
const names = Object.keys(deps || {})
229+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
230+
const someOutdatedInSection = names.some((n) => !!outdatedDeps[n])
231+
return (
232+
<div style={sectionStyle}>
233+
<div
234+
style={{
235+
display: 'flex',
236+
alignItems: 'center',
237+
justifyContent: 'space-between',
238+
gap: 6,
239+
}}
240+
>
241+
<h3 style={{ margin: 0, fontSize: 14 }}>{title}</h3>
242+
{someOutdatedInSection ? <BulkActions names={names} /> : null}
243+
</div>
244+
<table style={tableStyle}>
245+
<thead>
246+
<tr>
247+
<th style={thtd}>Package</th>
248+
<th style={thtd}>Version</th>
249+
<th style={thtd}>Status</th>
250+
<th style={thtd}>Actions</th>
251+
</tr>
252+
</thead>
253+
<tbody>
254+
{Object.entries(deps || {}).map(([dep, version]) => {
255+
const info = outdatedDeps[dep] as
256+
| {
257+
current: string
258+
wanted: string
259+
latest: string
260+
type?: 'dependencies' | 'devDependencies'
261+
}
262+
| undefined
263+
const isOutdated = !!info && info.current !== info.latest
264+
return (
265+
<tr key={dep}>
266+
<td style={thtd}>{dep}</td>
267+
<td style={thtd}>
268+
<VersionCell dep={dep} specified={version} />
269+
</td>
270+
<td style={thtd}>
271+
{isOutdated
272+
? badge('Outdated', '#e11d48')
273+
: badge('OK', '#10b981')}
274+
</td>
275+
<td style={thtd}>
276+
{isOutdated ? <UpgradeRowActions name={dep} /> : null}
277+
</td>
278+
</tr>
279+
)
280+
})}
281+
</tbody>
282+
</table>
283+
</div>
284+
)
285+
}
286+
287+
return (
288+
<div style={containerStyle}>
289+
<h2 style={{ margin: '0 0 8px 0', fontSize: 16 }}>Package.json</h2>
290+
{packageJson ? (
291+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
292+
<div style={sectionStyle}>
293+
<h3 style={{ marginTop: 0, marginBottom: 6, fontSize: 14 }}>
294+
Package info
295+
</h3>
296+
<div style={metaStyle}>
297+
<div>
298+
<strong>Name</strong>
299+
</div>
300+
<div>{packageJson.name}</div>
301+
<div>
302+
<strong>Version</strong>
303+
</div>
304+
<div>v{packageJson.version}</div>
305+
<div>
306+
<strong>Description</strong>
307+
</div>
308+
<div>{packageJson.description}</div>
309+
<div>
310+
<strong>Author</strong>
311+
</div>
312+
<div>{packageJson.author}</div>
313+
<div>
314+
<strong>License</strong>
315+
</div>
316+
<div>{packageJson.license}</div>
317+
<div>
318+
<strong>Repository</strong>
319+
</div>
320+
<div>{packageJson.repository?.url || packageJson.repository}</div>
321+
</div>
322+
</div>
323+
{renderDeps('Dependencies', packageJson.dependencies)}
324+
{renderDeps('Dev Dependencies', packageJson.devDependencies)}
325+
<div style={sectionStyle}>
326+
<h3 style={{ marginTop: 0, marginBottom: 6, fontSize: 14 }}>
327+
Outdated (All)
328+
</h3>
329+
{hasOutdated ? (
330+
<BulkActions />
331+
) : (
332+
<p style={{ margin: 0 }}>All dependencies are up to date.</p>
333+
)}
334+
</div>
335+
</div>
336+
) : (
337+
<p style={{ margin: 0 }}>No package.json data available</p>
338+
)}
339+
</div>
340+
)
341+
}

examples/react/basic/src/setup.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
createRouter,
1010
} from '@tanstack/react-router'
1111
import { TanStackDevtools } from '@tanstack/react-devtools'
12+
import { PackageJsonPanel } from './package-json-panel'
1213

1314
const rootRoute = createRootRoute({
1415
component: () => (
@@ -72,6 +73,10 @@ export default function DevtoolsExample() {
7273
name: 'TanStack Router',
7374
render: <TanStackRouterDevtoolsPanel router={router} />,
7475
},
76+
{
77+
name: 'Package.json',
78+
render: () => <PackageJsonPanel />,
79+
},
7580
/* {
7681
name: "The actual app",
7782
render: <iframe style={{ width: '100%', height: '100%' }} src="http://localhost:3005" />,

examples/react/basic/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default defineConfig({
1010
devtools({
1111
removeDevtoolsOnBuild: true,
1212
}),
13+
1314
Inspect(),
1415
sonda(),
1516
react({

0 commit comments

Comments
 (0)