Skip to content
Draft
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
1 change: 1 addition & 0 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const alias = {
'@devframes/vite': r('vite/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/view': r('json-render/src/node/create-view.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/hub': r('json-render-ui/src/hub.ts'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Resolve the entry's `view`:
it as the live spec, re-render on `'updated'`. **Unsubscribe in `dispose`.**
- `{ spec }`: render the embedded spec directly.

Detect static output via `context.rpc.connectionMeta.backend === 'static'`,
Detect static output via `context.rpc.connectionMeta?.backend === 'static'`,
disabling action dispatch there.

## Behavior expectations
Expand Down
31 changes: 31 additions & 0 deletions docs/content/1.guide/8.json-render.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,34 @@ for a browser-synthesized [client-only dock](/guide/client-context#client-only-d
reference implementation.
See [Build your own JSON-Render frontend](/guide/build-your-own-json-render-frontend)
and the [`json-render` example](https://github.com/devframes/devframe/tree/main/examples/json-render).

## Rendering with a custom RPC channel

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:

```ts
import renderer from '@devframes/json-render-ui/renderer'

const mounted = await renderer({
entry,
container,
context: { rpc: { call, sharedState } },
})

// When the surface closes:
mounted.dispose?.()
```

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

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:

```ts
import { createJsonRenderView } from '@devframes/json-render/view'

const context = { rpc: { sharedState } }
const metrics = createJsonRenderView(context, { id: 'metrics', spec: metricsSpec })
const details = createJsonRenderView(context, { id: 'details', spec: detailsSpec })
```

The node-side import and scoped node contexts use the same implementation.
8 changes: 8 additions & 0 deletions packages/json-render-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"exports": {
"./hub": "./dist/hub.mjs",
"./spa": "./dist/spa.mjs",
"./renderer": {
"types": "./dist/renderer.d.mts",
"default": "./dist/renderer/json-render.mjs"
},
"./package.json": "./package.json"
},
"files": [
Expand All @@ -39,12 +43,16 @@
},
"peerDependencies": {
"@devframes/hub": "workspace:*",
"@devframes/json-render": "workspace:*",
"devframe": "workspace:*"
},
"peerDependenciesMeta": {
"@devframes/hub": {
"optional": true
},
"@devframes/json-render": {
"optional": true
},
"devframe": {
"optional": true
}
Expand Down
4 changes: 2 additions & 2 deletions packages/json-render-ui/src/dock-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { JsonRenderViewRef, Spec } from '@devframes/json-render'
import type { JsonRenderDockRenderer } from '@devframes/json-render/hub'
import type { JsonRenderDockRenderer, JsonRenderRpcContext } from '@devframes/json-render/hub'
import type { ComponentRegistry } from '@json-render/vue'
import type { ActionBridgeRpc } from './action-bridge'
import { createApp, h, shallowRef } from 'vue'
Expand Down Expand Up @@ -33,7 +33,7 @@ export interface JsonRenderDockRendererOptions {
*/
export function createJsonRenderDockRenderer(
options: JsonRenderDockRendererOptions = {},
): JsonRenderDockRenderer {
): JsonRenderDockRenderer<JsonRenderRpcContext> {
const registry = options.registry ?? baseRegistry
return async ({ entry, container, context }) => {
const view: JsonRenderViewRef = entry.view
Expand Down
15 changes: 12 additions & 3 deletions packages/json-render-ui/src/renderer-module/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { JsonRenderDockRenderer } from '@devframes/json-render/hub'
import type { DockRendererInstance } from '@devframes/hub/client'
import type { JsonRenderDockRenderer, JsonRenderRpcContext } from '@devframes/json-render/hub'
import css from '../.generated/css'
import { createJsonRenderDockRenderer } from '../dock-renderer'

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

const instance = await inner({ entry, container: root, context })
let instance: DockRendererInstance
try {
instance = await inner({ entry, container: root, context })
}
catch (error) {
observer.disconnect()
colorSchemeRoot.remove()
throw error
}
return {
dispose() {
observer.disconnect()
Expand Down
11 changes: 9 additions & 2 deletions packages/json-render-ui/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { defineConfig } from 'tsdown'
* emitted `.d.mts` references the packages instead of inlining their whole
* type graph.
*/
export default defineConfig({
export default defineConfig([{
entry: {
/**
* Node-safe entry: the prebuilt SPA path + a devframe wiring helper.
Expand Down Expand Up @@ -46,4 +46,11 @@ export default defineConfig({
'@devframes/json-render/core',
],
},
})
}, {
entry: { renderer: 'src/renderer-module/index.ts' },
clean: false,
tsconfig: '../../tsconfig.base.json',
dts: { emitDtsOnly: true },
outExtensions: () => ({ dts: '.d.mts' }),
deps: { neverBundle: ['@devframes/json-render/hub'] },
}])
1 change: 1 addition & 0 deletions packages/json-render/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"./core": "./dist/core.mjs",
"./hub": "./dist/hub.mjs",
"./node": "./dist/node/index.mjs",
"./view": "./dist/view.mjs",
"./package.json": "./package.json"
},
"types": "./dist/index.d.mts",
Expand Down
22 changes: 18 additions & 4 deletions packages/json-render/src/hub.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { DockRenderer, DockRendererMountOptions } from '@devframes/hub/client'
import type { DevframeClientContext, DockRendererInstance, DockRendererMountOptions } from '@devframes/hub/client'
import type { DevframeDockEntryBase } from '@devframes/hub/types'
import type { DevframeRpcClient } from 'devframe/client'
import type { ConnectionMeta } from 'devframe/types'
import type { JsonRenderView } from './types'
import type { JsonRenderViewRef } from './view-ref'

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

/**
* The mount options a hub viewer hands a json-render dock renderer: the
* hub's `DockRendererMountOptions` narrowed to the `'json-render'` entry.
* hub's `DockRendererMountOptions` narrowed to the `'json-render'` entry
* and an optional context type. Existing renderers keep the full client context.
* This protocol package owns the renderer contract so every frontend
* (`@devframes/json-render-ui`, a community renderer, a host page's own)
* implements one shared shape instead of re-declaring it.
*/
export type JsonRenderDockMountOptions = DockRendererMountOptions<DevframeJsonRenderDockEntry>
export type JsonRenderDockMountOptions<Context = DevframeClientContext> = Omit<DockRendererMountOptions<DevframeJsonRenderDockEntry>, 'context'> & {
context: Context
}

/** Native RPC calls and shared state consumed by the reference JSON renderer. */
export interface JsonRenderRpcContext {
rpc: Pick<DevframeRpcClient, 'call' | 'sharedState'> & {
connectionMeta?: Pick<ConnectionMeta, 'backend'>
}
}

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

/**
* Build a `json-render` dock entry from a {@link JsonRenderView} and the dock
Expand Down
21 changes: 16 additions & 5 deletions packages/json-render/src/node/create-view.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DevframeNodeContext, DevframeScopedNodeContext } from 'devframe'
import type { RpcSharedStateHost } from 'devframe/types'
import type { SharedState, SharedStatePatch } from 'devframe/utils/shared-state'
import type { StandardSchemaV1 } from 'devframe/utils/simple-schema'
import type { DevframeJsonRenderSpec, JsonRenderStatePatch, JsonRenderView } from '../types'
Expand Down Expand Up @@ -34,17 +34,28 @@ export interface CreateJsonRenderViewOptions<SpecType extends DevframeJsonRender
title?: string
}

type AnyContext = DevframeNodeContext | DevframeScopedNodeContext<string>
/** Reuse one context per native shared-state instance for view discovery and duplicate detection. */
export interface JsonRenderViewContext {
rpc: { sharedState: RpcSharedStateHost }
}

/** Namespace and base shared state supplied by a scoped node context. */
export interface JsonRenderScopedViewContext {
base: JsonRenderViewContext
namespace: string
}

type AnyContext = JsonRenderViewContext | JsonRenderScopedViewContext

function isScoped(ctx: AnyContext): ctx is DevframeScopedNodeContext<string> {
function isScoped(ctx: AnyContext): ctx is JsonRenderScopedViewContext {
return 'base' in ctx && 'namespace' in ctx
}

// One registry of live view keys per base context, so a duplicate id within a
// scope is caught deterministically (not left to shared-state get() returning
// the pre-existing entry).
const registries = new WeakMap<object, Set<string>>()
function registryFor(ctx: DevframeNodeContext): Set<string> {
function registryFor(ctx: JsonRenderViewContext): Set<string> {
let set = registries.get(ctx)
if (!set) {
set = new Set()
Expand All @@ -57,7 +68,7 @@ function registryFor(ctx: DevframeNodeContext): Set<string> {
// `JSON_RENDER_INDEX_KEY`, so a frontend that does not know view ids ahead of
// time can discover every live view from a single subscription.
const indexStates = new WeakMap<object, SharedState<JsonRenderIndex>>()
function indexStateFor(ctx: DevframeNodeContext): SharedState<JsonRenderIndex> {
function indexStateFor(ctx: JsonRenderViewContext): SharedState<JsonRenderIndex> {
let state = indexStates.get(ctx)
if (!state) {
state = createSharedState<JsonRenderIndex>({ initialValue: {} })
Expand Down
18 changes: 18 additions & 0 deletions packages/json-render/test/create-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { DevframeJsonRenderSpec } from '../src/types'
import { mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createJsonRenderView as createPortableView } from '@devframes/json-render/view'
import { createHostContext } from 'devframe/node'
import { beforeEach, describe, expect, it } from 'vitest'
import { createJsonRenderView } from '../src/node/index'
Expand Down Expand Up @@ -57,6 +58,23 @@ describe('createJsonRenderView identity', () => {
})

describe('createJsonRenderView state', () => {
it('publishes and disposes through a shared-state-only context', async () => {
expect.assertions(5)
const context = { rpc: { sharedState: ctx.rpc.sharedState } }
const view = createPortableView(context, { id: 'portable', spec })
const state = await context.rpc.sharedState.get(view.ref.stateKey)
expect(state.value()).toEqual(spec)
const secondView = createPortableView(context, { id: 'second', spec })
const index = await context.rpc.sharedState.get(JSON_RENDER_INDEX_KEY)
expect(Object.keys(index.value())).toEqual([view.ref.stateKey, secondView.ref.stateKey])
expect(() => createPortableView(context, { id: 'portable', spec })).toThrow()
view.patchState([{ op: 'replace', path: '/count', value: 2 }])
expect(state.value().state).toEqual({ count: 2 })
view.dispose()
expect(context.rpc.sharedState.keys()).not.toContain(view.ref.stateKey)
secondView.dispose()
})

it('registers a shared state carrying the spec', async () => {
const view = createJsonRenderView(ctx, { id: 'v', spec })
expect(ctx.rpc.sharedState.keys()).toContain(view.ref.stateKey)
Expand Down
1 change: 1 addition & 0 deletions packages/json-render/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default defineConfig({
'index': 'src/index.ts',
'core': 'src/core.ts',
'hub': 'src/hub.ts',
'view': 'src/node/create-view.ts',
'node/index': 'src/node/index.ts',
},
outExtensions: () => ({ js: '.mjs', dts: '.d.mts' }),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Generated by tsnapi — public API snapshot of `@devframes/json-render-ui/renderer`
*/
// #region Default Export
declare const _default: JsonRenderDockRenderer<JsonRenderRpcContext>;
export default _default
// #endregion
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Generated by tsnapi — public API snapshot of `@devframes/json-render-ui/renderer`
*/
// #region Default Export
var _default
export default _default
// #endregion
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ export interface DevframeJsonRenderDockEntry extends DevframeDockEntryBase {
type: 'json-render';
view: JsonRenderViewRef;
}
export interface JsonRenderRpcContext {
rpc: Pick<DevframeRpcClient, 'call' | 'sharedState'> & {
connectionMeta?: Pick<ConnectionMeta, 'backend'>;
};
}
// #endregion

// #region Types
export type JsonRenderDockMountOptions = DockRendererMountOptions<DevframeJsonRenderDockEntry>;
export type JsonRenderDockRenderer = DockRenderer<DevframeJsonRenderDockEntry>;
export type JsonRenderDockMountOptions<Context = DevframeClientContext> = Omit<DockRendererMountOptions<DevframeJsonRenderDockEntry>, 'context'> & {
context: Context;
};
export type JsonRenderDockRenderer<Context = DevframeClientContext> = (_: JsonRenderDockMountOptions<Context>) => DockRendererInstance | Promise<DockRendererInstance>;
// #endregion

// #region Functions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
/**
* Generated by tsnapi — public API snapshot of `@devframes/json-render/node`
*/
// #region Interfaces
export interface CreateJsonRenderViewOptions<SpecType extends DevframeJsonRenderSpec = DevframeJsonRenderSpec> {
id: string;
spec: SpecType;
schema?: StandardSchemaV1 | false;
scope?: string;
title?: string;
}
// #endregion

// #region Functions
export declare function createJsonRenderView<SpecType extends DevframeJsonRenderSpec = DevframeJsonRenderSpec>(_: AnyContext, _: CreateJsonRenderViewOptions<SpecType>): JsonRenderView<SpecType>;
// #endregion

// #region Variables
export declare const jsonRenderDiagnostics: DevframeDiagnostics<typeof codes>;
// #endregion

// #region Referenced (internal)
type AnyContext = DevframeNodeContext | DevframeScopedNodeContext<string>;
// #region Other
export { createJsonRenderView }
export { CreateJsonRenderViewOptions }
// #endregion
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
/**
* Generated by tsnapi — public API snapshot of `@devframes/json-render/node`
*/
// #region Functions
export function createJsonRenderView(_, _) {}
// #endregion

// #region Variables
export var jsonRenderDiagnostics /* const */
// #region Other
export { createJsonRenderView }
export { diagnostics as jsonRenderDiagnostics }
// #endregion
Loading
Loading