Skip to content

Commit c590674

Browse files
committed
docs: plan for /__devframes/ standard middleware handlers
1 parent c3ef483 commit c590674

1 file changed

Lines changed: 183 additions & 0 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Plan: `/__devframes/` framework-agnostic standard middleware
2+
3+
> Plan of record settled in a design interview on 2026-08-05. Implementation lands as a
4+
> 5-PR GitHub stack (bottom → top), each layer passing the full gauntlet
5+
> (`pnpm lint && pnpm knip && pnpm test && pnpm typecheck && pnpm build`).
6+
7+
## Goal
8+
9+
One web-standard handler (`(Request) => Response`, Comark-style — see
10+
<https://content.comark.dev/getting-started/installation#mount-the-handler>) that carries the
11+
entire devtools surface — mounted devframes, WS RPC, auth, MCP, embedded floating mode —
12+
mountable on any framework with a single catch-all route (Vite, Nitro, Hono, Next.js, Nuxt,
13+
SvelteKit), running on Node ≥ 20 and Bun. The hub stays headless; UI is a composable slot.
14+
15+
## Architecture
16+
17+
### Two `createHandler` factories, one UI slot
18+
19+
| Import | Serves | Default base |
20+
|---|---|---|
21+
| `devframe/handler` | **One devframe**: its SPA (`distDir`; omitted → bridge mode serving only meta + WS), `__connection.json`, `__mcp`, WS RPC, auth. Own isolated context, one `def.setup(ctx)`. | `/__<id>/` (hosted rule) |
22+
| `@devframes/hub/handler` | **Multi-frame, headless**: shared hub context (docks/terminals/messages/commands + hub built-in RPCs + shared-state slots); every frame's `setup(ctx)` runs against it → one merged RPC registry, one WS endpoint, **one hub Auth**, one aggregate MCP. Frames auto-registered as iframe docks. | `/__devframes/` |
23+
24+
**`DevframeHubUi` slot** (type lives in `@devframes/hub`; data-first, zero policy):
25+
26+
```ts
27+
interface DevframeHubUi {
28+
viewer?: { distDir: string } // standalone viewer SPA served at the namespace root
29+
embedded?: { entry: string } // prebuilt bootstrap served at <base>embedded.js
30+
}
31+
```
32+
33+
`@devframes/hub-ui` (new package) exports `createUi(options?)` — the *reference* implementation
34+
(a port of Vite DevTools' web components). Vite DevTools / community supply their own `ui`
35+
object to the same slot, reusing all infra. There is **no** `createHandler` in hub-ui.
36+
37+
### Handler API (both factories)
38+
39+
```ts
40+
const h = createHandler(defOrOptions, {
41+
base?, // mount base; hosted default /__<id>/ (core) or /__devframes/ (hub)
42+
server?, // sugar: node http server → shared WS upgrade at <base>__ws
43+
ws?: DevframeWsOptions, // explicit control — url > port > route (default '__ws')
44+
auth?, // default TRUE (existing OTP/token machinery); explicit false to opt out
45+
mcp?, // per-frame MCP (core) / aggregate MCP (hub)
46+
key?, // globalThis memoization — HMR re-evaluation returns the live instance
47+
origin?, // banner origin override; else derived lazily from first request
48+
// hub only:
49+
devframes?, context?, // declarative list OR pre-built hub context
50+
configure?, // async (ctx) => {} for docks/commands/terminals/messages registration
51+
ui?, // DevframeHubUi
52+
})
53+
// → { fetch(request, runtimeCtx?), nodeMiddleware, websocket, ready, context,
54+
// connectionMeta(), close() }
55+
```
56+
57+
- Sync factory, **eager** async init; `fetch` awaits `ready` internally.
58+
- `fetch` 404s inside its base; `nodeMiddleware` (connect-style) calls `next()` outside it.
59+
- Bun: `fetch(req, server)` second arg + exposed `websocket` hooks (crossws Bun adapter).
60+
- `key` memoization: a re-evaluation returns the live instance (closes/replaces it if the
61+
options changed) — prevents eager side-car leaks under Next/Nitro/SvelteKit dev HMR.
62+
63+
### WebSocket resolution (precedence)
64+
65+
1. `ws.url` — advertise an external endpoint verbatim; the handler owns **no** transport.
66+
Hosts that want the handler's RPC on their *own* WS server use the documented recipe:
67+
`attachWsRpcTransport(handler.context RPC group, { server, path })` + a matching `ws`.
68+
2. `ws.port` — explicit side-car port.
69+
3. `server` — shared upgrade on the host's node http server at `<base><ws.route ?? '__ws'>`.
70+
4. *(default)* — **eager** auto side-car on a free port, started at handler creation so
71+
`__connection.json` is stable from the first request.
72+
73+
All four advertised consistently in `__connection.json`. The WS route unifies on **`__ws`**
74+
everywhere (breaking: was `__devframe_ws`), matching upstream Vite DevTools' `/__devtools/__ws`.
75+
76+
### Path layout (hub, under base `/__devframes/`)
77+
78+
| Path | Serves | Condition |
79+
|---|---|---|
80+
| `/` | `ui.viewer` dist, else the index document | — |
81+
| `__index.json` | JSON index: frame ids/bases, endpoint paths | always |
82+
| `embedded.js` | `ui.embedded.entry` | 404 without `ui.embedded` |
83+
| `__connection.json` | hub connection meta | always |
84+
| `__ws` | WS upgrade route (shared-server tier) | always |
85+
| `__client-imports.js` | dock client-script import map | always |
86+
| `__mcp` | **aggregate** MCP over the shared context registry | when `mcp` enabled |
87+
| `<id>/` | each frame's SPA + its per-frame `__connection.json` | reserved-name-validated ids |
88+
89+
Per-frame `__mcp` exists only on the singular handler (the hub's shared context makes the
90+
aggregate the meaningful endpoint; tool ids are already namespaced `devframes:plugin:<slug>:*`).
91+
92+
### Auth
93+
94+
- Gated **by default** on both factories (existing `createInteractiveAuth` OTP + token
95+
machinery; `anonymous:` pre-trust prefix; WS origin gate).
96+
- **Hub: a single Auth.** One `DevframeAuthHandler` owned by the hub handler, one OTP
97+
handshake, one trusted-token store, enforced at the one shared transport. Mounted frames
98+
have no auth of their own — trust established once covers every frame, the aggregate MCP
99+
origin gate, and the hub built-ins. Iframes may arrive pre-authorized via hub-served
100+
`authToken` meta or reuse the parent page's connection (`__DEVFRAME_CONNECTION__`).
101+
- Banner origin derived lazily from the first request (`origin` option overrides).
102+
103+
### Embedded mode
104+
105+
- `embedded.js` = prebuilt bundle: headless `createDevframeClientHost` + hub-ui's
106+
`DockEmbedded`. **Always visible on load** — no view-mode model in hub-ui. Visibility
107+
policy belongs to whoever authors the entry (Vite DevTools keeps its normal/passive/hidden
108+
model in *its own* entry via its own `embedded: { entry }`). Dock-local state
109+
(position/collapse) stays — component behavior, not visibility policy.
110+
- Base discovery from `import.meta.url`; OTP/auth UI included.
111+
- Injection = documented one-line `<script type="module" src="/__devframes/embedded.js">`
112+
per framework. No Vite sugar plugin in this effort (deferred).
113+
114+
### Singular vs hub mounting — what a devframe's SPA / RPC client sees
115+
116+
The devframe's SPA and RPC client code are **byte-identical** in both cases (devframe's
117+
portability promise): relative assets, base from `document.baseURI`, `connectDevframe()`
118+
fetching `<base>__connection.json`. The differences are environmental:
119+
120+
| What the SPA / RPC client sees | Singular (`/__git/`) | Hub (`/__devframes/git/`) |
121+
|---|---|---|
122+
| Runtime base | `/__git/` | `/__devframes/git/` (transparent to the SPA) |
123+
| `__connection.json` | Own meta; WS at `<base>__ws` or side-car | Per-frame meta pointing at the **shared** hub WS |
124+
| RPC registry | Only this frame's functions (+ `anonymous:` handshake) | Merged: all frames + hub built-ins — callable cross-frame by design |
125+
| Shared state | Own context's slots only | All frames' slots + hub slots |
126+
| Auth | Own gate, own token | **Single hub Auth** — frames delegate entirely; one handshake unlocks the namespace |
127+
| Hub subsystems | Absent | Present; frame is also an iframe dock |
128+
| MCP | `<base>__mcp`, this frame's tools | Aggregate only, at hub level |
129+
| Isolation | Hard (own context, own transport) | Cooperative (shared context — tools can compose) |
130+
131+
### Migrations (same effort, public names kept)
132+
133+
- `createDevServer` → thin node server over `createHandler` (+ port resolution, instance
134+
registry, openBrowser). One wiring everywhere.
135+
- `viteDevBridge` → wraps the singular handler (`nodeMiddleware` + `server.httpServer` WS).
136+
- `@devframes/next` → reduces to memoization/re-export sugar over the handler.
137+
- `@devframes/nuxt` externally untouched.
138+
- Retire the unused `DEVFRAME_MOUNT_PATH` constant in favor of the new default-base constants.
139+
140+
## Delivery: 5-PR GitHub stack (bottom → top, merge bottom-up)
141+
142+
1. **`feat/handler-core`** — this plan doc; `devframe/handler`: `createHandler` (fetch/node/Bun
143+
shapes, four WS resolutions, auth gate, per-frame MCP, `key` memo), `__ws` constants,
144+
new `DF00xx` diagnostics + `docs/errors/` pages, vitest coverage (fetch, middleware,
145+
WS tiers + meta correctness per resolution, auth gating).
146+
2. **`feat/handler-adapters`** — `createDevServer` / `viteDevBridge` / `@devframes/next`
147+
rebuilt on the handler; api-snapshot updates.
148+
3. **`feat/hub-handler`** — `@devframes/hub/handler`: internal `DevframeHost` impl, frame
149+
mounting at `<base><id>/`, `ui` slot + `DevframeHubUi` type, `__index.json`,
150+
`__client-imports.js`, aggregate MCP, `devframes`/`context`/`configure` options,
151+
`DF8xxx` diagnostics, tests incl. reserved-path guards, cross-frame RPC visibility,
152+
shared-WS meta resolution from per-frame metas.
153+
4. **`feat/hub-ui`** — new package `@devframes/hub-ui`: full web-components port from
154+
Vite DevTools (DockEmbedded, DockStandalone, floating, views, views-builtin,
155+
command-palette, message, auth UI — no mode plumbing), devframe-branded
156+
(`@antfu/design` tokens, Phosphor icons, named `z-*` layers), two prebuilt entries
157+
(vanilla standalone viewer shell, embedded bootstrap), `createUi()`, storybook per repo
158+
convention, typecheck/knip/exports wiring.
159+
5. **`feat/handler-examples-docs`** — migrate both reference hub examples onto the headless
160+
hub handler (hand-built UIs kept as protocol demos, parity + README parity maintained);
161+
new minimal `examples/nitro-devframe-hub` + `examples/hono-devframe-hub`
162+
(`ui: createUi()` + script tag; Hono verified on Node **and** Bun); local `scripts/` Bun
163+
smoke test (boots the Hono example on Bun: fetch + WS RPC + embedded.js); Comark-style
164+
mount guides for Vite/Nitro/Hono/Next/Nuxt/SvelteKit; "Singular vs hub mounting" docs page.
165+
166+
## Breaking changes (pre-1.0, called out in PR bodies)
167+
168+
- Hosted default bases move under the hub base: `/__<id>/` → `<hubBase><id>/`
169+
(coordinate with vite-devtools downstream).
170+
- WS route `__devframe_ws` → `__ws` across all adapters.
171+
172+
## Verification items / known risks
173+
174+
- Per-frame meta → shared `__ws` resolution (`resolveWsUrl`) — dedicated tests in PR 3.
175+
- Eager side-car + HMR leaks — built-in `key` memoization; all snippets set it.
176+
- Auth banner origin — lazy derivation from first request + `origin` override.
177+
- hub-ui port is the bulk of the diff (~5–8k LOC) — isolated in its own PR layer.
178+
179+
## Non-goals
180+
181+
Deno/Cloudflare runtimes; per-frame MCP under the hub; `__mode.json` / view-mode machinery;
182+
framework injection sugar (Vite plugin deferred); any UI inside `@devframes/hub`; any release
183+
(requires explicit human approval).

0 commit comments

Comments
 (0)