Skip to content

Commit f6c36c3

Browse files
committed
feat(json-render): expose native rendering for browser contexts
1 parent 9aa752a commit f6c36c3

20 files changed

Lines changed: 185 additions & 41 deletions

File tree

‎alias.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export const alias = {
7777
'@devframes/vite': r('vite/src/index.ts'),
7878
'@devframes/json-render/core': r('json-render/src/core.ts'),
7979
'@devframes/json-render/hub': r('json-render/src/hub.ts'),
80+
'@devframes/json-render/view': r('json-render/src/node/create-view.ts'),
8081
'@devframes/json-render/node': r('json-render/src/node/index.ts'),
8182
'@devframes/json-render': r('json-render/src/index.ts'),
8283
'@devframes/json-render-ui/hub': r('json-render-ui/src/hub.ts'),

‎docs/content/1.guide/21.build-your-own-json-render-frontend.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Resolve the entry's `view`:
2929
it as the live spec, re-render on `'updated'`. **Unsubscribe in `dispose`.**
3030
- `{ spec }`: render the embedded spec directly.
3131

32-
Detect static output via `context.rpc.connectionMeta.backend === 'static'`,
32+
Detect static output via `context.rpc.connectionMeta?.backend === 'static'`,
3333
disabling action dispatch there.
3434

3535
## Behavior expectations

‎docs/content/1.guide/8.json-render.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,34 @@ for a browser-synthesized [client-only dock](/guide/client-context#client-only-d
167167
reference implementation.
168168
See [Build your own JSON-Render frontend](/guide/build-your-own-json-render-frontend)
169169
and the [`json-render` example](https://github.com/devframes/devframe/tree/main/examples/json-render).
170+
171+
## Rendering with a custom RPC channel
172+
173+
A host page can import the reference browser bundle directly. It includes its renderer and styles. TypeScript consumers also install the `@devframes/json-render`, `@devframes/hub` and `devframe` peers for the renderer declarations:
174+
175+
```ts
176+
import renderer from '@devframes/json-render-ui/renderer'
177+
178+
const mounted = await renderer({
179+
entry,
180+
container,
181+
context: { rpc: { call, sharedState } },
182+
})
183+
184+
// When the surface closes:
185+
mounted.dispose?.()
186+
```
187+
188+
`call` and `sharedState` are native RPC members. An optional `connectionMeta.backend` marks static output. The reference implementation accepts this smaller `JsonRenderRpcContext`; a full hub client context also satisfies it. Custom `JsonRenderDockRenderer` implementations retain the full client context by default and can declare their own context type through its generic parameter.
189+
190+
For view publication in a worker, import `createJsonRenderView` from `@devframes/json-render/view`. Its native publishing state must support `get(key, { sharedState })`. Reuse one context object for all views on that state instance. View discovery and duplicate detection belong to this context:
191+
192+
```ts
193+
import { createJsonRenderView } from '@devframes/json-render/view'
194+
195+
const context = { rpc: { sharedState } }
196+
const metrics = createJsonRenderView(context, { id: 'metrics', spec: metricsSpec })
197+
const details = createJsonRenderView(context, { id: 'details', spec: detailsSpec })
198+
```
199+
200+
The node-side import and scoped node contexts use the same implementation.

‎packages/json-render-ui/package.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
"exports": {
2323
"./hub": "./dist/hub.mjs",
2424
"./spa": "./dist/spa.mjs",
25+
"./renderer": {
26+
"types": "./dist/renderer.d.mts",
27+
"default": "./dist/renderer/json-render.mjs"
28+
},
2529
"./package.json": "./package.json"
2630
},
2731
"files": [
@@ -39,12 +43,16 @@
3943
},
4044
"peerDependencies": {
4145
"@devframes/hub": "workspace:*",
46+
"@devframes/json-render": "workspace:*",
4247
"devframe": "workspace:*"
4348
},
4449
"peerDependenciesMeta": {
4550
"@devframes/hub": {
4651
"optional": true
4752
},
53+
"@devframes/json-render": {
54+
"optional": true
55+
},
4856
"devframe": {
4957
"optional": true
5058
}

‎packages/json-render-ui/src/dock-renderer.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { JsonRenderViewRef, Spec } from '@devframes/json-render'
2-
import type { JsonRenderDockRenderer } from '@devframes/json-render/hub'
2+
import type { JsonRenderDockRenderer, JsonRenderRpcContext } from '@devframes/json-render/hub'
33
import type { ComponentRegistry } from '@json-render/vue'
44
import type { ActionBridgeRpc } from './action-bridge'
55
import { createApp, h, shallowRef } from 'vue'
@@ -33,7 +33,7 @@ export interface JsonRenderDockRendererOptions {
3333
*/
3434
export function createJsonRenderDockRenderer(
3535
options: JsonRenderDockRendererOptions = {},
36-
): JsonRenderDockRenderer {
36+
): JsonRenderDockRenderer<JsonRenderRpcContext> {
3737
const registry = options.registry ?? baseRegistry
3838
return async ({ entry, container, context }) => {
3939
const view: JsonRenderViewRef = entry.view

‎packages/json-render-ui/src/renderer-module/index.ts‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { JsonRenderDockRenderer } from '@devframes/json-render/hub'
1+
import type { DockRendererInstance } from '@devframes/hub/client'
2+
import type { JsonRenderDockRenderer, JsonRenderRpcContext } from '@devframes/json-render/hub'
23
import css from '../.generated/css'
34
import { createJsonRenderDockRenderer } from '../dock-renderer'
45

@@ -32,7 +33,7 @@ const inner = createJsonRenderDockRenderer()
3233
* there, fully styled in a light-DOM host page and inside a viewer's shadow
3334
* root alike, without leaking the reset or any global rule into the page.
3435
*/
35-
const jsonRenderDockRenderer: JsonRenderDockRenderer = async ({ entry, container, context }) => {
36+
const jsonRenderDockRenderer: JsonRenderDockRenderer<JsonRenderRpcContext> = async ({ entry, container, context }) => {
3637
const shadow = container.shadowRoot ?? container.attachShadow({ mode: 'open' })
3738
if (!shadow.querySelector(`style[${STYLE_MARKER}]`)) {
3839
const style = document.createElement('style')
@@ -66,7 +67,15 @@ const jsonRenderDockRenderer: JsonRenderDockRenderer = async ({ entry, container
6667
colorSchemeRoot.append(root)
6768
shadow.append(colorSchemeRoot)
6869

69-
const instance = await inner({ entry, container: root, context })
70+
let instance: DockRendererInstance
71+
try {
72+
instance = await inner({ entry, container: root, context })
73+
}
74+
catch (error) {
75+
observer.disconnect()
76+
colorSchemeRoot.remove()
77+
throw error
78+
}
7079
return {
7180
dispose() {
7281
observer.disconnect()

‎packages/json-render-ui/tsdown.config.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { defineConfig } from 'tsdown'
1414
* emitted `.d.mts` references the packages instead of inlining their whole
1515
* type graph.
1616
*/
17-
export default defineConfig({
17+
export default defineConfig([{
1818
entry: {
1919
/**
2020
* Node-safe entry: the prebuilt SPA path + a devframe wiring helper.
@@ -46,4 +46,11 @@ export default defineConfig({
4646
'@devframes/json-render/core',
4747
],
4848
},
49-
})
49+
}, {
50+
entry: { renderer: 'src/renderer-module/index.ts' },
51+
clean: false,
52+
tsconfig: '../../tsconfig.base.json',
53+
dts: { emitDtsOnly: true },
54+
outExtensions: () => ({ dts: '.d.mts' }),
55+
deps: { neverBundle: ['@devframes/json-render/hub'] },
56+
}])

‎packages/json-render/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"./core": "./dist/core.mjs",
2424
"./hub": "./dist/hub.mjs",
2525
"./node": "./dist/node/index.mjs",
26+
"./view": "./dist/view.mjs",
2627
"./package.json": "./package.json"
2728
},
2829
"types": "./dist/index.d.mts",

‎packages/json-render/src/hub.ts‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import type { DockRenderer, DockRendererMountOptions } from '@devframes/hub/client'
1+
import type { DevframeClientContext, DockRendererInstance, DockRendererMountOptions } from '@devframes/hub/client'
22
import type { DevframeDockEntryBase } from '@devframes/hub/types'
3+
import type { DevframeRpcClient } from 'devframe/client'
4+
import type { ConnectionMeta } from 'devframe/types'
35
import type { JsonRenderView } from './types'
46
import type { JsonRenderViewRef } from './view-ref'
57

@@ -26,12 +28,22 @@ declare module '@devframes/hub/types' {
2628

2729
/**
2830
* The mount options a hub viewer hands a json-render dock renderer: the
29-
* hub's `DockRendererMountOptions` narrowed to the `'json-render'` entry.
31+
* hub's `DockRendererMountOptions` narrowed to the `'json-render'` entry
32+
* and an optional context type. Existing renderers keep the full client context.
3033
* This protocol package owns the renderer contract so every frontend
3134
* (`@devframes/json-render-ui`, a community renderer, a host page's own)
3235
* implements one shared shape instead of re-declaring it.
3336
*/
34-
export type JsonRenderDockMountOptions = DockRendererMountOptions<DevframeJsonRenderDockEntry>
37+
export type JsonRenderDockMountOptions<Context = DevframeClientContext> = Omit<DockRendererMountOptions<DevframeJsonRenderDockEntry>, 'context'> & {
38+
context: Context
39+
}
40+
41+
/** Native RPC calls and shared state consumed by the reference JSON renderer. */
42+
export interface JsonRenderRpcContext {
43+
rpc: Pick<DevframeRpcClient, 'call' | 'sharedState'> & {
44+
connectionMeta?: Pick<ConnectionMeta, 'backend'>
45+
}
46+
}
3547

3648
/**
3749
* The renderer contract for `'json-render'` docks: a hub `DockRenderer`
@@ -41,7 +53,9 @@ export type JsonRenderDockMountOptions = DockRendererMountOptions<DevframeJsonRe
4153
* module registered through the hub's renderer manifest
4254
* (`initHub({ renderers })`).
4355
*/
44-
export type JsonRenderDockRenderer = DockRenderer<DevframeJsonRenderDockEntry>
56+
export type JsonRenderDockRenderer<Context = DevframeClientContext> = (
57+
options: JsonRenderDockMountOptions<Context>,
58+
) => DockRendererInstance | Promise<DockRendererInstance>
4559

4660
/**
4761
* Build a `json-render` dock entry from a {@link JsonRenderView} and the dock

‎packages/json-render/src/node/create-view.ts‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DevframeNodeContext, DevframeScopedNodeContext } from 'devframe'
1+
import type { RpcSharedStateHost } from 'devframe/types'
22
import type { SharedState, SharedStatePatch } from 'devframe/utils/shared-state'
33
import type { StandardSchemaV1 } from 'devframe/utils/simple-schema'
44
import type { DevframeJsonRenderSpec, JsonRenderStatePatch, JsonRenderView } from '../types'
@@ -34,17 +34,28 @@ export interface CreateJsonRenderViewOptions<SpecType extends DevframeJsonRender
3434
title?: string
3535
}
3636

37-
type AnyContext = DevframeNodeContext | DevframeScopedNodeContext<string>
37+
/** Reuse one context per native shared-state instance for view discovery and duplicate detection. */
38+
export interface JsonRenderViewContext {
39+
rpc: { sharedState: RpcSharedStateHost }
40+
}
41+
42+
/** Namespace and base shared state supplied by a scoped node context. */
43+
export interface JsonRenderScopedViewContext {
44+
base: JsonRenderViewContext
45+
namespace: string
46+
}
47+
48+
type AnyContext = JsonRenderViewContext | JsonRenderScopedViewContext
3849

39-
function isScoped(ctx: AnyContext): ctx is DevframeScopedNodeContext<string> {
50+
function isScoped(ctx: AnyContext): ctx is JsonRenderScopedViewContext {
4051
return 'base' in ctx && 'namespace' in ctx
4152
}
4253

4354
// One registry of live view keys per base context, so a duplicate id within a
4455
// scope is caught deterministically (not left to shared-state get() returning
4556
// the pre-existing entry).
4657
const registries = new WeakMap<object, Set<string>>()
47-
function registryFor(ctx: DevframeNodeContext): Set<string> {
58+
function registryFor(ctx: JsonRenderViewContext): Set<string> {
4859
let set = registries.get(ctx)
4960
if (!set) {
5061
set = new Set()
@@ -57,7 +68,7 @@ function registryFor(ctx: DevframeNodeContext): Set<string> {
5768
// `JSON_RENDER_INDEX_KEY`, so a frontend that does not know view ids ahead of
5869
// time can discover every live view from a single subscription.
5970
const indexStates = new WeakMap<object, SharedState<JsonRenderIndex>>()
60-
function indexStateFor(ctx: DevframeNodeContext): SharedState<JsonRenderIndex> {
71+
function indexStateFor(ctx: JsonRenderViewContext): SharedState<JsonRenderIndex> {
6172
let state = indexStates.get(ctx)
6273
if (!state) {
6374
state = createSharedState<JsonRenderIndex>({ initialValue: {} })

0 commit comments

Comments
 (0)