Skip to content

Commit bc583c9

Browse files
committed
feat(inspect): explain the Instances tab and register example hub hosts
Give the Instances tab a self-describing empty state — what it lists, where discovery comes from (`~/.devframe/instances/`), and how to make instances appear (start another dev server, or a host that calls `registerDevframeInstance`; note the disable env var) — plus a one-line toolbar hint. Register the in-process example hub hosts in the instance registry so they show up in the tab like any standalone devframe: the vite-devframe-hub (which the tab previously left empty) and the a11y-messages-playground, mirroring the next-devframe-hub that already self-registers. Registration waits for the dev server to listen, reuses its own origin, and is folded into teardown so restarts don't leave ghost records. READMEs updated to keep the hub examples at parity.
1 parent adffc6b commit bc583c9

7 files changed

Lines changed: 143 additions & 9 deletions

File tree

examples/a11y-messages-playground/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ the focused dock — the same path a manual dock click takes.
7878

7979
| File | Role |
8080
|---|---|
81-
| `src/a11y-messages-playground.ts` | The Vite host — hub context, static + connection-meta mounts, side-car WS |
81+
| `src/a11y-messages-playground.ts` | The Vite host — hub context, static + connection-meta mounts, side-car WS, instance-registry registration |
8282
| `vite.config.ts` | Mounts a11y + messages; attaches the a11y agent as its dock's `clientScript` |
8383
| `src/client/main.ts` | Boots the client host, renders the dock rail + iframe stage |
8484
| `src/client/app-under-test.ts` | The intentionally-broken, multi-route app the agent scans |

examples/a11y-messages-playground/src/a11y-messages-playground.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import type { DevframeHubContext } from '@devframes/hub/node'
22
import type { ClientScriptEntry } from '@devframes/hub/types'
3+
import type { DevframeInstanceRegistration } from 'devframe/node'
34
import type { DevframeDefinition, DevframeHost } from 'devframe/types'
45
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
56
import { homedir } from 'node:os'
7+
import process from 'node:process'
68
import { createHubContext, mountDevframe } from '@devframes/hub/node'
79
import { DEVFRAME_CONNECTION_META_FILENAME } from 'devframe/constants'
8-
import { startHttpAndWs } from 'devframe/node'
10+
import { registerDevframeInstance, startHttpAndWs } from 'devframe/node'
911
import { serveStaticNodeMiddleware } from 'devframe/utils/serve-static'
1012
import { getPort } from 'get-port-please'
1113
import { join } from 'pathe'
@@ -37,6 +39,7 @@ export function a11yMessagesPlayground(options: A11yMessagesPlaygroundOptions =
3739
const base = normalizeBase(options.base ?? '/__hub/')
3840
let viteConfig: ResolvedConfig | undefined
3941
let started: { close: () => Promise<void> } | undefined
42+
let registration: DevframeInstanceRegistration | undefined
4043

4144
return {
4245
name: 'a11y-messages-playground',
@@ -48,9 +51,12 @@ export function a11yMessagesPlayground(options: A11yMessagesPlaygroundOptions =
4851

4952
async configureServer(server: ViteDevServer) {
5053
// Vite re-invokes `configureServer` on restart — tear the old server down
51-
// so we don't leak the WS port.
54+
// so we don't leak the WS port, and drop the previous registry record so
55+
// a restart doesn't leave a ghost instance behind.
5256
await started?.close().catch(() => {})
5357
started = undefined
58+
registration?.unregister()
59+
registration = undefined
5460

5561
const cwd = viteConfig!.root
5662
const port = options.port ?? await getPort({ port: 9878, portRange: [9878, 9978] })
@@ -103,6 +109,37 @@ export function a11yMessagesPlayground(options: A11yMessagesPlaygroundOptions =
103109
// Tell the hub UI (served at `base`) where to find the WS endpoint.
104110
serveConnectionMeta(base)
105111

112+
// Register this playground in the global instance registry
113+
// (`~/.devframe/instances/`) so discovery tooling — `devframe connect`
114+
// and the inspector's Instances tab — lists it like any standalone
115+
// devframe. See `examples/vite-devframe-hub` for the same pattern.
116+
const register = (): void => {
117+
const origin = host.resolveOrigin()
118+
const url = new URL(origin)
119+
registration = registerDevframeInstance({
120+
pid: process.pid,
121+
port: Number(url.port) || (url.protocol === 'https:' ? 443 : 80),
122+
origin,
123+
basePath: base,
124+
id: 'example:a11y-messages-playground',
125+
name: 'A11y + Messages Playground',
126+
rootDir: cwd,
127+
mcp: null,
128+
startedAt: Date.now(),
129+
})
130+
}
131+
if (server.httpServer?.listening)
132+
register()
133+
else
134+
server.httpServer?.once('listening', register)
135+
136+
const closeStarted = started.close
137+
started.close = async () => {
138+
registration?.unregister()
139+
registration = undefined
140+
await closeStarted()
141+
}
142+
106143
server.httpServer?.once('close', () => {
107144
void started?.close().catch(() => {})
108145
})

examples/next-devframe-hub/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's
2020

2121
The A11y Inspector shows a live axe-core report of this hub's own page: the host serves the plugin's in-page agent module (`a11yAgentBundlePath`) same-origin through the catch-all route and attaches it as the a11y dock's `clientScript`; the hub client runtime — `createDevframeClientHost()` booted in `app/page.tsx` — imports it into the page, so the docked panel and the agent share the origin their BroadcastChannel rides.
2222

23+
The **RPC & State Inspector** carries an **Instances** tab that lists every devframe dev server running on your machine. The host registers itself in the shared registry (`~/.devframe/instances/`) on startup via `registerDevframeInstance()`, so it shows up as "this instance"; start another example (e.g. `pnpm --filter vite-devframe-hub dev`, or any `node bin.mjs` CLI example) in a second terminal and it appears there too, each linking to its own SPA.
24+
2325
## What the example proves
2426

2527
- `createHubContext()` boots a hub with no Vite-specific code path; a `DevframeHost` impl plugs Next specifics (static mounts, connection meta, storage, origin) in uniformly
@@ -35,7 +37,7 @@ The plugins run node-side (child processes, the native `zigpty` PTY backend) and
3537

3638
| File | Role |
3739
|---|---|
38-
| `src/client/devframe/next-devframe-hub.ts` | The Next host — hub context, static-mount registry (incl. the a11y agent), side-car WS |
40+
| `src/client/devframe/next-devframe-hub.ts` | The Next host — hub context, static-mount registry (incl. the a11y agent), side-car WS, instance-registry registration |
3941
| `src/client/app/%5F_hub/%5F_connection.json/route.ts` | Boots the singleton host and serves `/__hub/__connection.json` |
4042
| `src/client/app/%5F_[id]/[[...path]]/route.ts` | Serves each mounted SPA and its connection meta under `/__<id>/` |
4143
| `src/client/app/page.tsx` | The browser UI that consumes the hub protocol |

examples/vite-devframe-hub/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's
2020

2121
The A11y Inspector shows a live axe-core report of this hub's own page. `vite.config.ts` attaches the plugin's in-page agent as the a11y dock's `clientScript` (served via `/@fs/`), and the hub client runtime — `createDevframeClientHost()` booted in `src/client/main.ts` — imports it into the host page. Panel and agent share the Vite origin their BroadcastChannel rides; hover a violation to ring the offending element in the hub UI.
2222

23+
The **RPC & State Inspector** carries an **Instances** tab that lists every devframe dev server running on your machine. The host registers itself in the shared registry (`~/.devframe/instances/`) on startup via `registerDevframeInstance()`, so it shows up as "this instance"; start another example (`pnpm --filter a11y-messages-playground dev`, or any `node bin.mjs` CLI example) in a second terminal and it appears there too, each linking to its own SPA.
24+
2325
## What the example proves
2426

2527
- `createHubContext()` boots a hub with no Vite-specific code path; a `DevframeHost` impl plugs framework specifics (static mounts, connection meta, storage, origin) in uniformly
@@ -36,7 +38,7 @@ The dock UI is plain DOM in `src/client/`. To skin your own viewer, read the sam
3638

3739
| File | Role |
3840
|---|---|
39-
| `src/vite-devframe-hub.ts` | The Vite host — hub context, static + connection-meta mounts, side-car WS |
41+
| `src/vite-devframe-hub.ts` | The Vite host — hub context, static + connection-meta mounts, side-car WS, instance-registry registration |
4042
| `vite.config.ts` | Mounts the built-in plugins via the host's `devframes` option; attaches the a11y agent as its dock's `clientScript` |
4143
| `src/client/main.ts` | The browser UI that consumes the hub protocol |
4244
| `src/client/icons.ts` | Offline Phosphor icons for the dock |

examples/vite-devframe-hub/src/vite-devframe-hub.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { DevframeHubContext } from '@devframes/hub/node'
22
import type { ClientScriptEntry } from '@devframes/hub/types'
3+
import type { DevframeInstanceRegistration } from 'devframe/node'
34
import type { DevframeDefinition, DevframeHost } from 'devframe/types'
45
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
56
import { homedir } from 'node:os'
7+
import process from 'node:process'
68
import { defineHubRpcFunction } from '@devframes/hub'
79
import { createHubContext, mountDevframe } from '@devframes/hub/node'
810
import { DEVFRAME_CONNECTION_META_FILENAME } from 'devframe/constants'
9-
import { startHttpAndWs } from 'devframe/node'
11+
import { registerDevframeInstance, startHttpAndWs } from 'devframe/node'
1012
import { serveStaticNodeMiddleware } from 'devframe/utils/serve-static'
1113
import { getPort } from 'get-port-please'
1214
import { join } from 'pathe'
@@ -74,6 +76,7 @@ export function viteDevframeHub(options: ViteDevframeHubOptions = {}): Plugin {
7476
const base = normalizeBase(options.base ?? '/__hub/')
7577
let viteConfig: ResolvedConfig | undefined
7678
let started: { close: () => Promise<void> } | undefined
79+
let registration: DevframeInstanceRegistration | undefined
7780

7881
return {
7982
name: 'vite-devframe-hub',
@@ -85,9 +88,12 @@ export function viteDevframeHub(options: ViteDevframeHubOptions = {}): Plugin {
8588

8689
async configureServer(server: ViteDevServer) {
8790
// Vite re-invokes `configureServer` on each restart. Tear down the
88-
// previous server so we don't leak the WS port.
91+
// previous server so we don't leak the WS port, and drop the previous
92+
// registry record so a restart doesn't leave a ghost instance behind.
8993
await started?.close().catch(() => {})
9094
started = undefined
95+
registration?.unregister()
96+
registration = undefined
9197

9298
const cwd = viteConfig!.root
9399
// Prefer 9777 but keep booting when it's taken (e.g. a lingering
@@ -175,6 +181,41 @@ export function viteDevframeHub(options: ViteDevframeHubOptions = {}): Plugin {
175181
// Tell the hub UI (served at `base`) where to find the WS endpoint.
176182
serveConnectionMeta(base)
177183

184+
// Record this hub in the global instance registry (`~/.devframe/instances/`)
185+
// so discovery tooling — `devframe connect` and the inspector's Instances
186+
// tab — lists it like any standalone devframe. `createDevServer` registers
187+
// automatically; an in-process host like this one registers explicitly,
188+
// reusing the Vite dev server's own origin (where `<base>__connection.json`
189+
// is served). Registration waits for the server to be listening so the
190+
// origin/port are known. Folded into `started.close` so every teardown
191+
// path (restart, httpServer close, `closeBundle`) also unregisters.
192+
const register = (): void => {
193+
const origin = host.resolveOrigin()
194+
const url = new URL(origin)
195+
registration = registerDevframeInstance({
196+
pid: process.pid,
197+
port: Number(url.port) || (url.protocol === 'https:' ? 443 : 80),
198+
origin,
199+
basePath: base,
200+
id: 'example:vite-devframe-hub',
201+
name: 'Vite Devframe Hub',
202+
rootDir: cwd,
203+
mcp: null,
204+
startedAt: Date.now(),
205+
})
206+
}
207+
if (server.httpServer?.listening)
208+
register()
209+
else
210+
server.httpServer?.once('listening', register)
211+
212+
const closeStarted = started.close
213+
started.close = async () => {
214+
registration?.unregister()
215+
registration = undefined
216+
await closeStarted()
217+
}
218+
178219
server.httpServer?.once('close', () => {
179220
void started?.close().catch(() => {})
180221
})

plugins/inspect/src/spa/components/InstancesView.vue

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,29 @@ function formatUptime(startedAt: number): string {
3434
<div class="pane">
3535
<div class="toolbar">
3636
<span class="muted">{{ (instances ?? []).length }} running {{ (instances ?? []).length === 1 ? 'instance' : 'instances' }}</span>
37+
<span class="muted">Devframe dev servers running on this machine, discovered via the shared registry (<code>~/.devframe/instances/</code>).</span>
3738
</div>
3839

3940
<div v-if="!instances" class="center">
4041
Discovering instances…
4142
</div>
42-
<div v-else-if="instances.length === 0" class="empty">
43-
No running devframe instances discovered. Each dev server registers itself while it runs — start one to see it here.
43+
<div v-else-if="instances.length === 0" class="inst-empty">
44+
<span class="i-ph-broadcast-duotone inst-empty-icon" />
45+
<p class="inst-empty-title">
46+
No devframe instances discovered
47+
</p>
48+
<p>
49+
This tab lists every devframe dev server running on your machine, so you
50+
can jump between them. Each server registers itself in
51+
<code>~/.devframe/instances/</code> while it runs.
52+
</p>
53+
<p>
54+
Nothing shows up when only static/build servers are running, when
55+
discovery is turned off (<code>DEVFRAME_DISABLE_INSTANCE_REGISTRY=1</code>),
56+
or when an in-process host hasn't opted in. Start another
57+
<code>devframe</code> dev server — or a hub host that calls
58+
<code>registerDevframeInstance()</code> — then hit refresh.
59+
</p>
4460
</div>
4561

4662
<div v-else class="cards">

plugins/inspect/src/spa/style.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,3 +701,39 @@ textarea.args:focus {
701701
text-overflow: ellipsis;
702702
white-space: nowrap;
703703
}
704+
705+
.inst-empty {
706+
max-width: 560px;
707+
margin: 0 auto;
708+
padding: 40px 20px;
709+
text-align: center;
710+
color: var(--df-fg-dim);
711+
}
712+
713+
.inst-empty p {
714+
margin: 8px 0 0;
715+
font-size: 12.5px;
716+
line-height: 1.55;
717+
}
718+
719+
.inst-empty-icon {
720+
font-size: 32px;
721+
color: var(--df-fg-faint);
722+
}
723+
724+
.inst-empty-title {
725+
margin-top: 10px !important;
726+
font-size: 14px;
727+
font-weight: 600;
728+
color: var(--df-fg);
729+
}
730+
731+
.inst-empty code,
732+
.toolbar code {
733+
font-family: var(--df-mono);
734+
font-size: 0.92em;
735+
padding: 1px 5px;
736+
border-radius: 4px;
737+
background: var(--df-bg-active);
738+
color: var(--df-fg-dim);
739+
}

0 commit comments

Comments
 (0)