JSON-Render
JSON-render describes a UI as data: a serializable component spec any frontend renders. Opt-in: a plain devframe pulls zero JSON-render dependencies. Two packages:
@devframes/json-render: framework-neutral protocol layer (spec/catalog types, prop schemas, view refs, node runtime), built on@json-render/core.@devframes/json-render-ui: reference Vue frontend implementing the base catalog with@antfu/design.
Authoring a view
createJsonRenderView registers the spec as shared state, returning a handle:
import { createJsonRenderView } from '@devframes/json-render/node'
export default defineDevframe({
/** … */
setup(ctx) {
const view = createJsonRenderView(ctx, {
id: 'metrics', // stable, unique within the scope
spec: {
root: 'root',
elements: {
root: { type: 'Card', props: { title: 'Live metrics' }, children: ['count'] },
count: { type: 'Text', props: { text: { $state: '/count' } }, children: [] },
},
state: { count: 0 },
},
})
// A structural change replaces the whole spec…
view.update(nextSpec)
// …while state travels as JSON-Pointer patches (only the changed path crosses the wire).
view.patchState([{ op: 'replace', path: '/count', value: 3 }])
// Unregisters the shared state and its listeners.
// view.dispose()
},
})The view id is scoped devframe:json-render:<scope>:<id>, scope defaulting to
the namespace or global. Diagnostics fire on invalid props
(DF0038), duplicate id (DF0039), disposed-view
use (DF0040), non-serializable spec
(DF0041).
The base catalog
Catalog v1 ships fourteen components: Stack, Card, Text, Badge,
Button, Icon, Divider, TextInput, Switch, KeyValueTable,
DataTable, CodeBlock, Progress, Tree. A spec is an @json-render/core
Spec plus a per-component Zod prop schema (basePropSchemas), validated at
ingress (node side) and render time (browser side); $state / $bindState
bindings work on scalar props.
Actions and state
- State is a JSON-serializable
Record<string, unknown>addressed by JSON Pointer. - Actions are unrestricted: an element event dispatches an RPC call of the same name, with no allowlist.
- Reserved built-ins (
setState,pushState,removeState,validateForm) are handled client-side, never bridged to RPC.
Rendering standalone
Out-of-box SPA (no client build)
createJsonRenderDevframe points clientAssets at the prebuilt
@devframes/json-render-ui/spa renderer:
import { createJsonRenderDevframe } from '@devframes/json-render-ui/spa'
import { createJsonRenderView } from '@devframes/json-render/node'
export default createJsonRenderDevframe({
id: 'my-app',
name: 'My App',
version,
packageName,
homepage,
description,
cli: { command: 'my-app', port: 9800 },
setup(ctx) {
createJsonRenderView(ctx, { id: 'main', title: 'Dashboard', spec })
},
})The SPA discovers views from the view index (JSON_RENDER_INDEX_KEY); one
view renders full-bleed, multiple get a title-labeled switcher.
Custom frontend
A custom frontend renders from shared state: connect with connectDevframe(),
read the view's state (keyed devframe:json-render:<scope>:<id>), subscribe to
updated events, and render with your registry; the Next
hub example has a React renderer. In a
static build spec + state are read-only: actions unavailable, local
state and bindings still work.
The reference frontend
@devframes/json-render-ui ships prebuilt /spa and /hub bundles.
Rendering inside a hub
@devframes/json-render/hub adds a json-render dock type:
// node side: register a dock entry carrying the view's serializable reference,
// and compose the frontend as a prebuilt renderer module
import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub'
import { toJsonRenderDockEntry } from '@devframes/json-render/hub'
initHub({
ui: createUi(),
renderers: [jsonRenderUiRenderer()],
configure(ctx) {
ctx.docks.register(toJsonRenderDockEntry(view, {
id: 'metrics',
title: 'Metrics',
icon: 'ph:chart-bar-duotone',
}))
},
})The hub publishes the module in the renderer
manifest, lazily imported on first mount (else a
missing-renderer fallback). A host page can register a
JsonRenderDockRenderer locally instead, overriding the manifest:
// host page: a locally-bundled frontend wins over the manifest module
import { createDevframeClientRuntime } from '@devframes/hub/client'
import { myJsonRenderDockRenderer } from './my-renderer'
const host = await createDevframeClientRuntime({
renderers: { 'json-render': myJsonRenderDockRenderer },
})
// the hub UI provider mounts the active dock entry into a container it owns
const result = await host.context.renderers.mount(entry, container)
if (result.status === 'mounted')
result.dispose // tear down when the hub UI provider decides; deactivation disposes tooThe dock entry carries a serializable JsonRenderViewRef in two shapes: { stateKey }
points at live shared state (createJsonRenderView); { spec } embeds it inline
for a browser-synthesized client-only dock.
Swapping the frontend
@devframes/json-render/hub exports the contract, JsonRenderDockRenderer and
JsonRenderDockMountOptions; @devframes/json-render-ui is the pluggable
reference implementation.
See Build your own JSON-Render frontend
and the json-render example.
Scoped Context
A scoped context is a namespaced view of the context: it auto-prefixes every RPC id, shared-state key, and streaming channel with your tool's id, and adds a typed, persisted settings store, used from a single tool's code.
Structured Diagnostics
ctx.diagnostics is a thin layer over nostics for author-defined coded diagnostics, each with a stable code, docs URL, and structured payload.