Skip to content

Commit 6cd18c8

Browse files
committed
docs: add minimal-json-render example and JSON-render guide
- examples/minimal-json-render: a standalone Vue + Vite devframe that authors a JSON-render view server-side (createJsonRenderView), ticks live state, bridges a Button action to RPC, and renders it with @devframes/json-render-ui. - docs/guide/json-render.md: authoring, base catalog, actions/state, standalone and hub rendering, and swapping the frontend; linked from the guide index. - docs/examples/minimal-json-render.md + examples index/sidebar entry. - hub + client-context guides updated for the open dock union and the client host renderer registry (no more hub createJsonRenderer). Created with the help of an agent.
1 parent 9a9595a commit 6cd18c8

20 files changed

Lines changed: 569 additions & 3 deletions

docs/.vitepress/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ function guideItems(prefix: string) {
2424
{ text: 'Cross-Plugin Services', link: `${prefix}/guide/services` },
2525
{ text: 'RPC', link: `${prefix}/guide/rpc` },
2626
{ text: 'Shared State', link: `${prefix}/guide/shared-state` },
27+
{ text: 'JSON-Render', link: `${prefix}/guide/json-render` },
2728
{ text: 'Streaming', link: `${prefix}/guide/streaming` },
2829
{ text: 'When Clauses', link: `${prefix}/guide/when-clauses` },
2930
{ text: 'Structured Diagnostics', link: `${prefix}/guide/diagnostics` },
@@ -76,6 +77,7 @@ function examplesItems(prefix: string) {
7677
{ text: 'Overview', link: `${prefix}/examples/` },
7778
{ text: 'Built with Devframe', link: `${prefix}/examples/built-with` },
7879
{ text: 'files-inspector', link: `${prefix}/examples/files-inspector` },
80+
{ text: 'minimal-json-render', link: `${prefix}/examples/minimal-json-render` },
7981
{ text: 'streaming-chat', link: `${prefix}/examples/streaming-chat` },
8082
{ text: 'next-runtime-snapshot', link: `${prefix}/examples/next-runtime-snapshot` },
8183
{ text: 'minimal-vite-devframe-hub', link: `${prefix}/examples/minimal-vite-devframe-hub` },

docs/examples/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ End-to-end examples that exercise the full adapter surface, each a runnable app
99
| Example | UI framework | What it shows |
1010
|---------|--------------|---------------|
1111
| [files-inspector](./files-inspector) | Preact | Lists files in the cwd via RPC; exercises the CLI dev / build / spa surfaces. |
12+
| [minimal-json-render](./minimal-json-render) | Vue | A server-authored JSON-render view rendered by `@devframes/json-render-ui`, with live state and an action bridge. |
1213
| [streaming-chat](./streaming-chat) | Preact | Streams synthetic chat tokens server → client, with history kept in shared state. |
1314
| [next-runtime-snapshot](./next-runtime-snapshot) | React (Next.js) | A Next.js App Router SPA over RPC, surfacing the host Node runtime. |
1415
| [minimal-vite-devframe-hub](./minimal-vite-devframe-hub) | Vanilla TypeScript (Vite) | A ~120-line Vite host wiring `@devframes/hub` end to end. |
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# minimal-json-render
6+
7+
A standalone devframe that serves a **JSON-render view**: the server authors an
8+
`@json-render/core` spec once, and the browser renders it with the official
9+
`@devframes/json-render-ui` Vue frontend.
10+
11+
Package: `minimal-json-render` · framework: **Vue + Vite**
12+
13+
## What it shows
14+
15+
- **`createJsonRenderView`** — registers the spec as shared state, validates
16+
element props at ingress, and returns a handle with `update` / `patchState` /
17+
`dispose`.
18+
- **Live state** — the server ticks `uptime` every second via `patchState`, so
19+
the view updates without replacing the whole spec.
20+
- **Action bridge** — the `Refresh` button's `press` action is dispatched as an
21+
RPC call of the same name; the handler bumps a counter and patches state, with
22+
per-action loading and error surfacing.
23+
- **Standalone rendering** — the app supplies the frontend lib
24+
(`@devframes/json-render-ui`); devframe serves the SPA, which subscribes to
25+
the view's shared state and renders it with `JsonRenderView`.
26+
- **Static output**`cli:build` snapshots the spec + state as a read-only
27+
render; the action bridge reports actions as unavailable (no live RPC).
28+
29+
## Run it
30+
31+
```sh
32+
pnpm --filter minimal-json-render dev # CLI dev server (live RPC)
33+
pnpm --filter minimal-json-render build # build the Vue client
34+
pnpm --filter minimal-json-render cli:build # static deploy → dist/static
35+
```
36+
37+
The dev server serves the SPA at `/__minimal-json-render/`.
38+
39+
## Source
40+
41+
[`examples/minimal-json-render`](https://github.com/devframes/devframe/tree/main/examples/minimal-json-render)

docs/guide/client-context.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Viewers with an HTML pipeline layer injection on top: `@vitejs/devtools` wraps t
3838
| `connect` | Options forwarded to `connectDevframe` when `rpc` is not supplied — pass `baseURL` to point at the hub's connection-meta mount (e.g. `/__hub/`). |
3939
| `clientType` | `'standalone'` (default) — the runtime owns the whole page (a hub UI). `'embedded'` — the runtime lives inside a user app alongside a panel. |
4040
| `loadClientScripts` | Import and run dock entries' client scripts. Default `true`. |
41+
| `renderers` | Dock renderers to register at boot, keyed by dock `type` (e.g. `{ 'json-render': createJsonRenderDockRenderer() }` from `@devframes/json-render-ui`). The hub ships none. |
4142

4243
Boot the host once per page: a second boot replaces the published context and logs a warning. `dispose()` tears down its listeners and unpublishes the context it owns.
4344

@@ -52,6 +53,7 @@ Boot the host once per page: a second boot replaces the published context and lo
5253
| `docks` | Dock entries and selection — `entries`, `selected`, `groupedEntries`, `switchEntry()`, `toggleEntry()`, `getStateById()`. |
5354
| `panel` | Dock panel state: position, size, drag/resize flags. |
5455
| `commands` | The command palette: `register()`, `execute()`, `getKeybindings()`. |
56+
| `renderers` | Dock-renderer registry — `register()`, `get()`, `has()`, `mount(entry, container)`. Routes a dock `type` to a host-registered renderer (e.g. [JSON-Render](./json-render)); the hub ships none. |
5557
| `when` | The [when-clause](./when-clauses) evaluation context. |
5658
| `connection` | The client's live [connection status](./client#handling-connection-and-auth-errors)`status`, `error`, and `events` — so a viewer can render one central connection indicator for every docked plugin. |
5759

docs/guide/hub.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ A hub-aware node context (`DevframeHubContext`) extends `DevframeNodeContext` wi
1212

1313
| Subsystem | Surface | Purpose |
1414
|---|---|---|
15-
| `ctx.docks` | `register / update / values / activate` | Multi-tool dock entries (iframes, launchers, json-render, custom-render) and groups that collapse them under one button. `activate(dockId, params?)` steers which dock the viewer shows — see [Cross-iframe dock activation](#cross-iframe-dock-activation). |
15+
| `ctx.docks` | `register / update / values / activate` | Multi-tool dock entries (iframes, launchers, custom-render) and groups that collapse them under one button. The dock union is **open**, so opt-in integrations contribute their own entry types. `activate(dockId, params?)` steers which dock the viewer shows — see [Cross-iframe dock activation](#cross-iframe-dock-activation). |
1616
| `ctx.terminals` | `register / startChildProcess` | Aggregate terminal sessions, stream output over a well-known channel. The single source of truth for "what sessions exist" — see [Terminals](/plugins/terminals#hub-aggregation) for how the terminals plugin renders and mirrors into it. |
1717
| `ctx.messages` | `add / update / remove / clear` | Server-side toast/notification queue (FIFO, capped at 1000). |
1818
| `ctx.commands` | `register / execute / list` | Hierarchical command palette with keybindings and `when` clauses. |
1919

20-
Plus a `createJsonRenderer(spec)` factory for building remote-UI panels via the framework-neutral json-render DSL.
20+
The hub itself is JSON-render-agnostic. Data-driven UI panels are an opt-in integration — see [JSON-Render](/guide/json-render), which contributes a `json-render` dock type to the open dock union and a client-host renderer, with no JSON-render dependency in the hub.
2121

2222
## Built-in RPC
2323

@@ -197,7 +197,7 @@ ctx.docks.register({
197197
})
198198
```
199199

200-
`groupId` lives on every entry kind, so iframes, launchers, json-render panels, and custom-render views all join groups the same way. The group and its members stay independent top-level entries in `devframe:docks`; a downstream UI derives the visual collapse by matching each member's `groupId` to the group's `id` and renders members in a popover or sub-navigation. `defaultChildId` names the member opened when the group button is activated.
200+
`groupId` lives on every entry kind, so iframes, launchers, custom-render views, and integration-contributed types (e.g. json-render panels) all join groups the same way. The group and its members stay independent top-level entries in `devframe:docks`; a downstream UI derives the visual collapse by matching each member's `groupId` to the group's `id` and renders members in a popover or sub-navigation. `defaultChildId` names the member opened when the group button is activated.
201201

202202
Grouping is one level deep: members join a group, and a group is always a top-level button. A member whose group is never registered renders as a normal top-level entry, so registration order is free.
203203

docs/guide/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Devframe keeps its surface focused on one tool, so the same definition stays por
2626
| **[Devframe Definition](./devframe-definition)** | One `defineDevframe` call describes your tool once; the adapters deploy it anywhere. |
2727
| **[RPC](./rpc)** | Type-safe bidirectional calls built on birpc + valibot. Supports `query`, `static`, `action`, and `event` types. |
2828
| **[Shared State](./shared-state)** | Observable, patch-synced state that survives reconnects and bridges server ↔ browser. |
29+
| **[JSON-Render](./json-render)** | Opt-in data-driven UI — author a view as a serializable spec, render it standalone or in a hub dock with a replaceable frontend. |
2930
| **[Diagnostics](./diagnostics)** | Coded warnings/errors via `nostics` — registered into the host's shared lookup so adapters and consumers share the same surface. |
3031
| **[Streaming](./streaming)** | One-way (RPC streaming) and two-way (uploads) channel primitives for long-running data. |
3132
| **[When Clauses](./when-clauses)** | VS Code-style conditional expressions for docks, commands, and custom UI. |

docs/guide/json-render.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# JSON-Render
6+
7+
JSON-render lets a devframe describe a UI as **data** — a serializable spec of
8+
components — and have any compatible frontend render it. It is an **opt-in**
9+
capability: a plain devframe app pulls zero JSON-render dependencies, and the
10+
hub stays JSON-render-agnostic. You add it by depending on two packages:
11+
12+
- **`@devframes/json-render`** — the framework-neutral protocol layer. It owns
13+
the spec/catalog types, the base catalog and its per-component prop schemas,
14+
the serializable view reference, and the node runtime factory. It builds on
15+
[`@json-render/core`](https://www.npmjs.com/package/@json-render/core) as its
16+
wire contract and has no Vue or DOM code.
17+
- **`@devframes/json-render-ui`** — the official reference frontend: a Vue
18+
renderer implementing the base catalog with [`@antfu/design`](https://github.com/antfu/design).
19+
Any compatible frontend library can replace it.
20+
21+
A single view authored once renders standalone (the app supplies a frontend
22+
lib) and inside a hub dock (the hub supplies one), live or static.
23+
24+
## Authoring a view
25+
26+
`createJsonRenderView` augments any devframe context. It registers the spec as
27+
shared state, validates every element's props against the base catalog at
28+
ingress, and returns a handle:
29+
30+
```ts
31+
import { createJsonRenderView } from '@devframes/json-render/node'
32+
33+
export default defineDevframe({
34+
//
35+
setup(ctx) {
36+
const view = createJsonRenderView(ctx, {
37+
id: 'metrics', // stable, unique within the scope
38+
spec: {
39+
root: 'root',
40+
elements: {
41+
root: { type: 'Card', props: { title: 'Live metrics' }, children: ['count'] },
42+
count: { type: 'Text', props: { text: { $state: '/count' } }, children: [] },
43+
},
44+
state: { count: 0 },
45+
},
46+
})
47+
48+
// A structural change replaces the whole spec…
49+
view.update(nextSpec)
50+
// …while state travels as JSON-Pointer patches (only the changed path crosses the wire).
51+
view.patchState([{ op: 'replace', path: '/count', value: 3 }])
52+
53+
// Unregisters the shared state and its listeners.
54+
// view.dispose()
55+
},
56+
})
57+
```
58+
59+
The view has a stable, scoped id — `devframe:json-render:<scope>:<id>` — so a
60+
client keeps its subscription across reconnects. `scope` defaults to the
61+
context's namespace (from `ctx.scope('my-plugin')`) or `global`. Element props
62+
are validated at ingress ([DF0038](/errors/DF0038)); a duplicate id
63+
([DF0039](/errors/DF0039)), a disposed-view use ([DF0040](/errors/DF0040)), and
64+
a non-JSON-serializable spec ([DF0041](/errors/DF0041)) each raise a diagnostic.
65+
66+
## The base catalog
67+
68+
Catalog v1 ships fourteen components — `Stack`, `Card`, `Text`, `Badge`,
69+
`Button`, `Icon`, `Divider`, `TextInput`, `Switch`, `KeyValueTable`,
70+
`DataTable`, `CodeBlock`, `Progress`, `Tree`. A Devframes spec **is** an
71+
`@json-render/core` `Spec`; the one validation Devframes adds is a per-component
72+
Zod prop schema (`basePropSchemas`), applied at both boundaries — spec ingress
73+
(server) and render time (client). Dynamic `$state` / `$bindState` expressions
74+
are accepted wherever a scalar prop is expected, so a valid binding never fails
75+
validation.
76+
77+
## Actions and state
78+
79+
- **State** is a JSON-serializable `Record<string, unknown>` addressed by JSON
80+
Pointer. State updates travel as patches; a structural change replaces the
81+
whole spec.
82+
- **Actions** are unrestricted: an element event maps to an action whose name is
83+
dispatched as an RPC call of the same name. There is no allowlist — a spec
84+
can invoke any RPC method the client can reach. The reference bridge tracks
85+
per-action loading state and surfaces RPC failures to the view rather than
86+
swallowing them.
87+
- **Reserved built-ins** (`setState`, `pushState`, `removeState`,
88+
`validateForm`) are handled client-side and are never bridged to RPC.
89+
90+
## Rendering standalone
91+
92+
The app supplies the frontend lib and devframe serves its SPA. Connect, read the
93+
view's shared state, and render it with `JsonRenderView`:
94+
95+
```ts
96+
import { JSON_RENDER_UPSTREAM_VERSION } from '@devframes/json-render'
97+
import { JsonRenderView } from '@devframes/json-render-ui'
98+
import { connectDevframe } from 'devframe/client'
99+
import { createApp, h, shallowRef } from 'vue'
100+
101+
const rpc = await connectDevframe()
102+
const state = await rpc.sharedState.get('devframe:json-render:global:metrics', { initialValue: null })
103+
const spec = shallowRef(state.value())
104+
state.on('updated', () => {
105+
spec.value = state.value()
106+
})
107+
108+
createApp({
109+
render: () => h(JsonRenderView, {
110+
spec: spec.value,
111+
rpc,
112+
upstreamVersion: JSON_RENDER_UPSTREAM_VERSION,
113+
interactive: rpc.connectionMeta.backend !== 'static',
114+
}),
115+
}).mount('#app')
116+
```
117+
118+
In a **static** build the spec + state are snapshotted as a read-only render;
119+
there is no live RPC, so the action bridge reports actions as unavailable and
120+
`interactive: false` renders a static-output notice. Local state and bindings
121+
still work.
122+
123+
The reference components author their class strings in `.ts`, so a consuming
124+
app must opt `.ts` into UnoCSS extraction (`content.pipeline.include` for Vite),
125+
and add the lib to the scanned sources when it lives in `node_modules`.
126+
127+
## Rendering inside a hub
128+
129+
The hub is JSON-render-agnostic; its dock union is **open**. The
130+
`@devframes/json-render/hub` subpath contributes the `json-render` dock type,
131+
and the hub's client host routes it to a **registered renderer**:
132+
133+
```ts
134+
// server — register a dock carrying the view's serializable reference
135+
import { toJsonRenderDockEntry } from '@devframes/json-render/hub'
136+
137+
ctx.docks.register(toJsonRenderDockEntry(view, {
138+
id: 'metrics',
139+
title: 'Metrics',
140+
icon: 'ph:chart-bar-duotone',
141+
}))
142+
```
143+
144+
```ts
145+
// host page — inject the frontend lib as the renderer for the dock type
146+
import { createDevframeClientHost } from '@devframes/hub/client'
147+
import { createJsonRenderDockRenderer } from '@devframes/json-render-ui'
148+
149+
const host = await createDevframeClientHost({
150+
renderers: { 'json-render': createJsonRenderDockRenderer() },
151+
})
152+
153+
// the viewer mounts the active dock into a container it owns
154+
const dispose = await host.context.renderers.mount(entry, container)
155+
```
156+
157+
The dock carries only a serializable `JsonRenderViewRef` (`{ stateKey,
158+
upstreamVersion }`) — no functions cross the wire. The client host disposes the
159+
renderer when the dock deactivates. A renderer/upstream-version mismatch logs a
160+
warning rather than blocking.
161+
162+
## Swapping the frontend
163+
164+
A third party replaces the whole registry — pass a custom `registry` to
165+
`createRenderer({ registry })` or `createJsonRenderDockRenderer({ registry })`.
166+
`@devframes/json-render-ui` is the reference implementation, not a hard
167+
dependency of the protocol; the hub acquires no Vue.
168+
169+
See the [`minimal-json-render` example](/examples/minimal-json-render) for a
170+
runnable end-to-end app.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# minimal-json-render
2+
3+
A standalone devframe that serves a **JSON-render view**: the server authors an
4+
`@json-render/core` spec once, and the browser renders it with the official
5+
`@devframes/json-render-ui` Vue frontend. It exercises the whole opt-in
6+
JSON-render stack end to end:
7+
8+
- **`@devframes/json-render/node`**`createJsonRenderView(ctx, { id, spec })`
9+
registers the spec as shared state, validates element props at ingress, and
10+
hands back a handle with `update` / `patchState` / `dispose`.
11+
- **live state** — the server ticks `uptime` every second via `patchState`, so
12+
the view updates without replacing the whole spec.
13+
- **action bridge** — the `Refresh` button's `press` action is dispatched as an
14+
RPC call of the same name; the handler bumps a counter and patches state.
15+
- **`@devframes/json-render-ui`** — the SPA (`src/client`) subscribes to the
16+
view's shared state and renders it with `JsonRenderView`. The app supplies the
17+
frontend lib; devframe serves the SPA.
18+
19+
## Run
20+
21+
```sh
22+
pnpm --filter minimal-json-render dev # live dev server (http://localhost:9877/__minimal-json-render/)
23+
pnpm --filter minimal-json-render build # build the SPA
24+
node bin.mjs build --out-dir dist/static # static snapshot (read-only; actions render disabled)
25+
```
26+
27+
In the static build, the spec + state are snapshotted as a read-only render and
28+
the action bridge reports actions as unavailable — there is no live RPC.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env node
2+
import process from 'node:process'
3+
import { createCac } from 'devframe/adapters/cac'
4+
import devframe from './src/devframe.ts'
5+
6+
async function main() {
7+
const cli = createCac(devframe)
8+
await cli.parse()
9+
}
10+
11+
main().catch((error) => {
12+
console.error(error)
13+
process.exit(1)
14+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "minimal-json-render",
3+
"type": "module",
4+
"version": "0.7.5",
5+
"private": true,
6+
"description": "Standalone devframe that serves a JSON-render view — a server-authored spec rendered by @devframes/json-render-ui, with live state and an action bridge.",
7+
"homepage": "https://github.com/devframes/devframe/tree/main/examples/minimal-json-render",
8+
"main": "src/devframe.ts",
9+
"bin": {
10+
"minimal-json-render": "./bin.mjs"
11+
},
12+
"scripts": {
13+
"build": "vite build --config src/client/vite.config.ts",
14+
"dev": "node bin.mjs",
15+
"cli:build": "node bin.mjs build --out-dir dist/static",
16+
"typecheck": "tsc --noEmit"
17+
},
18+
"dependencies": {
19+
"@antfu/design": "catalog:frontend",
20+
"@devframes/json-render": "workspace:*",
21+
"@devframes/json-render-ui": "workspace:*",
22+
"cac": "catalog:deps",
23+
"devframe": "workspace:*",
24+
"vue": "catalog:frontend"
25+
},
26+
"devDependencies": {
27+
"@vitejs/plugin-vue": "catalog:build",
28+
"get-port-please": "catalog:deps",
29+
"unocss": "catalog:frontend",
30+
"vite": "catalog:build"
31+
}
32+
}

0 commit comments

Comments
 (0)