Skip to content

Commit ef75b1a

Browse files
antfubotantfu
andauthored
feat: JSON-render packages (@devframes/json-render + @devframes/json-render-ui) (#116)
Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 32ef7f2 commit ef75b1a

125 files changed

Lines changed: 4963 additions & 145 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎alias.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ export const alias = {
4646
'@devframes/hub': r('hub/src/index.ts'),
4747
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
4848
'@devframes/nuxt': r('nuxt/src/index.ts'),
49+
'@devframes/json-render/core': r('json-render/src/core.ts'),
50+
'@devframes/json-render/hub': r('json-render/src/hub.ts'),
51+
'@devframes/json-render/node': r('json-render/src/node/index.ts'),
52+
'@devframes/json-render': r('json-render/src/index.ts'),
53+
'@devframes/json-render-ui/components': r('json-render-ui/src/components/index.ts'),
54+
'@devframes/json-render-ui': r('json-render-ui/src/index.ts'),
55+
'minimal-json-render/dashboard': fileURLToPath(new URL('./examples/minimal-json-render/src/dashboard.ts', import.meta.url)),
4956
'@devframes/plugin-code-server/client': p('code-server/src/client/index.ts'),
5057
'@devframes/plugin-code-server/node': p('code-server/src/node/index.ts'),
5158
'@devframes/plugin-code-server/constants': p('code-server/src/constants.ts'),

‎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` },
@@ -77,6 +78,7 @@ function examplesItems(prefix: string) {
7778
{ text: 'Overview', link: `${prefix}/examples/` },
7879
{ text: 'Built with Devframe', link: `${prefix}/examples/built-with` },
7980
{ text: 'files-inspector', link: `${prefix}/examples/files-inspector` },
81+
{ text: 'minimal-json-render', link: `${prefix}/examples/minimal-json-render` },
8082
{ text: 'streaming-chat', link: `${prefix}/examples/streaming-chat` },
8183
{ text: 'next-runtime-snapshot', link: `${prefix}/examples/next-runtime-snapshot` },
8284
{ text: 'minimal-vite-devframe-hub', link: `${prefix}/examples/minimal-vite-devframe-hub` },

‎docs/errors/DF0038.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0038: Invalid JSON-Render Element Props
6+
7+
## Message
8+
9+
> JSON-render view "`{id}`" received invalid props on element "`{key}`": `{issues}`
10+
11+
## Cause
12+
13+
`@devframes/json-render` validates every element's props against the base catalog's per-component Zod schema at spec ingress (`createJsonRenderView` / `view.update`). Upstream `@json-render/core` only checks component *names*, so this per-component prop check is the one validation Devframes adds. An element whose props don't match its component's schema is rejected here rather than failing silently at render.
14+
15+
## Example
16+
17+
```ts
18+
// ✗ Bad — `variant` is not one of the Button variants
19+
createJsonRenderView(ctx, {
20+
id: 'toolbar',
21+
spec: { root: 'a', elements: { a: { type: 'Button', props: { variant: 'nope' }, children: [] } } },
22+
})
23+
24+
// ✓ Good
25+
createJsonRenderView(ctx, {
26+
id: 'toolbar',
27+
spec: { root: 'a', elements: { a: { type: 'Button', props: { variant: 'primary', label: 'Save' }, children: [] } } },
28+
})
29+
```
30+
31+
## Fix
32+
33+
Match the element props to the base catalog's prop schema for that component. Dynamic `$state` / `$bindState` expressions are accepted wherever a scalar prop is expected, so a valid binding never triggers this.
34+
35+
## Source
36+
37+
- [`packages/json-render/src/node/create-view.ts`](https://github.com/devframes/devframe/blob/main/packages/json-render/src/node/create-view.ts) — `validateElementProps()` throws this at ingress and on `update`.

‎docs/errors/DF0039.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0039: Duplicate JSON-Render View
6+
7+
## Message
8+
9+
> A JSON-render view with id "`{id}`" already exists in scope "`{scope}`".
10+
11+
## Cause
12+
13+
Each JSON-render view has a stable, author-supplied id that forms its shared-state key `devframe:json-render:<scope>:<id>`. Ids must be unique within a scope so the client keeps a stable subscription across reconnects. Creating a second view with the same id in the same scope throws instead of clobbering the first.
14+
15+
## Example
16+
17+
```ts
18+
// ✗ Bad — same id twice in the same scope
19+
createJsonRenderView(ctx, { id: 'metrics', spec })
20+
createJsonRenderView(ctx, { id: 'metrics', spec }) // DF0039
21+
22+
// ✓ Good — dispose the previous view first, or use a distinct id
23+
const view = createJsonRenderView(ctx, { id: 'metrics', spec })
24+
view.dispose()
25+
createJsonRenderView(ctx, { id: 'metrics', spec })
26+
```
27+
28+
## Fix
29+
30+
Give each view a stable id unique within its scope, or dispose the previous view before recreating it. Scope defaults to the context namespace (or `global`); pass `scope` to isolate ids explicitly.
31+
32+
## Source
33+
34+
- [`packages/json-render/src/node/create-view.ts`](https://github.com/devframes/devframe/blob/main/packages/json-render/src/node/create-view.ts) — `createJsonRenderView()` throws this when the scoped id is already live.

‎docs/errors/DF0040.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0040: JSON-Render View Used After Disposal
6+
7+
## Message
8+
9+
> JSON-render view "`{id}`" was used after it was disposed.
10+
11+
## Cause
12+
13+
`view.dispose()` unregisters the view's shared state and its broadcast listeners. Calling `update` or `patchState` on a disposed handle is a lifecycle bug — the shared state it targeted no longer exists.
14+
15+
## Example
16+
17+
```ts
18+
const view = createJsonRenderView(ctx, { id: 'metrics', spec })
19+
view.dispose()
20+
view.update(nextSpec) // DF0040
21+
```
22+
23+
## Fix
24+
25+
Create a fresh view with `createJsonRenderView` instead of reusing a disposed handle. The id is free to reuse once disposed.
26+
27+
## Source
28+
29+
- [`packages/json-render/src/node/create-view.ts`](https://github.com/devframes/devframe/blob/main/packages/json-render/src/node/create-view.ts) — `update` / `patchState` throw this after `dispose()`.

‎docs/errors/DF0041.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0041: JSON-Render Spec Is Not JSON-Serializable
6+
7+
## Message
8+
9+
> JSON-render view "`{id}`" spec is not JSON-serializable: `{reason}`
10+
11+
## Cause
12+
13+
Specs and state travel across the RPC / static boundary as strict JSON. A spec containing functions, symbols, class instances, `Map`/`Set`, or circular references cannot cross that boundary, so it is rejected at ingress.
14+
15+
## Example
16+
17+
```ts
18+
// ✗ Bad — circular reference
19+
const spec: any = { root: 'a', elements: {} }
20+
spec.self = spec
21+
createJsonRenderView(ctx, { id: 'x', spec }) // DF0041
22+
23+
// ✓ Good — plain JSON data
24+
createJsonRenderView(ctx, { id: 'x', spec: { root: 'a', elements: { a: { type: 'Text', props: { text: 'hi' }, children: [] } } } })
25+
```
26+
27+
## Fix
28+
29+
Keep specs and state strict JSON — remove functions, symbols, class instances, `Map`/`Set`, or circular references.
30+
31+
## Source
32+
33+
- [`packages/json-render/src/node/create-view.ts`](https://github.com/devframes/devframe/blob/main/packages/json-render/src/node/create-view.ts) — `assertJsonSerializable()` throws this at ingress and on `update`.

‎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/examples/minimal-next-devframe-hub.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Package: `minimal-next-devframe-hub` · framework: **React (Next.js)**
1515
- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock.
1616
- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed.
1717
- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the Next route handler at `/__hub/__connection.json`, which starts the singleton host on demand.
18+
- The [JSON-render](/guide/json-render) hub integration with **registry replacement**: the host authors a view and projects it onto a `json-render` dock, and the React client renders it with a small in-example React registry (rather than the Vue `@devframes/json-render-ui`) — the path a non-Vue host uses.
1819

1920
## Run it
2021

‎docs/examples/minimal-vite-devframe-hub.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Package: `minimal-vite-devframe-hub` · framework: **Vanilla TypeScript (Vite)**
1515
- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock.
1616
- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed.
1717
- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the kit's `__connection.json` middleware.
18+
- The opt-in [JSON-render](/guide/json-render) hub integration end to end: the host authors a view on its hub context and projects it onto a `json-render` dock, and the client host renders it via `@devframes/json-render-ui` (registered through `createDevframeClientHost({ renderers })`).
1819

1920
## Run it
2021

0 commit comments

Comments
 (0)