|
| 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