diff --git a/alias.ts b/alias.ts index a7a63e2..636e0e2 100644 --- a/alias.ts +++ b/alias.ts @@ -51,6 +51,7 @@ export const alias = { '@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/spa': r('json-render-ui/src/spa.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'), diff --git a/docs/examples/minimal-json-render.md b/docs/examples/minimal-json-render.md index 9e9f843..f75731f 100644 --- a/docs/examples/minimal-json-render.md +++ b/docs/examples/minimal-json-render.md @@ -5,10 +5,10 @@ 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. +`@json-render/core` spec once, and the prebuilt `@devframes/json-render-ui` SPA +renders it — the app ships no client build of its own. -Package: `minimal-json-render` · framework: **Vue + Vite** +Package: `minimal-json-render` · frontend: **prebuilt `@devframes/json-render-ui/spa`** ## What it shows @@ -20,9 +20,9 @@ Package: `minimal-json-render` · framework: **Vue + Vite** - **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`. +- **Out-of-box SPA** — `createJsonRenderDevframe` points `cli.distDir` at the + prebuilt `@devframes/json-render-ui/spa`, which discovers the view from the + view index and renders it — no client build in the example. - **Static output** — `cli:build` snapshots the spec + state as a read-only render; the action bridge reports actions as unavailable (no live RPC). @@ -30,7 +30,6 @@ Package: `minimal-json-render` · framework: **Vue + Vite** ```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 ``` diff --git a/docs/guide/json-render.md b/docs/guide/json-render.md index d064fae..d6348ec 100644 --- a/docs/guide/json-render.md +++ b/docs/guide/json-render.md @@ -89,8 +89,44 @@ validation. ## Rendering standalone -The app supplies the frontend lib and devframe serves its SPA. Connect, read the -view's shared state, and render it with `JsonRenderView`: +### Out-of-box SPA (no client build) + +`@devframes/json-render-ui/spa` ships a prebuilt renderer, so an app serves a +JSON-render UI without authoring or building any client. Wrap the definition +with `createJsonRenderDevframe` — it points `cli.distDir` at the shipped SPA +(`jsonRenderSpaDir`) and sets `spa.loader: 'none'`: + +```ts +import { createJsonRenderDevframe } from '@devframes/json-render-ui/spa' +import { createJsonRenderView } from '@devframes/json-render/node' + +export default createJsonRenderDevframe({ + id: 'my-app', + name: 'My App', + version, + packageName, + homepage, + description, + cli: { command: 'my-app', port: 9800 }, + setup(ctx) { + createJsonRenderView(ctx, { id: 'main', title: 'Dashboard', spec }) + }, +}) +``` + +The SPA discovers views from the **view index** (`JSON_RENDER_INDEX_KEY`), a +shared state the node factory maintains as views are created and disposed. A +single view renders full-bleed on its own; once more than one view is +registered, a top bar appears with the shared segmented switcher, labelling each +view with its `title` (defaulting to the view id). The +`@devframes/json-render-ui/spa` entry is node-safe — it exposes only the asset +path and the wiring helper, pulling in no Vue. + +### Custom frontend + +To render with your own client, supply the frontend lib and let devframe serve +its SPA. Connect, read the view's shared state, and render it with +`JsonRenderView`: ```ts import { JSON_RENDER_UPSTREAM_VERSION } from '@devframes/json-render' @@ -185,5 +221,11 @@ A third party replaces the whole registry — pass a custom `registry` to `@devframes/json-render-ui` is the reference implementation, not a hard dependency of the protocol; the hub acquires no Vue. +A frontend need not implement every component. When a spec references a +component the active registry lacks, the renderer isolates that element behind a +placeholder — showing the component type and a gist of its prop keys (`{ label, +onPress }`) — and logs a `console.warn`, while the rest of the view renders +normally. + See the [`minimal-json-render` example](/examples/minimal-json-render) for a runnable end-to-end app. diff --git a/examples/minimal-json-render/README.md b/examples/minimal-json-render/README.md index 64f9269..b6a099e 100644 --- a/examples/minimal-json-render/README.md +++ b/examples/minimal-json-render/README.md @@ -19,15 +19,15 @@ dashboard** that exercises **every base-catalog component** — `Stack`, `Card`, `Save` sends the bound form values as action params and the server writes the name into the `KeyValueTable`. Each is dispatched as an RPC call of the same name, with per-action loading and error surfacing. -- **`@devframes/json-render-ui`** — the SPA (`src/client`) subscribes to the - view's shared state and renders it with `JsonRenderView`. The app supplies the - frontend lib; devframe serves the SPA. +- **`@devframes/json-render-ui/spa`** — the prebuilt out-of-box SPA. This example + has **no client build**: `createJsonRenderDevframe(...)` points `cli.distDir` at + the shipped renderer, which discovers the view from the JSON-render view index + and renders it with `JsonRenderView`. ## Run ```sh pnpm --filter minimal-json-render dev # live dev server (http://localhost:9877/__minimal-json-render/) -pnpm --filter minimal-json-render build # build the SPA node bin.mjs build --out-dir dist/static # static snapshot (read-only; actions render disabled) ``` diff --git a/examples/minimal-json-render/package.json b/examples/minimal-json-render/package.json index c4b9add..8931d68 100644 --- a/examples/minimal-json-render/package.json +++ b/examples/minimal-json-render/package.json @@ -14,23 +14,17 @@ "minimal-json-render": "./bin.mjs" }, "scripts": { - "build": "vite build --config src/client/vite.config.ts", "dev": "node bin.mjs", "cli:build": "node bin.mjs build --out-dir dist/static", "typecheck": "tsc --noEmit" }, "dependencies": { - "@antfu/design": "catalog:frontend", "@devframes/json-render": "workspace:*", "@devframes/json-render-ui": "workspace:*", "cac": "catalog:deps", - "devframe": "workspace:*", - "vue": "catalog:frontend" + "devframe": "workspace:*" }, "devDependencies": { - "@vitejs/plugin-vue": "catalog:build", - "get-port-please": "catalog:deps", - "unocss": "catalog:frontend", - "vite": "catalog:build" + "get-port-please": "catalog:deps" } } diff --git a/examples/minimal-json-render/src/client/main.ts b/examples/minimal-json-render/src/client/main.ts deleted file mode 100644 index 705ae28..0000000 --- a/examples/minimal-json-render/src/client/main.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { DevframeJsonRenderSpec } from '@devframes/json-render' -import type { ActionBridgeRpc } from '@devframes/json-render-ui' -import { JSON_RENDER_UPSTREAM_VERSION } from '@devframes/json-render' -import { JsonRenderView } from '@devframes/json-render-ui' -import { connectDevframe } from 'devframe/client' -import { createApp, h, shallowRef } from 'vue' -import { STATE_KEY } from '../shared.ts' -import 'virtual:uno.css' -import '@antfu/design/styles.css' - -// Shared design tokens flip on the `.dark` class; mirror the OS preference. -const mq = window.matchMedia('(prefers-color-scheme: dark)') -function applyScheme(dark: boolean): void { - document.documentElement.classList.toggle('dark', dark) -} -applyScheme(mq.matches) -mq.addEventListener('change', e => applyScheme(e.matches)) - -async function main(): Promise { - const root = document.getElementById('app') - if (!root) - throw new Error('#app mount node missing') - - // The app supplies the compatible frontend lib (@devframes/json-render-ui); - // devframe serves this SPA. Connect, subscribe to the view's shared state, - // and render — new server snapshots/patches re-render the view live. - const rpc = await connectDevframe() - const interactive = rpc.connectionMeta.backend !== 'static' - const state = await rpc.sharedState.get(STATE_KEY, { - initialValue: null as unknown as DevframeJsonRenderSpec, - }) - - const specRef = shallowRef(state.value() as DevframeJsonRenderSpec | null) - state.on('updated', () => { - specRef.value = state.value() as DevframeJsonRenderSpec | null - }) - - createApp({ - render: () => h('div', { class: 'min-h-screen bg-base color-base font-sans p6' }, [ - h(JsonRenderView, { - spec: specRef.value, - rpc: rpc as unknown as ActionBridgeRpc, - viewId: STATE_KEY, - upstreamVersion: JSON_RENDER_UPSTREAM_VERSION, - interactive, - }), - ]), - }).mount(root) -} - -main().catch((error) => { - console.error(error) - document.body.textContent = `Failed to start: ${(error as Error).message}` -}) diff --git a/examples/minimal-json-render/src/client/vite.config.ts b/examples/minimal-json-render/src/client/vite.config.ts deleted file mode 100644 index 56bbf6f..0000000 --- a/examples/minimal-json-render/src/client/vite.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fileURLToPath } from 'node:url' -import vue from '@vitejs/plugin-vue' -import UnoCSS from 'unocss/vite' -import { defineConfig } from 'vite' -import { alias } from '../../../../alias' - -export default defineConfig({ - base: './', - root: fileURLToPath(new URL('.', import.meta.url)), - resolve: { alias }, - plugins: [UnoCSS(), vue()], - // `@antfu/design` (pulled in by @devframes/json-render-ui) ships raw `.vue`; - // let @vitejs/plugin-vue compile its SFCs instead of esbuild pre-bundling. - optimizeDeps: { exclude: ['@antfu/design', '@devframes/json-render-ui'] }, - build: { - outDir: fileURLToPath(new URL('../../dist/client', import.meta.url)), - emptyOutDir: true, - }, -}) diff --git a/examples/minimal-json-render/src/dashboard.ts b/examples/minimal-json-render/src/dashboard.ts index ff134d8..e428524 100644 --- a/examples/minimal-json-render/src/dashboard.ts +++ b/examples/minimal-json-render/src/dashboard.ts @@ -127,7 +127,7 @@ export const dashboardSpec: DevframeJsonRenderSpec = { * view handle so callers can build a dock ref from it. */ export function createDashboardView(ctx: DevframeNodeContext): JsonRenderView { - const view = createJsonRenderView(ctx, { id: VIEW_ID, spec: dashboardSpec }) + const view = createJsonRenderView(ctx, { id: VIEW_ID, title: 'Dashboard', spec: dashboardSpec }) // The action bridge dispatches a spec action as an RPC call of the same // name. `Refresh` re-samples the metrics. diff --git a/examples/minimal-json-render/src/devframe.ts b/examples/minimal-json-render/src/devframe.ts index 81af62d..3dfd09c 100644 --- a/examples/minimal-json-render/src/devframe.ts +++ b/examples/minimal-json-render/src/devframe.ts @@ -1,12 +1,11 @@ -import { fileURLToPath } from 'node:url' -import { defineDevframe } from 'devframe/types' +import { createJsonRenderDevframe } from '@devframes/json-render-ui/spa' import pkg from '../package.json' with { type: 'json' } import { createDashboardView } from './dashboard.ts' -const BASE_PATH = '/__minimal-json-render/' -const distDir = fileURLToPath(new URL('../dist/client', import.meta.url)) - -export default defineDevframe({ +// `createJsonRenderDevframe` presets `spa.loader: 'none'` and points +// `cli.distDir` at the prebuilt `@devframes/json-render-ui` SPA, so this +// example serves the out-of-box renderer with no client build of its own. +export default createJsonRenderDevframe({ id: 'minimal-json-render', name: 'Minimal JSON-Render', version: pkg.version, @@ -14,16 +13,14 @@ export default defineDevframe({ homepage: pkg.homepage, description: pkg.description, icon: 'ph:layout-duotone', - basePath: BASE_PATH, + basePath: '/__minimal-json-render/', cli: { command: 'minimal-json-render', port: 9877, - distDir, // Single-user localhost demo — skip the trust handshake so the served SPA // can call the action RPCs without an OTP round-trip. auth: false, }, - spa: { loader: 'none' }, setup(ctx) { createDashboardView(ctx) }, diff --git a/examples/minimal-json-render/src/shared.ts b/examples/minimal-json-render/src/shared.ts index b3a571e..0fa92a3 100644 --- a/examples/minimal-json-render/src/shared.ts +++ b/examples/minimal-json-render/src/shared.ts @@ -1,16 +1,9 @@ -// Shared constants used by both the node devframe and the browser SPA. +// Shared constants for the node devframe. The prebuilt SPA discovers the view +// from the JSON-render view index, so no client-side view id/key is needed. /** Author-supplied, stable view id. */ export const VIEW_ID = 'dashboard' -/** - * The view's shared-state key. `createJsonRenderView` derives it as - * `devframe:json-render::`; the base context's scope is `global`. - * The SPA subscribes to this key directly. (A view's serializable - * `JsonRenderViewRef` carries the same `stateKey`.) - */ -export const STATE_KEY = `devframe:json-render:global:${VIEW_ID}` - // Spec action names dispatched by the bridge — each is an RPC method the server // registers. export const REFRESH_ACTION = 'minimal-json-render:refresh' diff --git a/packages/json-render-ui/package.json b/packages/json-render-ui/package.json index d093041..eddb524 100644 --- a/packages/json-render-ui/package.json +++ b/packages/json-render-ui/package.json @@ -22,6 +22,7 @@ "exports": { ".": "./dist/index.mjs", "./components": "./dist/components/index.mjs", + "./spa": "./dist/spa.mjs", "./package.json": "./package.json" }, "types": "./dist/index.d.mts", @@ -29,7 +30,7 @@ "dist" ], "scripts": { - "build": "tsdown", + "build": "tsdown && vite build --config src/spa/vite.config.ts", "watch": "tsdown --watch", "typecheck": "tsc --noEmit", "storybook": "storybook dev -p 6014 --host 0.0.0.0", diff --git a/packages/json-render-ui/src/JsonRender.stories.ts b/packages/json-render-ui/src/JsonRender.stories.ts index 6d57851..9f13fbd 100644 --- a/packages/json-render-ui/src/JsonRender.stories.ts +++ b/packages/json-render-ui/src/JsonRender.stories.ts @@ -1,6 +1,7 @@ import type { Spec } from '@devframes/json-render' import type { Meta, StoryObj } from '@storybook/vue3-vite' import { h } from 'vue' +import { baseRegistry } from './registry' import { JsonRenderView } from './renderer' // A no-op RPC — stories don't dispatch real actions. @@ -74,3 +75,31 @@ export const InvalidElement: StoryObj = story({ bad: { type: 'Badge', props: { variant: 'purple' }, children: [] }, }, }) + +// An element whose `type` is absent from the registry renders behind the +// unsupported placeholder (type + prop-keys gist), while its siblings render. +export const UnsupportedComponent: StoryObj = story({ + root: 'root', + elements: { + root: { type: 'Stack', props: { gap: 8 }, children: ['ok', 'chart', 'more'] }, + ok: { type: 'Text', props: { text: 'This view uses a component the frontend does not ship.' }, children: [] }, + chart: { type: 'Fancy3DChart', props: { data: [1, 2, 3], title: 'Revenue', animate: true }, children: [] }, + more: { type: 'Badge', props: { text: 'still renders', variant: 'success' }, children: [] }, + }, +}) + +// A frontend that supports only a subset of the catalog: rendering with a +// registry missing `DataTable` placeholders that element, everything else +// renders normally. +const { DataTable: _omitted, ...subsetRegistry } = baseRegistry as Record +export const SubsetRegistry: StoryObj = story( + { + root: 'root', + elements: { + root: { type: 'Stack', props: { gap: 8 }, children: ['title', 'table'] }, + title: { type: 'Text', props: { text: 'Registry without DataTable', variant: 'heading' }, children: [] }, + table: { type: 'DataTable', props: { rows: [{ id: 1, name: 'a' }], height: 120 }, children: [] }, + }, + }, + { registry: subsetRegistry }, +) diff --git a/packages/json-render-ui/src/components/_unsupported.ts b/packages/json-render-ui/src/components/_unsupported.ts new file mode 100644 index 0000000..8aebdc7 --- /dev/null +++ b/packages/json-render-ui/src/components/_unsupported.ts @@ -0,0 +1,25 @@ +import type { JrComponent } from './_shared' +import { h } from 'vue' + +/** Format a list of prop keys as a compact gist, e.g. `{ foo, bar }`. */ +export function formatPropKeys(keys: readonly string[]): string { + return keys.length ? `{ ${keys.join(', ')} }` : '{}' +} + +/** + * Internal placeholder rendered in place of an element whose `type` is not in + * the active registry — i.e. a component this frontend does not support. The + * rest of the view still renders. Not part of the public catalog. + * + * Shows three lines using the design system's semantic tokens: a label, the + * unsupported component type, and a gist of the element's prop keys. + */ +export const JsonRenderUnsupported: JrComponent<{ type?: string, keys?: string[] }> = ({ props }) => + h('div', { + class: 'flex flex-wrap gap-x-2 gap-y-1 gap-0.5 rounded text-center border-2 border-dashed border-orange:50 bg-orange:5 text-xs px2 py1', + role: 'note', + }, [ + h('div', { class: 'text-orange' }, 'Unsupported component:'), + h('div', { class: 'font-semibold text-orange font-mono' }, props.type ?? 'unknown'), + h('div', { class: 'color-faint font-mono' }, formatPropKeys(props.keys ?? [])), + ]) diff --git a/packages/json-render-ui/src/index.ts b/packages/json-render-ui/src/index.ts index a1d4533..de27ed1 100644 --- a/packages/json-render-ui/src/index.ts +++ b/packages/json-render-ui/src/index.ts @@ -9,6 +9,6 @@ export type { JsonRenderDockRendererOptions, } from './dock-renderer' -export { baseRegistry, ERROR_COMPONENT_TYPE } from './registry' +export { baseRegistry, ERROR_COMPONENT_TYPE, UNSUPPORTED_COMPONENT_TYPE } from './registry' export { createRenderer, JsonRenderView, sanitizeSpec } from './renderer' export type { CreateRendererOptions } from './renderer' diff --git a/packages/json-render-ui/src/registry.ts b/packages/json-render-ui/src/registry.ts index 3d05fec..c3ea374 100644 --- a/packages/json-render-ui/src/registry.ts +++ b/packages/json-render-ui/src/registry.ts @@ -18,10 +18,14 @@ import { Tree, } from './components' import { JsonRenderError } from './components/_error' +import { JsonRenderUnsupported } from './components/_unsupported' /** Reserved component type used to isolate an element that fails validation. */ export const ERROR_COMPONENT_TYPE = '__jsonRenderError' +/** Reserved component type used to placeholder an element the registry lacks. */ +export const UNSUPPORTED_COMPONENT_TYPE = '__jsonRenderUnsupported' + /** * The base Vue registry: the fourteen catalog-v1 components ported onto * `@antfu/design` semantic tokens, wrapped as Vue components via upstream @@ -45,5 +49,6 @@ export const baseRegistry: ComponentRegistry = defineRegistry(baseCatalog as any Progress, Tree, [ERROR_COMPONENT_TYPE]: JsonRenderError, + [UNSUPPORTED_COMPONENT_TYPE]: JsonRenderUnsupported, } as any, }).registry diff --git a/packages/json-render-ui/src/renderer.ts b/packages/json-render-ui/src/renderer.ts index d0c31d8..b228b22 100644 --- a/packages/json-render-ui/src/renderer.ts +++ b/packages/json-render-ui/src/renderer.ts @@ -6,7 +6,7 @@ import { basePropSchemas, JSON_RENDER_UPSTREAM_VERSION } from '@devframes/json-r import { JSONUIProvider, Renderer } from '@json-render/vue' import { computed, defineComponent, h, watch } from 'vue' import { createActionBridge } from './action-bridge' -import { baseRegistry, ERROR_COMPONENT_TYPE } from './registry' +import { baseRegistry, ERROR_COMPONENT_TYPE, UNSUPPORTED_COMPONENT_TYPE } from './registry' // Upstream ships these as heavily-typed `DefineComponent`s; render them through // a loose alias so `h()` doesn't demand their full public-instance surface. @@ -16,15 +16,32 @@ const ProviderC = JSONUIProvider as any const RendererC = Renderer as any /** - * Render-time prop validation: parse every element's props against the base - * catalog schema and swap any element that fails for the reserved error - * component, so one bad element is isolated instead of breaking the view. - * Returns the effective spec (unchanged when everything validates). + * Render-time isolation, parameterized by the active registry: + * + * - An element whose `type` is **not in the registry** (a component this + * frontend does not support) is swapped for the reserved *unsupported* + * placeholder, which shows the type and a gist of its prop keys. The rest of + * the view still renders. + * - An element whose props fail the base-catalog schema is swapped for the + * reserved *error* placeholder, so one bad element is isolated. + * + * Both cases emit a `console.warn` (browser-only failures keep `console.*` per + * the plan; no coded diagnostic). Returns the effective spec (unchanged when + * every element renders cleanly). */ -export function sanitizeSpec(spec: Spec): Spec { +export function sanitizeSpec(spec: Spec, registry: ComponentRegistry = baseRegistry): Spec { let changed = false const elements: Spec['elements'] = {} for (const [key, element] of Object.entries(spec.elements ?? {})) { + // Unsupported component: the active registry has no renderer for it. + if (!(element.type in registry)) { + changed = true + const keys = Object.keys(element.props ?? {}) + // Browser-only render failure — keep console.* per the plan. + console.warn(`[@devframes/json-render-ui] unsupported component "${element.type}" on element "${key}": not in the active registry. Rendering a placeholder.`) + elements[key] = { ...element, type: UNSUPPORTED_COMPONENT_TYPE, props: { type: element.type, keys } } + continue + } const schema = basePropSchemas[element.type as keyof typeof basePropSchemas] if (schema) { const result = schema.safeParse(element.props ?? {}) @@ -83,7 +100,7 @@ export const JsonRenderView = defineComponent({ // Reset the provider (reseed state) only on identity / version change. const resetKey = computed(() => `${props.viewId}::${props.upstreamVersion ?? JSON_RENDER_UPSTREAM_VERSION}`) - const effectiveSpec = computed(() => (props.spec ? sanitizeSpec(props.spec) : null)) + const effectiveSpec = computed(() => (props.spec ? sanitizeSpec(props.spec, props.registry) : null)) return () => { if (props.loading) diff --git a/packages/json-render-ui/src/spa.ts b/packages/json-render-ui/src/spa.ts new file mode 100644 index 0000000..9a554e8 --- /dev/null +++ b/packages/json-render-ui/src/spa.ts @@ -0,0 +1,39 @@ +import type { DevframeDefinition } from 'devframe/types' +import { fileURLToPath } from 'node:url' + +/** + * Absolute path to the prebuilt standalone SPA assets shipped by this package + * (`dist/spa`). Point a devframe's `cli.distDir` at it to serve the out-of-box + * renderer with no client build: + * + * ```ts + * import { jsonRenderSpaDir } from '@devframes/json-render-ui/spa' + * defineDevframe({ cli: { command: 'my-app', distDir: jsonRenderSpaDir }, spa: { loader: 'none' } }) + * ``` + * + * This entry is node-safe: it imports no Vue and no `@antfu/design`, so a build + * tool can read the path without pulling the browser renderer graph. + */ +export const jsonRenderSpaDir: string = fileURLToPath(new URL('./spa/', import.meta.url)) + +/** + * Wrap a devframe definition so it serves the prebuilt {@link jsonRenderSpaDir + * standalone SPA}. Presets `spa.loader: 'none'` and defaults `cli.distDir` to + * the SPA assets (an explicit `cli.distDir` still wins). The author supplies + * everything else (id, name, `setup`, port, …) as usual. + * + * ```ts + * export default createJsonRenderDevframe({ + * id: 'my-app', name: 'My App', version, packageName, homepage, description, + * cli: { command: 'my-app', port: 9800, auth: false }, + * setup(ctx) { createJsonRenderView(ctx, { id: 'main', spec }) }, + * }) + * ``` + */ +export function createJsonRenderDevframe(definition: DevframeDefinition): DevframeDefinition { + return { + ...definition, + spa: { loader: 'none', ...definition.spa }, + cli: { ...definition.cli, distDir: definition.cli?.distDir ?? jsonRenderSpaDir }, + } +} diff --git a/examples/minimal-json-render/src/client/index.html b/packages/json-render-ui/src/spa/index.html similarity index 87% rename from examples/minimal-json-render/src/client/index.html rename to packages/json-render-ui/src/spa/index.html index 1513189..79308c2 100644 --- a/examples/minimal-json-render/src/client/index.html +++ b/packages/json-render-ui/src/spa/index.html @@ -4,7 +4,7 @@ - Minimal JSON-Render + JSON Render
diff --git a/packages/json-render-ui/src/spa/main.ts b/packages/json-render-ui/src/spa/main.ts new file mode 100644 index 0000000..adb3a5d --- /dev/null +++ b/packages/json-render-ui/src/spa/main.ts @@ -0,0 +1,142 @@ +import type { DevframeJsonRenderSpec, JsonRenderIndex, JsonRenderIndexEntry } from '@devframes/json-render' +import type { ActionBridgeRpc } from '../action-bridge' +import { JSON_RENDER_INDEX_KEY } from '@devframes/json-render' +import { connectDevframe } from 'devframe/client' +import { computed, createApp, defineComponent, h, ref, shallowReactive, shallowRef, watch } from 'vue' +import { JsonRenderView } from '../renderer' +import 'virtual:uno.css' +import '@antfu/design/styles.css' + +// Shared design tokens flip on the `.dark` class; mirror the OS preference. +const mq = window.matchMedia('(prefers-color-scheme: dark)') +function applyScheme(dark: boolean): void { + document.documentElement.classList.toggle('dark', dark) +} +applyScheme(mq.matches) +mq.addEventListener('change', e => applyScheme(e.matches)) + +const surface = 'flex min-h-screen items-center justify-center color-faint text-sm' + +async function main(): Promise { + const root = document.getElementById('app') + if (!root) + throw new Error('#app mount node missing') + + const rpc = await connectDevframe() + const interactive = rpc.connectionMeta.backend !== 'static' + + // Discover every live view from the single view-index shared state, then + // subscribe to each view's own state. The author never wires a view id into + // the client — publishing a view is enough for it to appear here. + const indexState = await rpc.sharedState.get(JSON_RENDER_INDEX_KEY, { initialValue: {} }) + // Seed the current server value before mounting so an empty tree isn't + // mistaken for "no views" during the first round-trip. + if (interactive) { + try { + const value = await rpc.call('devframe:rpc:server-state:get', JSON_RENDER_INDEX_KEY) + if (value && typeof value === 'object') + indexState.mutate(() => value as JsonRenderIndex) + } + catch { + // Non-fatal: fall back to live 'updated' events below. + } + } + + const indexRef = shallowRef(indexState.value() as JsonRenderIndex) + indexState.on('updated', () => { + indexRef.value = indexState.value() as JsonRenderIndex + }) + + const App = defineComponent({ + name: 'JsonRenderSpa', + setup() { + const entries = computed(() => + Object.values(indexRef.value).sort((a, b) => a.title.localeCompare(b.title))) + + // Per-view specs, keyed by stateKey. `undefined` = subscribing (loading), + // `null` = subscribed with no spec yet. + const specs = shallowReactive>({}) + const subscribed = new Set() + const active = ref(null) + + watch(entries, (list) => { + for (const entry of list) { + if (subscribed.has(entry.stateKey)) + continue + subscribed.add(entry.stateKey) + specs[entry.stateKey] = undefined + void rpc.sharedState + .get(entry.stateKey, { initialValue: null as unknown as DevframeJsonRenderSpec }) + .then((state) => { + specs[entry.stateKey] = state.value() as DevframeJsonRenderSpec | null + state.on('updated', () => { + specs[entry.stateKey] = state.value() as DevframeJsonRenderSpec | null + }) + }) + } + if (!active.value || !list.some(e => e.stateKey === active.value)) + active.value = list[0]?.stateKey ?? null + }, { immediate: true }) + + function renderTabs(list: JsonRenderIndexEntry[]) { + return h('div', { class: 'inline-flex gap-1 rounded bg-secondary p1 text-sm' }, list.map(entry => + h('button', { + 'key': entry.stateKey, + 'type': 'button', + 'data-state': active.value === entry.stateKey ? 'active' : 'inactive', + 'class': [ + 'rounded px3 py1 transition-colors', + active.value === entry.stateKey + ? 'bg-base color-base shadow-sm' + : 'color-muted hover:color-base', + ], + 'onClick': () => { active.value = entry.stateKey }, + }, entry.title))) + } + + return () => { + const list = entries.value + if (!list.length) + return h('div', { class: surface }, 'No JSON-render views registered.') + + const activeEntry = list.find(e => e.stateKey === active.value) ?? list[0] + const spec = specs[activeEntry.stateKey] + + // A single view renders on its own — no nav. The top bar (brand + + // segmented view switcher) only appears once there's more than one. + const multiple = list.length > 1 + + return h('div', { class: 'min-h-screen bg-base color-base font-sans' }, [ + multiple + ? h('div', { + class: 'flex items-center gap-3 border-b border-base px5 h-nav', + }, [ + h('div', { class: 'flex items-center gap-2 font-medium' }, [ + h('div', { class: 'i-ph:layout-duotone color-primary text-lg' }), + h('span', 'JSON Render'), + ]), + h('div', { class: 'ml-auto' }, [renderTabs(list)]), + ]) + : null, + h('div', { class: 'p6' }, [ + h(JsonRenderView, { + spec: spec ?? null, + rpc: rpc as unknown as ActionBridgeRpc, + viewId: activeEntry.stateKey, + upstreamVersion: activeEntry.upstreamVersion, + interactive, + loading: spec === undefined, + }), + ]), + ]) + } + }, + }) + + createApp(App).mount(root) +} + +main().catch((error) => { + console.error(error) + document.body.textContent = `Failed to start: ${(error as Error).message}` +}) diff --git a/examples/minimal-json-render/src/client/shims.d.ts b/packages/json-render-ui/src/spa/shims.d.ts similarity index 62% rename from examples/minimal-json-render/src/client/shims.d.ts rename to packages/json-render-ui/src/spa/shims.d.ts index 02e6de6..4e1a4d7 100644 --- a/examples/minimal-json-render/src/client/shims.d.ts +++ b/packages/json-render-ui/src/spa/shims.d.ts @@ -2,8 +2,8 @@ declare module '*.css' {} declare module 'virtual:uno.css' {} -// @devframes/json-render-ui resolves to source in the workspace, and wraps -// `@antfu/design` `.vue` components — declare the module so `tsc` resolves them. +// `@antfu/design` ships raw `.vue` components — declare the module so `tsc` +// resolves them when the SPA renders through the shared components. declare module '*.vue' { import type { DefineComponent } from 'vue' diff --git a/examples/minimal-json-render/uno.config.ts b/packages/json-render-ui/src/spa/uno.config.ts similarity index 71% rename from examples/minimal-json-render/uno.config.ts rename to packages/json-render-ui/src/spa/uno.config.ts index 5709e9f..d424d6c 100644 --- a/examples/minimal-json-render/uno.config.ts +++ b/packages/json-render-ui/src/spa/uno.config.ts @@ -8,10 +8,10 @@ import { transformerVariantGroup, } from 'unocss' -// The SPA renders `@devframes/json-render-ui`, whose components author class -// strings in `.ts` render functions — so `.ts` is opted into extraction. Same -// `@antfu/design` stack (sage-green preset, Wind4, Phosphor, DM Sans/Mono) as -// every other devframe surface. +// The prebuilt SPA renders `@devframes/json-render-ui`, whose components author +// class strings in `.ts` render functions — so `.ts` is opted into extraction. +// Same `@antfu/design` stack (sage-green preset, Wind4, Phosphor, DM Sans/Mono) +// as every other devframe surface. export default defineConfig({ presets: [ presetAnthonyDesign({ primary: '#3a6a45' }), @@ -21,8 +21,8 @@ export default defineConfig({ ], transformers: [transformerDirectives(), transformerVariantGroup()], preflights: [{ getCSS: () => '*,::before,::after{border-color:#8882}' }], - // `Badge` in @devframes/json-render-ui picks a `badge-color-` at runtime - // from a fixed set, so those classes need safelisting. + // `Badge` picks a `badge-color-` at runtime from a fixed set, so those + // classes need safelisting. safelist: ['badge-color-green', 'badge-color-amber', 'badge-color-red', 'badge-color-blue'], shortcuts: { 'z-nav': 'z-[30]', diff --git a/packages/json-render-ui/src/spa/vite.config.ts b/packages/json-render-ui/src/spa/vite.config.ts new file mode 100644 index 0000000..dcd6b75 --- /dev/null +++ b/packages/json-render-ui/src/spa/vite.config.ts @@ -0,0 +1,24 @@ +import { fileURLToPath } from 'node:url' +import vue from '@vitejs/plugin-vue' +import UnoCSS from 'unocss/vite' +import { defineConfig } from 'vite' +import { alias } from '../../../../alias' + +// The out-of-box standalone SPA. `base: './'` keeps every asset URL relative so +// the bundle is mount-path portable — it discovers its runtime base from +// `document.baseURI` and connects via `connectDevframe()`. devframe's dev/build +// adapters serve this directory verbatim (no HTML rewriting) when an app wires +// `cli.distDir = jsonRenderSpaDir`. +export default defineConfig({ + base: './', + root: fileURLToPath(new URL('.', import.meta.url)), + resolve: { alias }, + plugins: [UnoCSS(), vue()], + // `@antfu/design` (pulled in by the renderer) ships raw `.vue`; let + // `@vitejs/plugin-vue` compile its SFCs instead of esbuild pre-bundling. + optimizeDeps: { exclude: ['@antfu/design', '@devframes/json-render-ui'] }, + build: { + outDir: fileURLToPath(new URL('../../dist/spa', import.meta.url)), + emptyOutDir: true, + }, +}) diff --git a/packages/json-render-ui/test/renderer.test.ts b/packages/json-render-ui/test/renderer.test.ts index de1428e..c15b900 100644 --- a/packages/json-render-ui/test/renderer.test.ts +++ b/packages/json-render-ui/test/renderer.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest' -import { ERROR_COMPONENT_TYPE } from '../src/registry' +import { describe, expect, it, vi } from 'vitest' +import { ERROR_COMPONENT_TYPE, UNSUPPORTED_COMPONENT_TYPE } from '../src/registry' import { sanitizeSpec } from '../src/renderer' describe('sanitizeSpec (render-time validation)', () => { @@ -24,4 +24,39 @@ describe('sanitizeSpec (render-time validation)', () => { expect(result.elements.a.type).toBe(ERROR_COMPONENT_TYPE) expect(result.elements.b.type).toBe('Text') }) + + it('placeholders an unsupported component and keeps the rest of the view', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const spec = { + root: 'root', + elements: { + root: { type: 'Stack', props: {}, children: ['a', 'b'] }, + a: { type: 'Fancy3DChart', props: { data: [1, 2], title: 'x' }, children: [] }, + b: { type: 'Text', props: { text: 'ok' }, children: [] }, + }, + } + const result = sanitizeSpec(spec) + expect(result).not.toBe(spec) + expect(result.elements.a.type).toBe(UNSUPPORTED_COMPONENT_TYPE) + // Carries the original type + a gist of the element's prop keys. + expect(result.elements.a.props).toEqual({ type: 'Fancy3DChart', keys: ['data', 'title'] }) + expect(result.elements.b.type).toBe('Text') + expect(warn).toHaveBeenCalledOnce() + warn.mockRestore() + }) + + it('respects a subset registry — a base component absent from it is unsupported', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const spec = { + root: 'a', + elements: { a: { type: 'Button', props: { label: 'Go' }, children: [] } }, + } + // A registry that supports neither Button nor the reserved placeholders + // except the unsupported one, so Button resolves to the placeholder. + const registry = { [UNSUPPORTED_COMPONENT_TYPE]: () => null } as any + const result = sanitizeSpec(spec, registry) + expect(result.elements.a.type).toBe(UNSUPPORTED_COMPONENT_TYPE) + expect(result.elements.a.props).toEqual({ type: 'Button', keys: ['label'] }) + warn.mockRestore() + }) }) diff --git a/packages/json-render-ui/tsdown.config.ts b/packages/json-render-ui/tsdown.config.ts index 0883b81..8ffcd8f 100644 --- a/packages/json-render-ui/tsdown.config.ts +++ b/packages/json-render-ui/tsdown.config.ts @@ -7,6 +7,9 @@ export default defineConfig({ entry: { 'index': 'src/index.ts', 'components/index': 'src/components/index.ts', + // Node-safe entry: exposes the prebuilt SPA path + a devframe wiring + // helper. Imports no Vue / `@antfu/design`, only `node:url`. + 'spa': 'src/spa.ts', }, outExtensions: () => ({ js: '.mjs', dts: '.d.mts' }), clean: true, diff --git a/packages/json-render/src/index.ts b/packages/json-render/src/index.ts index 287c098..f269774 100644 --- a/packages/json-render/src/index.ts +++ b/packages/json-render/src/index.ts @@ -33,7 +33,10 @@ export { // ── Devframes-facing type names ────────────────────────────────────────── export type { DevframeJsonRenderSpec, JsonRenderView } from './types' +// ── View index (frontend view discovery) ───────────────────────────────── +export { JSON_RENDER_INDEX_KEY } from './view-index' + +export type { JsonRenderIndex, JsonRenderIndexEntry } from './view-index' // ── Serializable view reference ────────────────────────────────────────── export { JSON_RENDER_UPSTREAM_VERSION } from './view-ref' - export type { JsonRenderViewRef } from './view-ref' diff --git a/packages/json-render/src/node/create-view.ts b/packages/json-render/src/node/create-view.ts index 6d83315..bf13899 100644 --- a/packages/json-render/src/node/create-view.ts +++ b/packages/json-render/src/node/create-view.ts @@ -1,8 +1,10 @@ import type { DevframeNodeContext, DevframeScopedNodeContext } from 'devframe/types' import type { SharedState, SharedStatePatch } from 'devframe/utils/shared-state' import type { DevframeJsonRenderSpec, JsonRenderStatePatch, JsonRenderView } from '../types' +import type { JsonRenderIndex } from '../view-index' import { createSharedState } from 'devframe/utils/shared-state' import { basePropSchemas } from '../prop-schemas' +import { JSON_RENDER_INDEX_KEY } from '../view-index' import { JSON_RENDER_UPSTREAM_VERSION } from '../view-ref' import { diagnostics } from './diagnostics' @@ -22,6 +24,12 @@ export interface CreateJsonRenderViewOptions { * `'global'`. */ scope?: string + /** + * Human-facing label published in the view index and used by a multi-view + * frontend (e.g. the standalone SPA's view switcher) to name this view. + * Defaults to {@link CreateJsonRenderViewOptions.id}. + */ + title?: string } type AnyContext = DevframeNodeContext | DevframeScopedNodeContext @@ -43,6 +51,20 @@ function registryFor(ctx: DevframeNodeContext): Set { return set } +// One shared view-index state per base context, published at +// `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>() +function indexStateFor(ctx: DevframeNodeContext): SharedState { + let state = indexStates.get(ctx) + if (!state) { + state = createSharedState({ initialValue: {} }) + indexStates.set(ctx, state) + void ctx.rpc.sharedState.get(JSON_RENDER_INDEX_KEY, { sharedState: state as SharedState }) + } + return state +} + /** Parse an RFC 6901 JSON Pointer into path segments. */ function parsePointer(pointer: string): string[] { if (pointer === '' || pointer === '/') @@ -106,6 +128,7 @@ export function createJsonRenderView( const baseCtx = scoped ? ctx.base : ctx const scope = options.scope ?? (scoped ? ctx.namespace : 'global') const { id } = options + const title = options.title ?? id const stateKey = `devframe:json-render:${scope}:${id}` const registry = registryFor(baseCtx) @@ -127,6 +150,12 @@ export function createJsonRenderView( // subsequent `update`/`patchState` calls broadcast correctly. void baseCtx.rpc.sharedState.get(stateKey, { sharedState: state as SharedState }) + // Publish the view into the shared index so a frontend can discover it. + const index = indexStateFor(baseCtx) + index.mutate((idx) => { + idx[stateKey] = { id, scope, stateKey, title, upstreamVersion: JSON_RENDER_UPSTREAM_VERSION } + }) + let disposed = false function assertLive(): void { if (disposed) @@ -135,6 +164,7 @@ export function createJsonRenderView( return { id, + title, ref: { stateKey, upstreamVersion: JSON_RENDER_UPSTREAM_VERSION }, value: () => state.value() as DevframeJsonRenderSpec, update(spec) { @@ -158,6 +188,9 @@ export function createJsonRenderView( return disposed = true registry.delete(stateKey) + index.mutate((idx) => { + delete idx[stateKey] + }) baseCtx.rpc.sharedState.delete(stateKey) }, } diff --git a/packages/json-render/src/types.ts b/packages/json-render/src/types.ts index d182c20..9eec463 100644 --- a/packages/json-render/src/types.ts +++ b/packages/json-render/src/types.ts @@ -30,6 +30,8 @@ export interface JsonRenderStatePatch { export interface JsonRenderView { /** Author-supplied stable id, unique within the view's scope. */ readonly id: string + /** Human-facing label published in the view index (defaults to `id`). */ + readonly title: string /** The serializable reference clients subscribe through. */ readonly ref: JsonRenderViewRef /** Replace the entire spec (a structural change replaces the whole spec). */ diff --git a/packages/json-render/src/view-index.ts b/packages/json-render/src/view-index.ts new file mode 100644 index 0000000..4d9b8b8 --- /dev/null +++ b/packages/json-render/src/view-index.ts @@ -0,0 +1,32 @@ +/** + * Well-known shared-state key carrying the **view index**: a map of every + * live JSON-render view registered on a context, keyed by its `stateKey`. + * + * A frontend that does not know view ids ahead of time (e.g. the prebuilt + * standalone SPA in `@devframes/json-render-ui`) subscribes to this one key to + * discover which views exist, then subscribes to each view's own state. A hub + * dock, by contrast, is handed a specific {@link JsonRenderViewRef} and needs + * no index. + */ +export const JSON_RENDER_INDEX_KEY = 'devframe:json-render:index' + +/** + * One entry in the {@link JSON_RENDER_INDEX_KEY view index}. Fully + * serializable: it locates a view's live state and carries the display title a + * multi-view frontend uses to label it. + */ +export interface JsonRenderIndexEntry { + /** Author-supplied stable id, unique within the view's scope. */ + id: string + /** The view's scope segment (e.g. `global` or a context namespace). */ + scope: string + /** Shared-state key the client subscribes to for the live spec + state. */ + stateKey: string + /** Human-facing label for the view (defaults to `id`). */ + title: string + /** Upstream `@json-render/*` version the view was authored against. */ + upstreamVersion: string +} + +/** The shape of the view-index shared state: entries keyed by `stateKey`. */ +export type JsonRenderIndex = Record diff --git a/packages/json-render/test/create-view.test.ts b/packages/json-render/test/create-view.test.ts index ef893a0..00ca4e7 100644 --- a/packages/json-render/test/create-view.test.ts +++ b/packages/json-render/test/create-view.test.ts @@ -6,6 +6,7 @@ import { join } from 'node:path' import { createHostContext } from 'devframe/node' import { beforeEach, describe, expect, it } from 'vitest' import { createJsonRenderView } from '../src/node/index' +import { JSON_RENDER_INDEX_KEY } from '../src/view-index' import { JSON_RENDER_UPSTREAM_VERSION } from '../src/view-ref' function createHost(): DevframeHost { @@ -97,6 +98,30 @@ describe('createJsonRenderView validation', () => { }) }) +describe('createJsonRenderView index', () => { + it('publishes an index entry (title defaults to id) and removes it on dispose', async () => { + const view = createJsonRenderView(ctx, { id: 'metrics', spec }) + const index = await ctx.rpc.sharedState.get(JSON_RENDER_INDEX_KEY) + expect((index.value() as Record)[view.ref.stateKey]).toEqual({ + id: 'metrics', + scope: 'global', + stateKey: view.ref.stateKey, + title: 'metrics', + upstreamVersion: JSON_RENDER_UPSTREAM_VERSION, + }) + + view.dispose() + expect((index.value() as Record)[view.ref.stateKey]).toBeUndefined() + }) + + it('carries an explicit title', async () => { + const view = createJsonRenderView(ctx, { id: 'm', title: 'Metrics', spec }) + const index = await ctx.rpc.sharedState.get(JSON_RENDER_INDEX_KEY) + expect((index.value() as Record)[view.ref.stateKey].title).toBe('Metrics') + expect(view.title).toBe('Metrics') + }) +}) + describe('createJsonRenderView disposal', () => { it('unregisters the shared state and frees the id', () => { const view = createJsonRenderView(ctx, { id: 'v', spec }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef26034..1850c65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -474,9 +474,6 @@ importers: examples/minimal-json-render: dependencies: - '@antfu/design': - specifier: catalog:frontend - version: 0.3.2(@antfu/utils@9.3.0)(@iconify-json/catppuccin@1.2.17)(@tanstack/vue-virtual@3.13.30(vue@3.5.40(typescript@6.0.3)))(@unocss/core@66.7.5)(colorjs.io@0.6.1)(dompurify@3.4.12)(floating-vue@5.2.2(vue@3.5.40(typescript@6.0.3)))(playwright@1.61.1)(reka-ui@2.10.1(vue@3.5.40(typescript@6.0.3)))(splitpanes@4.1.2(vue@3.5.40(typescript@6.0.3)))(unocss@66.7.5(@unocss/postcss@66.7.5(postcss@8.5.19))(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.1)(yaml@2.9.0)))(vue@3.5.40(typescript@6.0.3)) '@devframes/json-render': specifier: workspace:* version: link:../../packages/json-render @@ -489,22 +486,10 @@ importers: devframe: specifier: workspace:* version: link:../../packages/devframe - vue: - specifier: catalog:frontend - version: 3.5.40(typescript@6.0.3) devDependencies: - '@vitejs/plugin-vue': - specifier: catalog:build - version: 6.0.8(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.1)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)) get-port-please: specifier: catalog:deps version: 3.2.0 - unocss: - specifier: catalog:frontend - version: 66.7.5(@unocss/postcss@66.7.5(postcss@8.5.19))(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.1)(yaml@2.9.0)) - vite: - specifier: catalog:build - version: 8.1.5(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.1)(yaml@2.9.0) examples/minimal-next-devframe-hub: dependencies: diff --git a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.d.ts index a7f0497..0f82f5d 100644 --- a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.d.ts @@ -45,7 +45,7 @@ export declare function createJsonRenderDockRenderer(_?: JsonRenderDockRendererO export declare function createRenderer(_?: CreateRendererOptions): import("vue").DefineComponent<{}, () => import("vue").VNode, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>; -export declare function sanitizeSpec(_: Spec): Spec; +export declare function sanitizeSpec(_: Spec, _?: ComponentRegistry): Spec; // #endregion // #region Variables @@ -128,6 +128,7 @@ export declare const JsonRenderView: import("vue").DefineComponent; +export declare const UNSUPPORTED_COMPONENT_TYPE: string; // #endregion // #region Other diff --git a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.js index b492644..ce5f54a 100644 --- a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.js +++ b/tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.js @@ -5,13 +5,14 @@ export function createActionBridge(_, _) {} export function createJsonRenderDockRenderer(_) {} export function createRenderer(_) {} -export function sanitizeSpec(_) {} +export function sanitizeSpec(_, _) {} // #endregion // #region Variables export var baseRegistry /* const */ export var ERROR_COMPONENT_TYPE /* const */ export var JsonRenderView /* const */ +export var UNSUPPORTED_COMPONENT_TYPE /* const */ // #endregion // #region Other diff --git a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/spa.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/json-render-ui/spa.snapshot.d.ts new file mode 100644 index 0000000..47a6ac1 --- /dev/null +++ b/tests/__snapshots__/tsnapi/@devframes/json-render-ui/spa.snapshot.d.ts @@ -0,0 +1,10 @@ +/** + * Generated by tsnapi — public API snapshot of `@devframes/json-render-ui/spa` + */ +// #region Functions +export declare function createJsonRenderDevframe(_: DevframeDefinition): DevframeDefinition; +// #endregion + +// #region Variables +export declare const jsonRenderSpaDir: string; +// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/spa.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/json-render-ui/spa.snapshot.js new file mode 100644 index 0000000..d0f84dd --- /dev/null +++ b/tests/__snapshots__/tsnapi/@devframes/json-render-ui/spa.snapshot.js @@ -0,0 +1,10 @@ +/** + * Generated by tsnapi — public API snapshot of `@devframes/json-render-ui/spa` + */ +// #region Functions +export function createJsonRenderDevframe(_) {} +// #endregion + +// #region Variables +export var jsonRenderSpaDir /* const */ +// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/json-render/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/json-render/index.snapshot.d.ts index f3e7532..c2af7e7 100644 --- a/tests/__snapshots__/tsnapi/@devframes/json-render/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/json-render/index.snapshot.d.ts @@ -1,8 +1,19 @@ /** * Generated by tsnapi — public API snapshot of `@devframes/json-render` */ +// #region Interfaces +export interface JsonRenderIndexEntry { + id: string; + scope: string; + stateKey: string; + title: string; + upstreamVersion: string; +} +// #endregion + // #region Types export type BaseComponentName = keyof typeof basePropSchemas; +export type JsonRenderIndex = Record; // #endregion // #region Variables @@ -236,6 +247,7 @@ export declare const IconPropsSchema: z.ZodObject<{ name: z.ZodOptional, z.ZodRecord>]>>; size: z.ZodOptional, z.ZodRecord>]>>; }, z.core.$strip>; +export declare const JSON_RENDER_INDEX_KEY: string; export declare const KeyValueTablePropsSchema: z.ZodObject<{ data: z.ZodOptional, z.ZodIntersection, z.ZodRecord>]>>; loading: z.ZodOptional, z.ZodRecord>]>>; diff --git a/tests/__snapshots__/tsnapi/@devframes/json-render/index.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/json-render/index.snapshot.js index 986c18b..ff963ff 100644 --- a/tests/__snapshots__/tsnapi/@devframes/json-render/index.snapshot.js +++ b/tests/__snapshots__/tsnapi/@devframes/json-render/index.snapshot.js @@ -16,6 +16,7 @@ export { CodeBlockPropsSchema } export { DataTablePropsSchema } export { DividerPropsSchema } export { IconPropsSchema } +export { JSON_RENDER_INDEX_KEY } export { JSON_RENDER_UPSTREAM_VERSION } export { KeyValueTablePropsSchema } export { ProgressPropsSchema } diff --git a/tests/__snapshots__/tsnapi/@devframes/json-render/node.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/json-render/node.snapshot.d.ts index 9731f5f..f6c0ee0 100644 --- a/tests/__snapshots__/tsnapi/@devframes/json-render/node.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/json-render/node.snapshot.d.ts @@ -6,6 +6,7 @@ export interface CreateJsonRenderViewOptions { id: string; spec: DevframeJsonRenderSpec; scope?: string; + title?: string; } // #endregion diff --git a/tsconfig.base.json b/tsconfig.base.json index 8629e6c..78f90a2 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -139,6 +139,9 @@ "@devframes/json-render-ui/components": [ "./packages/json-render-ui/src/components/index.ts" ], + "@devframes/json-render-ui/spa": [ + "./packages/json-render-ui/src/spa.ts" + ], "@devframes/json-render-ui": [ "./packages/json-render-ui/src/index.ts" ], diff --git a/turbo.json b/turbo.json index d1775d6..e35c6bb 100644 --- a/turbo.json +++ b/turbo.json @@ -108,11 +108,6 @@ "dependsOn": ["devframe#build"], "outputs": ["dist/**"] }, - "minimal-json-render#build": { - "outputLogs": "new-only", - "dependsOn": ["devframe#build", "@devframes/json-render#build", "@devframes/json-render-ui#build"], - "outputs": ["dist/**"] - }, "@devframes/plugin-git#build": { "outputLogs": "new-only", "dependsOn": ["devframe#build"],