|
| 1 | +import type { |
| 2 | + DevframeJsonRenderSpec, |
| 3 | + JsonRenderView, |
| 4 | + JsonRenderViewRef, |
| 5 | + UIElement, |
| 6 | +} from '@devframes/json-render' |
| 7 | +import type { CreateJsonRenderViewOptions } from '@devframes/json-render/node' |
| 8 | +import type { KitNodeContext } from '@vitejs/devtools-kit' |
| 9 | +import { createJsonRenderView } from '@devframes/json-render/node' |
| 10 | +import { nanoid } from '@vitejs/devtools-kit/utils/nanoid' |
| 11 | + |
| 12 | +// `json-render` is the opt-in `@devframes/json-render` integration, which |
| 13 | +// contributes the `'json-render'` variant to the hub's open dock union. This |
| 14 | +// type-only re-export also pulls in that module augmentation so |
| 15 | +// `docks.register({ type: 'json-render' })` resolves for plugin authors. |
| 16 | +export type { DevframeJsonRenderDockEntry as DevToolsViewJsonRender } from '@devframes/json-render/hub' |
| 17 | + |
| 18 | +/** A json-render spec — the declarative UI description a plugin authors. */ |
| 19 | +export type JsonRenderSpec<Element extends UIElement = UIElement> = DevframeJsonRenderSpec<Element> |
| 20 | + |
| 21 | +/** A single element within a spec's `elements` map. */ |
| 22 | +export type JsonRenderElement = UIElement |
| 23 | + |
| 24 | +export type { JsonRenderView, JsonRenderViewRef } |
| 25 | + |
| 26 | +/** |
| 27 | + * The handle returned by `ctx.createJsonRenderer()`. It wraps a devframe |
| 28 | + * {@link JsonRenderView} and exposes the kit's method names. |
| 29 | + * |
| 30 | + * Register a `json-render` dock entry with the handle as `ui` |
| 31 | + * (`docks.register({ type: 'json-render', ui, … })`); the client subscribes |
| 32 | + * through its `stateKey`. Drive the panel by calling `updateSpec` / |
| 33 | + * `updateState` on the handle. |
| 34 | + */ |
| 35 | +export interface JsonRenderer<SpecType extends JsonRenderSpec = JsonRenderSpec> { |
| 36 | + /** Replace the entire spec. */ |
| 37 | + updateSpec: (spec: SpecType) => void |
| 38 | + /** Shallow-merge values into the view's `state`. */ |
| 39 | + updateState: (state: Record<string, unknown>) => void |
| 40 | + /** Unregister the underlying view's shared state and listeners. */ |
| 41 | + dispose: () => void |
| 42 | + /** Shared-state key the client subscribes to for the live spec + state. */ |
| 43 | + readonly _stateKey: string |
| 44 | + /** The serializable reference to the underlying view. */ |
| 45 | + readonly view: JsonRenderViewRef<SpecType> |
| 46 | +} |
| 47 | + |
| 48 | +type CreateJsonRenderer = <SpecType extends JsonRenderSpec>( |
| 49 | + spec: SpecType, |
| 50 | + options?: Pick<CreateJsonRenderViewOptions<SpecType>, 'schema'>, |
| 51 | +) => JsonRenderer<SpecType> |
| 52 | + |
| 53 | +declare module '@vitejs/devtools-kit' { |
| 54 | + interface KitNodeContext { |
| 55 | + /** |
| 56 | + * Create a json-render handle for building declarative, server-driven |
| 57 | + * panels. Provided by Vite DevTools (`@vitejs/devtools`), which owns the |
| 58 | + * `@devframes/json-render` dependency so the kit stays light. |
| 59 | + */ |
| 60 | + createJsonRenderer: CreateJsonRenderer |
| 61 | + } |
| 62 | +} |
| 63 | + |
| 64 | +/** Identity helper that types a json-render spec literal with inference. */ |
| 65 | +export function defineJsonRenderSpec<SpecType extends JsonRenderSpec>(spec: SpecType): SpecType { |
| 66 | + return spec |
| 67 | +} |
| 68 | + |
| 69 | +export function attachJsonRenderer(context: KitNodeContext): void { |
| 70 | + const createJsonRenderer: CreateJsonRenderer = (spec, options = {}) => { |
| 71 | + const view = createJsonRenderView(context, { id: `kit-${nanoid()}`, spec, ...options }) |
| 72 | + |
| 73 | + // Methods are non-enumerable so the handle stays serializable when carried |
| 74 | + // on a dock entry's `ui` field — the docks shared-state projection walks |
| 75 | + // only enumerable own keys, so the closures never reach the wire. |
| 76 | + const handle = { |
| 77 | + _stateKey: view.ref.stateKey, |
| 78 | + view: view.ref, |
| 79 | + } as JsonRenderer<typeof spec> // methods are attached right below |
| 80 | + Object.defineProperties(handle, { |
| 81 | + updateSpec: { value: (next: typeof spec) => view.update(next) }, |
| 82 | + updateState: { |
| 83 | + value: (state: Record<string, unknown>) => { |
| 84 | + view.patchState( |
| 85 | + Object.entries(state).map(([key, value]) => ({ op: 'add' as const, path: `/${key}`, value })), |
| 86 | + ) |
| 87 | + }, |
| 88 | + }, |
| 89 | + dispose: { value: () => view.dispose() }, |
| 90 | + }) |
| 91 | + return handle |
| 92 | + } |
| 93 | + |
| 94 | + Object.defineProperty(context, 'createJsonRenderer', { value: createJsonRenderer, enumerable: true }) |
| 95 | +} |
0 commit comments