Skip to content

Commit 2aaae94

Browse files
committed
docs(plans): json-render as an iframe view provider (plan of record)
Capture-only design doc from a design interview. json-render becomes a swappable, framework-agnostic iframe-hosted 'view provider' (default @devframes/json-render-ui), registered node-side via initHub({ viewProviders }) and discovered by the client through a read-only devframe:view-providers shared-state map. The hub UI renders a provider-backed dock as a pooled iframe (?view=<stateKey>), materializes inline specs into an ephemeral key, and shows a built-in empty state when a view type has no provider. Collapses the current triple json-render implementation to one SPA reused by every UI; keeps the in-process renderer seam for niche types. No implementation yet.
1 parent 7c18542 commit 2aaae94

1 file changed

Lines changed: 222 additions & 0 deletions

File tree

plans/json-render-ui-packaging.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# Plan: json-render as an iframe view provider — one renderer for every UI
2+
3+
> Plan of record settled in a design interview on 2026-08-10. Capture only —
4+
> **not yet implemented**. Implementation lands as a staged GitHub stack
5+
> (bottom → top), each PR passing the full gauntlet
6+
> (`pnpm lint && pnpm knip && pnpm test && pnpm typecheck && pnpm build`).
7+
>
8+
> Supersedes the first draft of this file (which had hub-ui reuse
9+
> `@devframes/json-render-ui` as an **in-process Vue `DockRenderer`**). The
10+
> interview pivoted to an **iframe-hosted view-provider** model, which decouples
11+
> json-render from the hub UI's framework entirely.
12+
13+
## Goal
14+
15+
Make "choose or build your own UI" coherent and make json-render + the hub UI
16+
**compose** instead of fuse. Deliver json-render as a **swappable, framework-
17+
agnostic view provider rendered in an iframe**: a hub maps a dock *view type*
18+
(e.g. `json-render`) to a provider SPA; the hub UI renders that type as an
19+
iframe pointing at the provider; when no provider is registered for a type, the
20+
UI shows an explicit empty state. The official provider is
21+
`@devframes/json-render-ui`; community implementations swap it.
22+
23+
**The crux win:** json-render is implemented **once** (json-render-ui's SPA) and
24+
reused by *every* hub UI — Vue, React, vanilla — via an iframe. The current
25+
triplication (json-render-ui Vue + hub-ui's inline Vue copy + the Next example's
26+
React port) collapses to a single implementation.
27+
28+
## Current reality (why this matters)
29+
30+
- `@devframes/json-render` — framework-neutral **core**: catalog/schema (the 14
31+
catalog-v1 components, `packages/json-render/src/prop-schemas.ts:131`), the
32+
node factory `createJsonRenderView` (publishes a spec to a shared-state
33+
`stateKey` + a `JSON_RENDER_INDEX_KEY` entry,
34+
`packages/json-render/src/node/create-view.ts:131`), and the `/hub` projection
35+
`DevframeJsonRenderDockEntry` carrying only a **serializable**
36+
`JsonRenderViewRef = { stateKey } | { spec }` (`packages/json-render/src/hub.ts:11`,
37+
`view-ref.ts:9`).
38+
- `@devframes/json-render-ui` — the official **Vue** renderer. Already ships a
39+
standalone **SPA** (`jsonRenderSpaDir` + `createJsonRenderDevframe`,
40+
`packages/json-render-ui/src/spa.ts:17`) that `connectDevframe()`s and reads
41+
specs from shared state — but as a **multi-view dashboard** driven by
42+
`JSON_RENDER_INDEX_KEY` (`packages/json-render-ui/src/spa/main.ts:31`), not a
43+
single-view renderer pointed at one dock. Also ships an in-process
44+
`createJsonRenderDockRenderer` (`dock-renderer.ts:14`).
45+
- `@devframes/hub-ui` — the reference viewer. Carries its **own inline Vue
46+
json-render** copy and renders json-render via a hardcoded `ViewEntry.vue`
47+
switch → `ViewJsonRender.vue`, on the **deprecated** `entry.ui._stateKey`.
48+
- `examples/next-devframe-hub` — a **third** (React) port of the catalog.
49+
50+
**Machinery we reuse (already exists):**
51+
52+
- A **mounted devframe is already an iframe dock**: `mountDevframe` serves an SPA
53+
at `<base><id>/` + the shared connection meta, then auto-registers
54+
`{ type:'iframe', url: base }` (`packages/hub/src/node/mount-devframe.ts:91,103`).
55+
A "provider SPA" is just a mounted devframe.
56+
- **iframe-pane pooling** keys live iframes by `frameId` and preserves their
57+
state across tab switches (`packages/hub-ui/src/client/components/views/ViewIframe.vue:74,204`).
58+
- An **iframe SPA under the hub base reads the same shared state / RPC** (one
59+
socket, relative `./__connection.json`), so a `stateKey` reaches it
60+
(`packages/json-render-ui/src/spa/main.ts:25,68`).
61+
- **Cross-frame dark/light** already propagates via the `devframes-color-scheme`
62+
localStorage key (`packages/hub-ui/src/client/state/color-mode.ts:11`), and the
63+
brand primary is published as `branding.json` (the branding feature, PR #177).
64+
65+
**Gaps this plan fills:** there is no view-type → provider-URL registry, no
66+
single-view mode on the provider SPA, no inline-spec→iframe path, and no
67+
"no provider" placeholder — the only current detection is a `console.warn` in
68+
the in-process renderer seam (`packages/hub/src/client/host.ts:453`).
69+
70+
## Decisions (design interview, 2026-08-10)
71+
72+
1. **Scope**: agree architecture + staged plan; capture as this doc; implement
73+
later.
74+
2. **General mechanism**: a **view-type → iframe-provider** mapping (any dock view
75+
type can resolve to a swappable provider SPA), with `json-render` as the
76+
first/official one.
77+
3. **Iframe is the model**: json-render renders as an iframe provider in hub-ui
78+
and the examples; the in-process `createJsonRenderDockRenderer` is
79+
**deprecated**. The hub's in-process `renderers` seam **stays** for niche/
80+
custom in-process types and `custom-render` — just not for json-render.
81+
4. **Registration**: node-side `initHub({ viewProviders })` mounts each provider
82+
SPA (via the existing `mountDevframe` path) and maps view-type → its base URL.
83+
Headless core: `@devframes/hub` takes **no** json-render-ui dependency; the
84+
app/reference setup wires the official provider. Swap by passing a different
85+
provider; omit it → placeholder.
86+
5. **Provider descriptor**: a **mountable devframe SPA** (default = json-render-ui's
87+
SPA). Off-origin/hosted providers work through the existing iframe `remote`
88+
option as a documented extension, not the v1 focus.
89+
6. **Discovery**: the hub publishes the view-type → base-URL map as a **read-only
90+
shared-state key** (`devframe:view-providers`), seeded node-side, read by the
91+
client pre-mount (like `devframe:docks`). Works on both `initHub` and the
92+
manual `createHubContext` path; needs no `__index.json` fetch. Absent entry =
93+
placeholder.
94+
7. **View ref → iframe**: the provider SPA gains a **single-view mode**
95+
(`?view=<stateKey>`) rendering exactly that view from shared state. Entries
96+
with an inline `spec` are **materialized by the client resolver into an
97+
ephemeral shared-state key** (disposed on unmount), so the iframe always
98+
receives a uniform `stateKey`.
99+
8. **Frame model**: **one pooled iframe per view**, `frameId = provider + stateKey`,
100+
`src = <providerBase>?view=<stateKey>` — reuses iframe-pane pooling (state
101+
preserved, reused on reselect). No host↔iframe nav protocol.
102+
9. **Placeholder**: hub-ui renders a **built-in empty state** naming the missing
103+
type ("This host has no `json-render` view provider registered", with a hint to
104+
register `@devframes/json-render-ui`), detected from the empty
105+
`devframe:view-providers` entry. No iframe mounted.
106+
10. **Theming the provider iframe**: the provider SPA follows the host via the
107+
shared same-origin channels — dark/light from `devframes-color-scheme`
108+
storage, brand primary by fetching the hub's `branding.json` and applying
109+
`--devframe-primary`. No new protocol.
110+
11. **Catalog growth**: promote hub-ui's extra `Tabs`/`Link`/`Select` into the
111+
base catalog (additive; port hub-ui's prop shapes) so the one provider renders
112+
all 17. (Carried over from the prior interview.)
113+
12. **UI selection**: `initHub({ ui })` remains the swap point for the hub UI
114+
itself; `viewProviders` is the swap point for renderers. Renderer/provider
115+
packages stay hub-agnostic; document the contracts. No runtime multi-UI
116+
registry.
117+
118+
## Target architecture
119+
120+
### Package topology
121+
122+
| Package | Role |
123+
|---|---|
124+
| `@devframes/json-render` | Framework-neutral **core** — catalog/schema (**17**), node `createJsonRenderView` (publishes `stateKey` + index), `/hub` projection (`DevframeJsonRenderDockEntry` with `view: JsonRenderViewRef`). |
125+
| `@devframes/json-render-ui` | The **one** renderer, shipped primarily as a **provider SPA** (iframe): `jsonRenderSpaDir` + a `jsonRenderProvider()` factory for `viewProviders`, a **single-view mode** (`?view=<stateKey>`), and `branding.json` theming. The Vue registry/components power the SPA. In-process `createJsonRenderDockRenderer` is **deprecated**. |
126+
| `@devframes/hub` | Headless runtime; gains the **general view-provider mechanism**: `initHub({ viewProviders })` mounts provider SPAs and publishes the read-only `devframe:view-providers` shared-state map. Keeps the in-process `renderers` seam for custom in-process types. No json-render-ui dependency. |
127+
| `@devframes/hub-ui` | Reference viewer. Resolves a provider-backed dock's type → base URL, renders it as a **pooled iframe** (`frameId = provider+stateKey`, `?view=<stateKey>`), materializes inline specs to an ephemeral key, and shows the **no-provider empty state**. No inline json-render, no json-render-ui dependency, no `--jr-primary` bridge. |
128+
| examples (Vite/Vue, Next/React) | BYO references — wire `initHub({ viewProviders: { 'json-render': jsonRenderProvider() } })`; the **React example drops its own json-render port** and uses the shared iframe provider. |
129+
130+
### Composition model — "choose or build your own UI"
131+
132+
- A **hub UI** = a node `DevframeHubUi` provider (`viewer`/`embedded`/`assets`,
133+
swapped via `initHub({ ui })`) + a client shell.
134+
- **Renderers are iframe view providers**, registered node-side per view type
135+
(`initHub({ viewProviders })`), swappable, and **framework-agnostic**: the same
136+
Vue json-render-ui SPA renders inside a Vue, React, or vanilla host because it's
137+
an iframe. The in-process `DockRenderer` seam remains for niche same-process
138+
needs but is no longer json-render's path.
139+
- **Data path**: plugin → `createJsonRenderView` (publishes spec to a `stateKey`)
140+
→ dock entry `view: { stateKey }` → hub UI resolves `json-render` → provider
141+
base, mounts `<base>?view=<stateKey>` → provider SPA reads that `stateKey` off
142+
the shared socket and renders. Inline `spec` → client writes an ephemeral key
143+
first.
144+
- **No provider** for a type → hub UI empty state.
145+
146+
## Staged PR stack
147+
148+
Each PR independently shippable, gated by the full gauntlet; land bottom → top.
149+
150+
### PR A — Catalog growth (core) · depends on: —
151+
Add `Tabs`/`Link`/`Select` to `@devframes/json-render` `basePropSchemas` (zod) +
152+
`componentDescriptions`, porting hub-ui's current prop shapes; they flow into
153+
`baseCatalog` automatically. Additive (old specs stay valid). Update `tsnapi`
154+
snapshots.
155+
156+
### PR B — json-render-ui: 17 components + provider SPA · depends on: A
157+
- Implement `Tabs`/`Link`/`Select` in the SPA's registry (→ 17).
158+
- Add **single-view mode** to the SPA: `?view=<stateKey>` renders only that view
159+
from shared state (keep the index-driven dashboard for standalone use).
160+
- Add **theming**: fetch `branding.json`, apply `--devframe-primary`; dark/light
161+
via the shared `devframes-color-scheme` key.
162+
- Export **`jsonRenderProvider()`** (a `DevframeDefinition` over `jsonRenderSpaDir`,
163+
building on `createJsonRenderDevframe`) for `viewProviders`.
164+
- **Deprecate** `createJsonRenderDockRenderer`.
165+
166+
### PR C — hub: general view-provider mechanism · depends on: —
167+
- `InitHubOptions.viewProviders?: Record<string, ViewProviderDef>` — mount each
168+
provider via the existing `mountDevframe` path and record type → base URL.
169+
- Publish a **read-only `devframe:view-providers`** shared-state map
170+
(type → `{ base }`), seeded node-side; same on the manual `createHubContext`
171+
path. Keep the in-process `renderers` seam untouched.
172+
- Document the provider contract (mountable devframe; `remote` URL extension) and
173+
the (unchanged, structurally-decoupled) `DockRenderer` contract.
174+
175+
### PR D — hub-ui: render providers as iframes · depends on: B, C
176+
- Read `devframe:view-providers` at context init; for a provider-backed dock type
177+
(json-render + any future type), render a **pooled iframe**
178+
(`frameId = provider+stateKey`, `src = <base>?view=<stateKey>`) — via
179+
`ViewIframe` or a thin `ViewProvider.vue`.
180+
- **Inline resolver**: `view: { spec }` → write to an ephemeral shared-state key,
181+
pass that key, dispose on unmount.
182+
- **No-provider empty state** component, driven by the absent map entry.
183+
- Migrate `ViewEntry.vue` json-render branch to the provider iframe; migrate off
184+
`entry.ui._stateKey` to `entry.view`. **Delete** `src/client/json-render/`,
185+
`ViewJsonRender.vue`, the local `DevframeViewJsonRender` type, and the
186+
`--jr-primary` bridge (json-render now themes itself in its own SPA).
187+
188+
### PR E — examples · depends on: B, C
189+
- Wire `initHub({ viewProviders: { 'json-render': jsonRenderProvider() } })` in the
190+
Vite and Next examples.
191+
- **Delete the Next example's React json-render registry + dock-renderer** — the
192+
shared iframe provider renders it. (Supersedes the prior "React example parity"
193+
decision: no React json-render implementation is needed.)
194+
195+
### PR F — later cycle (out of scope now) · depends on: D, E
196+
Remove the deprecated hub json-render API (`DevframeViewJsonRender`,
197+
`createJsonRenderer`, `_stateKey`) and json-render-ui's deprecated
198+
`createJsonRenderDockRenderer`. Its own breaking-change cycle.
199+
200+
## Interactions & sequencing notes
201+
202+
- **Branding stack (#176, #177)**: PR D deletes the inline json-render and the
203+
`--jr-primary` bridge #177 added; json-render now themes via `branding.json`
204+
inside its own SPA (decision 10). Land D after #176/#177 merge (or rebase).
205+
- **Additive catalog**: PR A cannot break existing specs.
206+
- **One implementation everywhere**: after D+E, hub-ui and both examples render
207+
json-render through the same iframe provider SPA — the triplication is gone and
208+
the React port is deleted, not duplicated.
209+
- **`__index.json` avoided**: discovery rides `devframe:view-providers` shared
210+
state (decision 6), sidestepping the manual-path/`__index.json` fetch gap found
211+
during the branding work.
212+
213+
## Non-goals
214+
215+
- A runtime multi-UI registry or config-file UI selection (decision 12).
216+
- First-party non-Vue renderer packages — unnecessary now that any host reuses the
217+
Vue provider via iframe (decision 2/3).
218+
- Removing the in-process `renderers` seam (kept for custom in-process types,
219+
decision 3).
220+
- First-class remote/off-origin providers in v1 (documented extension only,
221+
decision 5).
222+
- Removing deprecated APIs now (PR F, later cycle).

0 commit comments

Comments
 (0)