Skip to content

Commit 47d81f6

Browse files
authored
docs: json-render packages implementation plan (#114)
1 parent d31b345 commit 47d81f6

3 files changed

Lines changed: 408 additions & 0 deletions

File tree

plans/json-render/plan.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# JSON Render Packages — Implementation Plan
2+
3+
A single, concrete plan for `@devframes/json-render` and `@devframes/json-render-ui`, reflecting the decisions settled in a full design-tree interview. It is grounded in two primary-source studies kept alongside this file:
4+
5+
- [Upstream json-render contracts](research/upstream-json-render.md)`@json-render/core@0.19.0` / `@json-render/vue@0.19.0`.
6+
- [Existing JSON-render integration seams](research/existing-integration-seams.md) — this repo and `vitejs/devtools@v0.4.2`.
7+
8+
Scope is this repository. This PR is **documentation only** — the plan and research; no package scaffolding or code. Implementing, publishing, or releasing the packages, and adopting them in `vitejs/devtools`, are follow-up efforts.
9+
10+
This plan intentionally **supersedes** the earlier wayfinder destination on two points: it does **not** adopt a declared-action allowlist (the action bridge is unrestricted, §4), and it **defers** incremental named catalog extensions (v1 replaces the whole registry instead, §7).
11+
12+
## Destination
13+
14+
Two opt-in packages plus the devframe/hub changes that host them, so that JSON-render is a capability a devframe app *adds*, never a cost a plain app pays:
15+
16+
- `@devframes/json-render` — the opt-in, framework-neutral protocol layer. Augments a devframe context with a view factory; owns the spec/catalog types, the base catalog and its Devframes-authored prop schemas, the serializable view reference, and (when a hub is present) contributes the `json-render` dock type. Uses upstream `@json-render/core` as its wire contract. No Vue, no DOM.
17+
- `@devframes/json-render-ui` — the official reference frontend library: a Vue renderer implementing the base catalog with `@antfu/design`. Any compatible frontend library can replace it.
18+
19+
A single view definition authored once renders standalone (the app supplies a compatible frontend lib) and inside a hub dock (the hub supplies the frontend lib), live or static.
20+
21+
## 1. Package relationship
22+
23+
```
24+
devframe (core) — no json-render awareness; a plain app pulls ZERO json-render deps
25+
▲ peer
26+
@devframes/json-render — OPT-IN protocol layer; augments a devframe ctx (arrow is json-render → devframe only)
27+
▲ peer
28+
@devframes/json-render-ui — OFFICIAL / reference compatible frontend lib (others may be compatible)
29+
30+
@devframes/hub — json-render-AGNOSTIC; extensible dock types; registerRenderer() routes a dock
31+
type to a registered renderer; NO json-render dependency
32+
```
33+
34+
- **`devframe` core stays clean.** It never imports json-render. The current base-context doc comment that names `createJsonRenderer` as a host augmentation is removed. A devframe app that doesn't declare JSON-render views has no new dependency.
35+
- **`@devframes/json-render` is the opt-in layer.** It peer-depends on `devframe` and augments an existing `DevframeNodeContext` through the context's own scope / RPC / shared-state / views surfaces — it does not fork the context. Depends on `@json-render/core` (caret, §3) and `zod`.
36+
- **`@devframes/json-render-ui` is the reference frontend.** It peer-depends on `@devframes/json-render` and `vue`, depends on `@json-render/vue` (caret, paired with core), and dev-depends on `@antfu/design`. It is never a dependency of the protocol package — Vue stays out of the protocol.
37+
- **`@devframes/hub` gains no json-render dependency** (§5).
38+
39+
### Deployment modes
40+
41+
- **Standalone.** The author opts in with `devframe` + `@devframes/json-render` + a compatible frontend lib (`@devframes/json-render-ui`, or another). The `dev` / `build` adapters and the `vite` helper serve that lib's SPA. devframe bundles no renderer by default.
42+
- **Hub-mounted.** The app provides `@devframes/json-render` (protocol + view); the **hub provides the frontend lib** to render, through its client-host `registerRenderer()` (default `@devframes/json-render-ui`). The app need not bundle a UI.
43+
44+
## 2. Package layout and exports
45+
46+
### `@devframes/json-render`
47+
48+
`packages/json-render/`. ESM, `tsdown`, own `tsconfig.json`, `build` + `typecheck` scripts, committed tsnapi snapshot. Node and browser entry graphs kept separate:
49+
50+
| Subpath | Contents |
51+
| --- | --- |
52+
| `.` (isomorphic) | Base catalog + Devframes-authored per-component prop schemas, `JsonRenderViewRef`, and the re-exported protocol types. Pure data + Zod. |
53+
| `./node` | `createJsonRenderView` and devframe integration helpers (RPC/shared-state/scope). Node only. |
54+
| `./core` | Curated named re-exports of `@json-render/core` (below). |
55+
56+
**`./core` re-export — builders + types only.** Explicit named list, never `export *`: `defineSchema`, `defineCatalog`, catalog inference types, `Spec`, `UIElement`, `StateStore`, `createStateStore`. Excludes streaming, prompt/generation, and devtools hooks. Each name is a Devframes semver commitment.
57+
58+
**Type names.** `DevframeJsonRenderSpec` (alias of upstream `Spec`), `JsonRenderView`, `JsonRenderViewRef`. These replace the hub's hand-written `JsonRenderSpec` / `JsonRenderer`.
59+
60+
### `@devframes/json-render-ui`
61+
62+
`packages/json-render-ui/`. Same monorepo contract plus a Storybook.
63+
64+
| Subpath | Contents |
65+
| --- | --- |
66+
| `.` | `createRenderer()` shell, base Vue registry, provider lifecycle wiring, action bridge, loading/error surfaces. |
67+
| `./components` | Individual ported components for direct import / reuse. |
68+
| `./spa` | Prebuilt standalone SPA entry assets (relative base) served by devframe adapters. |
69+
70+
## 3. Upstream coupling
71+
72+
- **Upstream is the wire contract.** A Devframes spec *is* an `@json-render/core` `Spec`; the Vue `schema` is accepted **as-is**. Devframes does not author a bespoke spec schema and does not extend the schema to validate the fields the Vue schema omits (`state`, `on`, `repeat`, `watch`) — those pass structurally unchecked.
73+
- **Per-component prop validation is the one validation Devframes adds.** Because upstream `defineCatalog` collapses multi-component `propsOf` to `Record<string, unknown>`, Devframes authors a Zod prop schema per base component and parses element props against it at **both** boundaries: at spec ingress (server, in `createJsonRenderView` update — reject invalid props with a `DF` diagnostic) and at render time (client renderer, isolate a bad element).
74+
- **Caret range** on `@json-render/core` and `@json-render/vue` (`^0.19.0`, kept paired). No compatibility test suite; the committed **lockfile is the only guard** against a breaking upgrade. This is an accepted risk given the pre-1.0 upstream.
75+
- **Compatibility signal is the upstream package version.** No Devframes protocol/catalog version stamp.
76+
- **Streaming / generative-UI is out of v1.** A complete remote spec renders without it; it may return later behind an opt-in `./core/stream` subpath, reusing core's patch semantics.
77+
- **License.** Apache-2.0, compatible; keep required notices for any redistributed upstream object code and avoid implying Vercel endorsement.
78+
79+
## 4. Actions, state, and validation
80+
81+
- **Action bridge is unrestricted.** An element event maps to an action whose name is dispatched as an RPC call — any spec action string calls any RPC method the client can reach (the current `vitejs/devtools` behavior). There is no declared-action allowlist and no param validation. The capability implication — a spec, or anything that can write one to shared state, can invoke any client-reachable RPC — is accepted.
82+
- **Improve on the Vite bridge's quality.** Unlike the current proxy, the bridge tracks per-action loading state and surfaces RPC failures to the view rather than silently swallowing them to the console.
83+
- **State** is `Record<string, unknown>` addressed by JSON Pointer, JSON-serializable. **State updates travel as JSON-Pointer patches** (enable patches on this server-created shared state, currently disabled); a structural change replaces the whole spec.
84+
- **Serialization.** Strict JSON only for specs and state; the shared-state getter is marked `jsonSerializable: true` so the obligation is enforced.
85+
- **Reserved client-local built-ins** (no RPC): `setState`, `pushState`, `removeState`, `validateForm`, per upstream semantics.
86+
87+
## 5. Devframe runtime integration
88+
89+
`createJsonRenderView` lives in `@devframes/json-render/node` and augments a base `DevframeNodeContext`:
90+
91+
```ts
92+
const view = createJsonRenderView(ctx, { id: 'metrics', spec })
93+
view.update(spec)
94+
view.patchState([{ op: 'replace', path: '/count', value: 3 }])
95+
view.dispose()
96+
```
97+
98+
- **Identity.** Scoped stable IDs `devframe:json-render:<scope>:<authorId>` (author-supplied, stable); a duplicate id within a scope raises a `DF` diagnostic. Replaces the current global allocation-order counter.
99+
- **Lifecycle.** `dispose()` unregisters the shared state and its listeners.
100+
- **Static output.** The build dumps the spec + state as a shared-state query snapshot (read-only render). Action RPC is not dumped; controls whose elements carry `on` handlers render **disabled** with an "unavailable in static output" affordance. Local state/bindings still work.
101+
- **Default UI.** No renderer is bundled into devframe. When a definition declares JSON-render views and the author has added a compatible frontend lib, the `dev` / `build` adapters and the `vite` helper serve that lib's `./spa` assets (relative base, runtime connection discovery preserved). The `spa` adapter is wired when `createSpa` lands. Under a hub the hub supplies the renderer instead.
102+
- **View primitive.** Reuse the existing devframe `views` surface; the JSON-render view is a typed specialization.
103+
104+
## 6. Official Vue + Antfu Design UI
105+
106+
`@devframes/json-render-ui` ships:
107+
108+
- **Base registry** implementing catalog v1's fourteen components (`Stack`, `Card`, `Text`, `Badge`, `Button`, `Icon`, `Divider`, `TextInput`, `Switch`, `KeyValueTable`, `DataTable`, `CodeBlock`, `Progress`, `Tree`) on `@antfu/design` semantic tokens, the shared UnoCSS preset, and dark mode via `.dark`. Reference behavior is the Vite catalog; the hardcoded RGB palette and bespoke button/input styling are replaced by semantic tokens.
109+
- **Corrections from the Vite catalog:** typed event payloads (`Button.press`, `TextInput.change`, `Switch.change`, `DataTable.rowClick` carry their value/row/index); `CodeBlock` honors `language`; components honor `loading`; provider state reset is explicit (see below).
110+
- **Icons are fully dynamic.** The `Icon` component resolves whatever icon name a spec supplies at runtime (sanitized), with no preferred or bundled icon set. This is a deliberate, documented deviation from the repo's Phosphor-first icon convention, which applies to a surface's own chrome, not to spec-driven content icons.
111+
- **Renderer shell** wiring the upstream provider + renderer with the unrestricted action bridge (§4), seeding `spec.state`, and owning reset semantics: **reset provider state on view-identity or upstream-version change; preserve it across ordinary spec/state updates.**
112+
- **Registry replacement.** A third party replaces the whole registry (there is no incremental extension in v1, §7).
113+
- **Standalone SPA** under `./spa` (relative assets, OS-preference dark flip).
114+
- **Storybook** following the repo's one setup, covering every component, loading/error, and a full sample view.
115+
116+
## 7. Hub projection and renderer substitution
117+
118+
- **The hub is json-render-agnostic.** Its dock union becomes **extensible/open** rather than hard-coding a `json-render` variant; the opt-in `@devframes/json-render` integration contributes the `json-render` dock type and its serializable ref. `@devframes/hub` carries **no** json-render dependency. This enlarges the migration: the current closed `DevframeViewJsonRender` variant and the `createJsonRenderer` factory are removed from hub.
119+
- **Serializable dock ref.** `JsonRenderViewRef = { stateKey, upstreamVersion }` — no functions, no Devframes catalog version. This fixes the accidental `_stateKey` projection and the current type/projection mismatch.
120+
- **`registerRenderer()` on `@devframes/hub/client`.** Configured at `createDevframeClientHost` boot; the host application injects `@devframes/json-render-ui` as the default. The client-host routes a dock type to its registered renderer, drives loading/error, and disposes on deactivation (unsubscribing shared-state listeners the Vite viewer leaks). A renderer/upstream-version mismatch logs a warning rather than blocking. The hub package itself acquires no Vue.
121+
- **Extensions deferred.** v1 supports the versioned base catalog plus whole-registry replacement. Incremental named extensions (add-on components with their own identity/version) are post-v1 — they clash with the upstream-version-only compatibility model and add protocol surface now.
122+
123+
## 8. Implementation sequence
124+
125+
Each step is an independently reviewable slice; ⟶ marks the dependency.
126+
127+
1. **Scaffold `@devframes/json-render`** — manifest, `tsdown.config.ts` (isomorphic/node/core graphs), `tsconfig.json`, `typecheck`, empty tsnapi snapshot, caret core dep.
128+
2. **Protocol surface** ⟶1 — `./core` re-exports, base catalog + per-component Zod prop schemas, `JsonRenderViewRef`, `DF` diagnostics. Unit tests.
129+
3. **Runtime factory** ⟶2 — `createJsonRenderView` (scoped IDs, shared state, JSON-Pointer state patches enabled, ingress prop validation, `dispose`), `jsonSerializable` getter.
130+
4. **Hub decoupling** ⟶2 — open the dock union, remove `createJsonRenderer` + the closed json-render variant from hub, add `registerRenderer()` to the client host, regenerate hub tsnapi.
131+
5. **Scaffold `@devframes/json-render-ui` + base registry** ⟶2 — package, fourteen ported components (dynamic `Icon`), tokens, Storybook.
132+
6. **Renderer shell + action bridge** ⟶2,5 — provider/renderer wiring, unrestricted bridge with loading/error, render-time prop validation, reset semantics.
133+
7. **Client-host routing** ⟶4,6 — dock-type → registered renderer, default injection, disposal.
134+
8. **Standalone adapters + SPA** ⟶3,6 — serve the author's frontend lib `./spa`; static snapshot + static-action affordance.
135+
9. **Prototype** ⟶7,8 — one disposable end-to-end prototype (one view: standalone + hub dock + swapped registry + live update + static snapshot); feed ergonomics back before finalizing.
136+
10. **Test matrix** ⟶ across — §9.
137+
11. **Docs + error pages** ⟶ across — package docs, `docs/errors/DFxxxx.md` per new code.
138+
139+
## 9. Test matrix
140+
141+
- Protocol: per-component prop validation (ingress + render), catalog membership.
142+
- Runtime: scoped ID stability, `update` / `patchState`, disposal + listener cleanup, multiple concurrent views, reconnect.
143+
- Transport: state-patch vs whole-spec replace, `jsonSerializable` enforcement, shared-state broadcast.
144+
- Actions: bridge dispatch, per-action loading, error surfacing.
145+
- Hub: extensible dock projection (no functions on the wire), client-host renderer routing/injection, version-mismatch warning, deactivation disposal.
146+
- Standalone: live dev render; static build ordering (states registered before dump), static replay read-only, static-action affordance.
147+
- Vue UI: each component's props/defaults/events/bindings (`press`/`change`/`rowClick`/`$bindState`), dynamic icon resolution, loading/error, reset-on-identity/version-change; Storybook render.
148+
- Snapshots: tsnapi for both new packages and the updated hub; `tests/exports.test.ts` subpaths.
149+
150+
## 10. Diagnostics
151+
152+
New node diagnostics use `nostics` with sequential `DF` codes. `@devframes/json-render` protocol/runtime → the core range (next free after the current highest `DF00xx`); hub dock changes stay in the hub dock range (`DF81xx`). Set: invalid element props (ingress), duplicate view id, disposed-view use, catalog/registry error. Browser-only render failures keep `console.*`.
153+
154+
## 11. Acceptance criteria
155+
156+
- A plain devframe app pulls no json-render dependency; JSON-render is added only by depending on `@devframes/json-render`.
157+
- One view definition renders standalone (the app's frontend lib) and in a hub dock (the hub's registered renderer), live and static.
158+
- `@devframes/hub` and `devframe` acquire no Vue and no json-render dependency respectively; the frontend lib is fully replaceable via `registerRenderer()`.
159+
- Dock projection carries only serializable data; no function survives to the wire, and the projected type matches what the client receives.
160+
- Views have stable scoped IDs and dispose cleanly with no leaked shared state or listeners.
161+
- Element props are validated at ingress and render; the action bridge surfaces loading and errors.
162+
- `pnpm lint && pnpm test && pnpm typecheck && pnpm build` pass, with tsnapi snapshots and exports tests updated.
163+
164+
## Out of scope
165+
166+
- Implementing, publishing, or releasing the packages (this plan is the specification).
167+
- Changing `vitejs/devtools` to consume the packages or deleting its current implementation.
168+
- Official React/Solid/Svelte/vanilla frontend libs — other frontends are supported through registry replacement, not shipped here.
169+
- A declared-action allowlist, a Devframes protocol/catalog version, incremental named catalog extensions, and streaming — each explicitly deferred or declined above.

0 commit comments

Comments
 (0)