Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 309 additions & 0 deletions examples/react/basic/src/package-json-panel.tsx
Original file line number Diff line number Diff line change
@@ -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<any>(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) => (
<span
style={{
background: color,
color: '#fff',
borderRadius: 4,
padding: '1px 4px',
fontSize: 11,
}}
>
{text}
</span>
)
const btn = (
label: string,
onClick: () => void,
variant: 'primary' | 'ghost' = 'primary',
) => (
<button
onClick={onClick}
style={{
padding: '2px 6px',
borderRadius: 5,
border:
variant === 'primary' ? '1px solid #6d28d9' : '1px solid transparent',
cursor: 'pointer',
background: variant === 'primary' ? '#7c3aed' : 'transparent',
color: variant === 'primary' ? '#fff' : '#7c3aed',
fontSize: 12,
}}
>
{label}
</button>
)

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 (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span>{current}</span>
{dt && latest ? (
<span
style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
>
<span style={{ opacity: 0.6 }}>→</span>
{badge(`latest ${latest}`, diffColor[dt])}
</span>
) : null}
</div>
)
}

const UpgradeRowActions = ({ name }: { name: string }) => {
const info = outdatedDeps[name]
if (!info) return null
return (
<div style={{ display: 'flex', gap: 6 }}>
{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',
)}
</div>
)
}

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 (
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{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',
)}
</div>
)
}

const renderDeps = (title: string, deps?: Record<string, string>) => {
const names = Object.keys(deps || {})
const someOutdatedInSection = names.some((n) => !!outdatedDeps[n])
return (
<div style={sectionStyle}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 6,
}}
>
<h3 style={{ margin: 0, fontSize: 14 }}>{title}</h3>
{someOutdatedInSection ? <BulkActions names={names} /> : null}
</div>
<table style={tableStyle}>
<thead>
<tr>
<th style={thtd}>Package</th>
<th style={thtd}>Version</th>
<th style={thtd}>Status</th>
<th style={thtd}>Actions</th>
</tr>
</thead>
<tbody>
{Object.entries(deps || {}).map(([dep, version]) => {
const info = outdatedDeps[dep]
const isOutdated = !!info && info.current !== info.latest
return (
<tr key={dep}>
<td style={thtd}>{dep}</td>
<td style={thtd}>
<VersionCell dep={dep} specified={version as string} />
</td>
<td style={thtd}>
{isOutdated
? badge('Outdated', '#e11d48')
: badge('OK', '#10b981')}
</td>
<td style={thtd}>
{isOutdated ? <UpgradeRowActions name={dep} /> : null}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}

return (
<div style={containerStyle}>
<h2 style={{ margin: '0 0 8px 0', fontSize: 16 }}>Package.json</h2>
{packageJson ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={sectionStyle}>
<h3 style={{ marginTop: 0, marginBottom: 6, fontSize: 14 }}>
Package info
</h3>
<div style={metaStyle}>
<div>
<strong>Name</strong>
</div>
<div>{packageJson.name}</div>
<div>
<strong>Version</strong>
</div>
<div>v{packageJson.version}</div>
<div>
<strong>Description</strong>
</div>
<div>{packageJson.description}</div>
<div>
<strong>Author</strong>
</div>
<div>{packageJson.author}</div>
<div>
<strong>License</strong>
</div>
<div>{packageJson.license}</div>
<div>
<strong>Repository</strong>
</div>
<div>{packageJson.repository?.url || packageJson.repository}</div>
</div>
</div>
{renderDeps('Dependencies', packageJson.dependencies)}
{renderDeps('Dev Dependencies', packageJson.devDependencies)}
<div style={sectionStyle}>
<h3 style={{ marginTop: 0, marginBottom: 6, fontSize: 14 }}>
Outdated (All)
</h3>
{hasOutdated ? (
<BulkActions />
) : (
<p style={{ margin: 0 }}>All dependencies are up to date.</p>
)}
</div>
</div>
) : (
<p style={{ margin: 0 }}>No package.json data available</p>
)}
</div>
)
}
5 changes: 5 additions & 0 deletions examples/react/basic/src/setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => (
Expand Down Expand Up @@ -72,6 +73,10 @@ export default function DevtoolsExample() {
name: 'TanStack Router',
render: <TanStackRouterDevtoolsPanel router={router} />,
},
{
name: 'Package.json',
render: () => <PackageJsonPanel />,
},
/* {
name: "The actual app",
render: <iframe style={{ width: '100%', height: '100%' }} src="http://localhost:3005" />,
Expand Down
1 change: 1 addition & 0 deletions examples/react/basic/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default defineConfig({
devtools({
removeDevtoolsOnBuild: true,
}),

Inspect(),
sonda(),
react({
Expand Down
7 changes: 7 additions & 0 deletions packages/devtools-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
"default": "./dist/esm/index.js"
}
},
"./client": {
"import": {
"types": "./dist/esm/client.d.ts",
"default": "./dist/esm/client.js"
}
},
"./package.json": "./package.json"
},
"sideEffects": false,
Expand Down Expand Up @@ -56,6 +62,7 @@
"@babel/parser": "^7.28.3",
"@babel/traverse": "^7.28.3",
"@babel/types": "^7.28.2",
"@tanstack/devtools-event-client": "workspace:*",
"@tanstack/devtools-event-bus": "workspace:*",
"chalk": "^5.6.0",
"launch-editor": "^2.11.1"
Expand Down
1 change: 1 addition & 0 deletions packages/devtools-vite/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { devtoolsEventClient } from './event'
37 changes: 37 additions & 0 deletions packages/devtools-vite/src/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { EventClient } from '@tanstack/devtools-event-client'

interface EventMap {
'tanstack-devtools-vite:ready': {
packageJson: {
name?: string
version?: string
dependencies?: Record<string, string>
devDependencies?: Record<string, string>
peerDependencies?: Record<string, string>
[key: string]: any
} | null
outdatedDeps: Record<
string,
{
current: string
wanted: string
latest: string
dependent: string
location: string
}
> | null
}
'tanstack-devtools-vite:mounted': void
}

export class DevtoolsEventClient extends EventClient<EventMap> {
constructor() {
super({
pluginId: 'tanstack-devtools-vite',
})
}
}

const devtoolsEventClient = new DevtoolsEventClient()

export { devtoolsEventClient }
Loading
Loading