Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ export const alias = {
'@devframes/hub': r('hub/src/index.ts'),
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
'@devframes/nuxt': r('nuxt/src/index.ts'),
'@devframes/json-render/core': r('json-render/src/core.ts'),
'@devframes/json-render/hub': r('json-render/src/hub.ts'),
'@devframes/json-render/node': r('json-render/src/node/index.ts'),
'@devframes/json-render': r('json-render/src/index.ts'),
'@devframes/json-render-ui/components': r('json-render-ui/src/components/index.ts'),
'@devframes/json-render-ui': r('json-render-ui/src/index.ts'),
'minimal-json-render/dashboard': fileURLToPath(new URL('./examples/minimal-json-render/src/dashboard.ts', import.meta.url)),
'@devframes/plugin-code-server/client': p('code-server/src/client/index.ts'),
'@devframes/plugin-code-server/node': p('code-server/src/node/index.ts'),
'@devframes/plugin-code-server/constants': p('code-server/src/constants.ts'),
Expand Down
2 changes: 2 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function guideItems(prefix: string) {
{ text: 'Cross-Plugin Services', link: `${prefix}/guide/services` },
{ text: 'RPC', link: `${prefix}/guide/rpc` },
{ text: 'Shared State', link: `${prefix}/guide/shared-state` },
{ text: 'JSON-Render', link: `${prefix}/guide/json-render` },
{ text: 'Streaming', link: `${prefix}/guide/streaming` },
{ text: 'When Clauses', link: `${prefix}/guide/when-clauses` },
{ text: 'Structured Diagnostics', link: `${prefix}/guide/diagnostics` },
Expand Down Expand Up @@ -77,6 +78,7 @@ function examplesItems(prefix: string) {
{ text: 'Overview', link: `${prefix}/examples/` },
{ text: 'Built with Devframe', link: `${prefix}/examples/built-with` },
{ text: 'files-inspector', link: `${prefix}/examples/files-inspector` },
{ text: 'minimal-json-render', link: `${prefix}/examples/minimal-json-render` },
{ text: 'streaming-chat', link: `${prefix}/examples/streaming-chat` },
{ text: 'next-runtime-snapshot', link: `${prefix}/examples/next-runtime-snapshot` },
{ text: 'minimal-vite-devframe-hub', link: `${prefix}/examples/minimal-vite-devframe-hub` },
Expand Down
37 changes: 37 additions & 0 deletions docs/errors/DF0038.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
outline: deep
---

# DF0038: Invalid JSON-Render Element Props

## Message

> JSON-render view "`{id}`" received invalid props on element "`{key}`": `{issues}`

## Cause

`@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.

## Example

```ts
// ✗ Bad — `variant` is not one of the Button variants
createJsonRenderView(ctx, {
id: 'toolbar',
spec: { root: 'a', elements: { a: { type: 'Button', props: { variant: 'nope' }, children: [] } } },
})

// ✓ Good
createJsonRenderView(ctx, {
id: 'toolbar',
spec: { root: 'a', elements: { a: { type: 'Button', props: { variant: 'primary', label: 'Save' }, children: [] } } },
})
```

## Fix

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.

## Source

- [`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`.
34 changes: 34 additions & 0 deletions docs/errors/DF0039.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
outline: deep
---

# DF0039: Duplicate JSON-Render View

## Message

> A JSON-render view with id "`{id}`" already exists in scope "`{scope}`".

## Cause

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.

## Example

```ts
// ✗ Bad — same id twice in the same scope
createJsonRenderView(ctx, { id: 'metrics', spec })
createJsonRenderView(ctx, { id: 'metrics', spec }) // DF0039

// ✓ Good — dispose the previous view first, or use a distinct id
const view = createJsonRenderView(ctx, { id: 'metrics', spec })
view.dispose()
createJsonRenderView(ctx, { id: 'metrics', spec })
```

## Fix

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.

## Source

- [`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.
29 changes: 29 additions & 0 deletions docs/errors/DF0040.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
outline: deep
---

# DF0040: JSON-Render View Used After Disposal

## Message

> JSON-render view "`{id}`" was used after it was disposed.

## Cause

`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.

## Example

```ts
const view = createJsonRenderView(ctx, { id: 'metrics', spec })
view.dispose()
view.update(nextSpec) // DF0040
```

## Fix

Create a fresh view with `createJsonRenderView` instead of reusing a disposed handle. The id is free to reuse once disposed.

## Source

- [`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()`.
33 changes: 33 additions & 0 deletions docs/errors/DF0041.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
outline: deep
---

# DF0041: JSON-Render Spec Is Not JSON-Serializable

## Message

> JSON-render view "`{id}`" spec is not JSON-serializable: `{reason}`

## Cause

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.

## Example

```ts
// ✗ Bad — circular reference
const spec: any = { root: 'a', elements: {} }
spec.self = spec
createJsonRenderView(ctx, { id: 'x', spec }) // DF0041

// ✓ Good — plain JSON data
createJsonRenderView(ctx, { id: 'x', spec: { root: 'a', elements: { a: { type: 'Text', props: { text: 'hi' }, children: [] } } } })
```

## Fix

Keep specs and state strict JSON — remove functions, symbols, class instances, `Map`/`Set`, or circular references.

## Source

- [`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`.
1 change: 1 addition & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ End-to-end examples that exercise the full adapter surface, each a runnable app
| Example | UI framework | What it shows |
|---------|--------------|---------------|
| [files-inspector](./files-inspector) | Preact | Lists files in the cwd via RPC; exercises the CLI dev / build / spa surfaces. |
| [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. |
| [streaming-chat](./streaming-chat) | Preact | Streams synthetic chat tokens server → client, with history kept in shared state. |
| [next-runtime-snapshot](./next-runtime-snapshot) | React (Next.js) | A Next.js App Router SPA over RPC, surfacing the host Node runtime. |
| [minimal-vite-devframe-hub](./minimal-vite-devframe-hub) | Vanilla TypeScript (Vite) | A ~120-line Vite host wiring `@devframes/hub` end to end. |
Expand Down
41 changes: 41 additions & 0 deletions docs/examples/minimal-json-render.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
outline: deep
---

# minimal-json-render

A standalone devframe that serves a **JSON-render view**: the server authors an
`@json-render/core` spec once, and the browser renders it with the official
`@devframes/json-render-ui` Vue frontend.

Package: `minimal-json-render` · framework: **Vue + Vite**

## What it shows

- **`createJsonRenderView`** — registers the spec as shared state, validates
element props at ingress, and returns a handle with `update` / `patchState` /
`dispose`.
- **Live state** — the server ticks `uptime` every second via `patchState`, so
the view updates without replacing the whole spec.
- **Action bridge** — the `Refresh` button's `press` action is dispatched as an
RPC call of the same name; the handler bumps a counter and patches state, with
per-action loading and error surfacing.
- **Standalone rendering** — the app supplies the frontend lib
(`@devframes/json-render-ui`); devframe serves the SPA, which subscribes to
the view's shared state and renders it with `JsonRenderView`.
- **Static output** — `cli:build` snapshots the spec + state as a read-only
render; the action bridge reports actions as unavailable (no live RPC).

## Run it

```sh
pnpm --filter minimal-json-render dev # CLI dev server (live RPC)
pnpm --filter minimal-json-render build # build the Vue client
pnpm --filter minimal-json-render cli:build # static deploy → dist/static
```

The dev server serves the SPA at `/__minimal-json-render/`.

## Source

[`examples/minimal-json-render`](https://github.com/devframes/devframe/tree/main/examples/minimal-json-render)
1 change: 1 addition & 0 deletions docs/examples/minimal-next-devframe-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Package: `minimal-next-devframe-hub` · framework: **React (Next.js)**
- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock.
- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed.
- 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.
- 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.

## Run it

Expand Down
1 change: 1 addition & 0 deletions docs/examples/minimal-vite-devframe-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Package: `minimal-vite-devframe-hub` · framework: **Vanilla TypeScript (Vite)**
- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock.
- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed.
- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the kit's `__connection.json` middleware.
- 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 })`).

## Run it

Expand Down
2 changes: 2 additions & 0 deletions docs/guide/client-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Viewers with an HTML pipeline layer injection on top: `@vitejs/devtools` wraps t
| `connect` | Options forwarded to `connectDevframe` when `rpc` is not supplied — pass `baseURL` to point at the hub's connection-meta mount (e.g. `/__hub/`). |
| `clientType` | `'standalone'` (default) — the runtime owns the whole page (a hub UI). `'embedded'` — the runtime lives inside a user app alongside a panel. |
| `loadClientScripts` | Import and run dock entries' client scripts. Default `true`. |
| `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. |

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.

Expand All @@ -52,6 +53,7 @@ Boot the host once per page: a second boot replaces the published context and lo
| `docks` | Dock entries and selection — `entries`, `selected`, `groupedEntries`, `switchEntry()`, `toggleEntry()`, `getStateById()`. |
| `panel` | Dock panel state: position, size, drag/resize flags. |
| `commands` | The command palette: `register()`, `execute()`, `getKeybindings()`. |
| `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. |
| `when` | The [when-clause](./when-clauses) evaluation context. |
| `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. |

Expand Down
6 changes: 3 additions & 3 deletions docs/guide/hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ A hub-aware node context (`DevframeHubContext`) extends `DevframeNodeContext` wi

| Subsystem | Surface | Purpose |
|---|---|---|
| `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). |
| `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). |
| `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. |
| `ctx.messages` | `add / update / remove / clear` | Server-side toast/notification queue (FIFO, capped at 1000). |
| `ctx.commands` | `register / execute / list` | Hierarchical command palette with keybindings and `when` clauses. |

Plus a `createJsonRenderer(spec)` factory for building remote-UI panels via the framework-neutral json-render DSL.
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.

## Built-in RPC

Expand Down Expand Up @@ -197,7 +197,7 @@ ctx.docks.register({
})
```

`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.
`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.

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.

Expand Down
1 change: 1 addition & 0 deletions docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Devframe keeps its surface focused on one tool, so the same definition stays por
| **[Devframe Definition](./devframe-definition)** | One `defineDevframe` call describes your tool once; the adapters deploy it anywhere. |
| **[RPC](./rpc)** | Type-safe bidirectional calls built on birpc + valibot. Supports `query`, `static`, `action`, and `event` types. |
| **[Shared State](./shared-state)** | Observable, patch-synced state that survives reconnects and bridges server ↔ browser. |
| **[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. |
| **[Diagnostics](./diagnostics)** | Coded warnings/errors via `nostics` — registered into the host's shared lookup so adapters and consumers share the same surface. |
| **[Streaming](./streaming)** | One-way (RPC streaming) and two-way (uploads) channel primitives for long-running data. |
| **[When Clauses](./when-clauses)** | VS Code-style conditional expressions for docks, commands, and custom UI. |
Expand Down
Loading
Loading