From 1ac595f1d0f619fd868414826ab57fb2c6c1389b Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Tue, 18 Aug 2026 00:54:27 +0000 Subject: [PATCH] chore(json-render-ui)!: stop leaking frontend packages as runtime deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reference Vue frontend is delivered only as self-contained prebuilt bundles — the standalone SPA (`/spa`) and the hub renderer module (`/hub`), each inlining Vue, the upstream renderer, and the compiled @antfu/design styles. The tsdown `.`/`components` library entries were the only surface that externalised those frontend packages, yet nothing consumes them at runtime. Drop the `.` and `./components` exports so the package ships zero runtime frontend dependencies: `@json-render/vue` moves to devDependencies (bundled into the Vite artifacts), `vue`/`@devframes/json-render` leave the peer set, and the node-safe entries keep only `@devframes/hub` + `devframe` as optional, type-only peers. Also switch tsdown to the node platform and scope `neverBundle` to the type-only references, so `dist/spa.d.mts` references `devframe` instead of inlining its entire type graph (3150 -> 32 lines) and the build no longer walks devframe's server source. BREAKING CHANGE: `@devframes/json-render-ui` no longer exports `.` or `./components`. `JsonRenderView`, `createRenderer`, the component exports, and `createJsonRenderDockRenderer` are no longer importable; consume the Vue frontend via the prebuilt `/spa` SPA or the `/hub` renderer module, and build custom frontends against the `@devframes/json-render/hub` contract. Created with the help of an agent. --- docs/guide/client-context.md | 2 +- docs/guide/json-render.md | 69 ++++----- knip.jsonc | 9 +- packages/json-render-ui/package.json | 13 +- packages/json-render-ui/tsdown.config.ts | 41 ++++-- pnpm-lock.yaml | 7 +- .../json-render-ui/components.snapshot.d.ts | 23 --- .../json-render-ui/components.snapshot.js | 22 --- .../json-render-ui/index.snapshot.d.ts | 138 ------------------ .../json-render-ui/index.snapshot.js | 33 ----- 10 files changed, 69 insertions(+), 288 deletions(-) delete mode 100644 tests/__snapshots__/tsnapi/@devframes/json-render-ui/components.snapshot.d.ts delete mode 100644 tests/__snapshots__/tsnapi/@devframes/json-render-ui/components.snapshot.js delete mode 100644 tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.d.ts delete mode 100644 tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.js diff --git a/docs/guide/client-context.md b/docs/guide/client-context.md index 902acece..88dbd696 100644 --- a/docs/guide/client-context.md +++ b/docs/guide/client-context.md @@ -38,7 +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`). Local registrations take precedence over the hub's [renderer manifest](./hub-initiate#renderer-modules). | +| `renderers` | Dock renderers to register at boot, keyed by dock `type` (e.g. `{ 'json-render': myRenderer }` — any implementation of the dock-renderer contract the host bundles). Local registrations take precedence over the hub's [renderer manifest](./hub-initiate#renderer-modules). | 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. diff --git a/docs/guide/json-render.md b/docs/guide/json-render.md index e2084e58..9cf02d84 100644 --- a/docs/guide/json-render.md +++ b/docs/guide/json-render.md @@ -124,37 +124,21 @@ 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 { JsonRenderView } from '@devframes/json-render-ui' -import { connectDevframe } from 'devframe/client' -import { createApp, h, shallowRef } from 'vue' - -const rpc = await connectDevframe() -const state = await rpc.sharedState.get('devframe:json-render:global:metrics', { initialValue: null }) -const spec = shallowRef(state.value()) -state.on('updated', () => { - spec.value = state.value() -}) - -createApp({ - render: () => h(JsonRenderView, { - spec: spec.value, - rpc, - interactive: rpc.connectionMeta.backend !== 'static', - }), -}).mount('#app') -``` +A custom frontend renders a view straight from its shared state: connect with +`connectDevframe()`, read the view's state (keyed +`devframe:json-render::`), subscribe to its `updated` events, and +render each spec element with your own component registry. The renderer +contract and the base catalog's per-component prop schemas live in the +framework-neutral `@devframes/json-render` package, so a frontend in any +framework implements the same spec — see [Build your own JSON-render +frontend](./build-your-own-json-render-frontend) and the React renderer in the +[Next hub example](/examples/hub-next). In a **static** build the spec + state are snapshotted as a read-only render; -there is no live RPC, so the action bridge reports actions as unavailable and -`interactive: false` renders a static-output notice. Local state and bindings -still work. +there is no live RPC, so actions report as unavailable and a frontend shows a +static-output notice. Local state and bindings still work. -### Consuming the reference frontend +### The reference frontend `@devframes/json-render-ui` wraps `@antfu/design`'s Vue components directly (`ActionButton`, `DisplayBadge`, `LayoutCard`, `FormTextInput`, `FormSwitch`, @@ -164,14 +148,13 @@ the rest of the devframe surfaces. A few catalog components stay bespoke where `@antfu/design` has no matching primitive — `Stack`, `Text`, `CodeBlock`, the value-tree `Tree`, and the row-clickable/loadable `DataTable`. -A consuming Vite app therefore: - -- installs `@antfu/design` (a peer dependency) and imports `@antfu/design/styles.css`; -- excludes it from dep pre-bundling so `@vitejs/plugin-vue` compiles its SFCs — - `optimizeDeps: { exclude: ['@antfu/design'] }`; -- composes the shared UnoCSS preset (`presetAnthonyDesign`) and safelists the - runtime-selected badge colors the base catalog can emit — - `safelist: ['badge-color-green', 'badge-color-amber', 'badge-color-red', 'badge-color-blue']`. +It ships as two self-contained prebuilt bundles — the standalone SPA +(`@devframes/json-render-ui/spa`) and the hub renderer module +(`@devframes/json-render-ui/hub`) — each inlining Vue, the upstream renderer, +and the compiled `@antfu/design` styles. A consuming app wires nothing and +pulls no frontend package into its own graph: the SPA is served verbatim as +`cli.distDir`, and the hub module is imported natively by the viewer from the +renderer manifest. ## Rendering inside a hub @@ -207,15 +190,18 @@ registration for the type, a viewer shows its missing-renderer fallback panel. A host page that builds its own client can register a renderer **locally** -instead — it takes precedence over the manifest: +instead — it takes precedence over the manifest. The renderer is any +implementation of the `JsonRenderDockRenderer` contract the host bundles +itself (the [Next hub example](/examples/hub-next) registers a React one this +way): ```ts // host page — a locally-bundled frontend wins over the manifest module import { createDevframeClientHost } from '@devframes/hub/client' -import { createJsonRenderDockRenderer } from '@devframes/json-render-ui' +import { myJsonRenderDockRenderer } from './my-renderer' const host = await createDevframeClientHost({ - renderers: { 'json-render': createJsonRenderDockRenderer() }, + renderers: { 'json-render': myJsonRenderDockRenderer }, }) // the viewer mounts the active dock into a container it owns @@ -249,8 +235,9 @@ or a local registration at `createDevframeClientHost({ renderers })`. `@devframes/json-render-ui` is the reference implementation, not a hard dependency of the protocol; the hub acquires no Vue. -Within a frontend, the registry swaps too — pass a custom `registry` to -`createRenderer({ registry })` or `createJsonRenderDockRenderer({ registry })`. +Within a frontend, the component registry is pluggable too — an implementation +maps each catalog component type to its own component, so a frontend can render +a subset or theme the built-ins without touching the protocol. A frontend need not implement every component. When a spec references a component the active registry lacks, the renderer isolates that element behind a diff --git a/knip.jsonc b/knip.jsonc index 11898832..e0aeb1c6 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -139,10 +139,11 @@ "ignoreDependencies": ["@nuxt/schema"] }, "packages/json-render-ui": { - // `src/components/index.ts` is already picked up via - // `tsdown.config.ts`; only `spa.ts`/`hub.ts` (node-safe entries) and - // the prebuilt renderer module (built by its own Vite config, consumed - // at runtime via the hub's renderer manifest) need declaring. + // Published node-safe entries are `spa.ts`/`hub.ts`; the browser + // renderer ships only as self-contained Vite bundles (the standalone + // SPA and the prebuilt renderer module, consumed at runtime via the + // hub's renderer manifest). `src/index.ts` stays as the source barrel + // those Vite/Storybook builds resolve, so it's declared as an entry too. "entry": ["src/{index,spa,hub}.ts", "src/renderer-module/index.ts"], // The standalone SPA's own Vite config (`src/spa/vite.config.ts`) // mounts `unocss/vite` with no explicit config path, so UnoCSS diff --git a/packages/json-render-ui/package.json b/packages/json-render-ui/package.json index 7ea21cae..27eb98b2 100644 --- a/packages/json-render-ui/package.json +++ b/packages/json-render-ui/package.json @@ -20,13 +20,10 @@ ], "sideEffects": false, "exports": { - ".": "./dist/index.mjs", - "./components": "./dist/components/index.mjs", "./hub": "./dist/hub.mjs", "./spa": "./dist/spa.mjs", "./package.json": "./package.json" }, - "types": "./dist/index.d.mts", "files": [ "dist" ], @@ -42,22 +39,22 @@ }, "peerDependencies": { "@devframes/hub": "workspace:*", - "@devframes/json-render": "workspace:*", - "vue": "^3.5.0" + "devframe": "workspace:*" }, "peerDependenciesMeta": { "@devframes/hub": { "optional": true + }, + "devframe": { + "optional": true } }, - "dependencies": { - "@json-render/vue": "catalog:frontend" - }, "devDependencies": { "@antfu/design": "catalog:frontend", "@devframes/hub": "workspace:*", "@devframes/json-render": "workspace:*", "@iconify-json/ph": "catalog:frontend", + "@json-render/vue": "catalog:frontend", "@storybook/addon-docs": "catalog:storybook", "@storybook/vue3-vite": "catalog:storybook", "@unocss/preset-icons": "catalog:frontend", diff --git a/packages/json-render-ui/tsdown.config.ts b/packages/json-render-ui/tsdown.config.ts index 7e229e43..425d49fa 100644 --- a/packages/json-render-ui/tsdown.config.ts +++ b/packages/json-render-ui/tsdown.config.ts @@ -1,28 +1,41 @@ import { defineConfig } from 'tsdown' -// Browser-only library. Vue and the protocol package are peers, so they stay -// external (the consuming app / hub host provides them). Components are plain -// `ComponentFn` render functions in `.ts`, so no SFC compiler is needed. +// Node-safe entries only. The browser renderer (Vue components, the upstream +// renderer, the `@antfu/design` ports) ships exclusively as self-contained +// Vite bundles — the standalone SPA (`src/spa/vite.config.ts`) and the hub +// renderer module (`src/renderer-module/vite.config.ts`) — both of which +// inline vue, `@json-render/vue`, and `@antfu/design` at build time, so no +// frontend package leaks out as a runtime dependency. +// +// These two tsdown entries expose only path/registration helpers pointing at +// those prebuilt bundles; their sole imports are node built-ins plus a couple +// of type-only references. Keep those types external (`neverBundle`) so the +// emitted `.d.mts` references the packages instead of inlining their whole +// type graph. 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', + // Node-safe entry: the prebuilt SPA path + a devframe wiring helper. + // Imports only `node:url` (plus a `devframe` type). + spa: 'src/spa.ts', // Node-safe entry: the hub renderer-manifest registration pointing at the // prebuilt module in `dist/renderer/` (built by its own Vite config). - 'hub': 'src/hub.ts', + hub: 'src/hub.ts', }, outExtensions: () => ({ js: '.mjs', dts: '.d.mts' }), clean: true, tsconfig: '../../tsconfig.base.json', dts: true, - platform: 'browser', + platform: 'node', deps: { - // Keep peers external; `@antfu/design` ships `.vue` source that the - // consumer's Vite (with @vitejs/plugin-vue) compiles, so it must not be - // bundled/parsed here. - neverBundle: ['vue', '@antfu/design', /^@antfu\/design\//, '@devframes/json-render', '@devframes/json-render/core'], + // Type-only references in these node entries — keep them external so the + // `.d.mts` references each package rather than inlining its type graph. + neverBundle: [ + 'devframe', + '@devframes/hub', + '@devframes/hub/initiate', + '@devframes/json-render', + '@devframes/json-render/hub', + '@devframes/json-render/core', + ], }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4b7ea68..26a8090d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1341,10 +1341,6 @@ importers: version: 0.22.14(@volar/typescript@2.4.28(typescript@6.0.3))(oxc-resolver@11.24.2)(tsx@4.23.12)(typescript@6.0.3) packages/json-render-ui: - dependencies: - '@json-render/vue': - specifier: catalog:frontend - version: 0.19.0(vue@3.5.41(typescript@6.0.3))(zod@4.4.3) devDependencies: '@antfu/design': specifier: catalog:frontend @@ -1358,6 +1354,9 @@ importers: '@iconify-json/ph': specifier: catalog:frontend version: 1.2.2 + '@json-render/vue': + specifier: catalog:frontend + version: 0.19.0(vue@3.5.41(typescript@6.0.3))(zod@4.4.3) '@storybook/addon-docs': specifier: catalog:storybook version: 10.5.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.0)(rollup@4.60.3)(storybook@10.5.7(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0)) diff --git a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/components.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/json-render-ui/components.snapshot.d.ts deleted file mode 100644 index ec96a0be..00000000 --- a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/components.snapshot.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Generated by tsnapi — public API snapshot of `@devframes/json-render-ui/components` - */ -// #region Other -export { Badge } -export { Button } -export { Card } -export { CodeBlock } -export { DataTable } -export { Divider } -export { Icon } -export { JrComponent } -export { KeyValueTable } -export { Link } -export { Progress } -export { Select } -export { Stack } -export { Switch } -export { Tabs } -export { Text } -export { TextInput } -export { Tree } -// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/components.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/json-render-ui/components.snapshot.js deleted file mode 100644 index a2a52e9e..00000000 --- a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/components.snapshot.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Generated by tsnapi — public API snapshot of `@devframes/json-render-ui/components` - */ -// #region Other -export { Badge } -export { Button } -export { Card } -export { CodeBlock } -export { DataTable } -export { Divider } -export { Icon } -export { KeyValueTable } -export { Link } -export { Progress } -export { Select } -export { Stack } -export { Switch } -export { Tabs } -export { Text } -export { TextInput } -export { Tree } -// #endregion \ No newline at end of file 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 deleted file mode 100644 index 15ab0bab..00000000 --- a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.d.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Generated by tsnapi — public API snapshot of `@devframes/json-render-ui` - */ -// #region Interfaces -export interface ActionBridgeRpc { - call: (_: string, ..._: unknown[]) => Promise; -} -export interface CreateRendererOptions { - registry?: ComponentRegistry; -} -export interface JsonRenderActionBridge { - handlers: Record) => Promise>; - loading: Record; - error: { - value: JsonRenderActionError | null; - }; -} -export interface JsonRenderActionError { - action: string; - error: unknown; -} -export interface JsonRenderDockRendererOptions { - registry?: ComponentRegistry; -} -// #endregion - -// #region Types -export type JrComponent

> = (_: BaseComponentProps

) => VNodeChild; -// #endregion - -// #region Functions -export declare function createActionBridge(_: ActionBridgeRpc, _?: { - interactive?: boolean; -}): JsonRenderActionBridge; -export declare function createJsonRenderDockRenderer(_?: JsonRenderDockRendererOptions): JsonRenderDockRenderer$1; -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, _?: ComponentRegistry): Spec; -// #endregion - -// #region Variables -export declare const Badge: JrComponent; -export declare const baseRegistry: ComponentRegistry; -export declare const Button: JrComponent; -export declare const Card: JrComponent; -export declare const CodeBlock: JrComponent; -export declare const DataTable: JrComponent; -export declare const Divider: JrComponent<{ - label?: string; -}>; -export declare const ERROR_COMPONENT_TYPE: string; -export declare const Icon: JrComponent; -export declare const JsonRenderView: import("vue").DefineComponent; - default: null; - }; - rpc: { - type: PropType; - required: true; - }; - registry: { - type: PropType; - default: () => ComponentRegistry; - }; - viewId: { - type: StringConstructor; - default: string; - }; - interactive: { - type: BooleanConstructor; - default: boolean; - }; - loading: { - type: BooleanConstructor; - default: boolean; - }; - connectionError: { - type: PropType; - default: null; - }; -}>, () => import("vue").VNode, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly; - default: null; - }; - rpc: { - type: PropType; - required: true; - }; - registry: { - type: PropType; - default: () => ComponentRegistry; - }; - viewId: { - type: StringConstructor; - default: string; - }; - interactive: { - type: BooleanConstructor; - default: boolean; - }; - loading: { - type: BooleanConstructor; - default: boolean; - }; - connectionError: { - type: PropType; - default: null; - }; -}>> & Readonly<{}>, { - spec: Spec | null; - registry: ComponentRegistry; - viewId: string; - interactive: boolean; - loading: boolean; - connectionError: string | null; -}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>; -export declare const KeyValueTable: JrComponent; -export declare const Link: JrComponent; -export declare const Progress: JrComponent; -export declare const Select: JrComponent; -export declare const Stack: JrComponent; -export declare const Switch: JrComponent; -export declare const Tabs: JrComponent; -export declare const Text: JrComponent; -export declare const TextInput: JrComponent; -export declare const Tree: JrComponent; -export declare const UNSUPPORTED_COMPONENT_TYPE: string; -// #endregion - -// #region Other -export { JsonRenderDockMountOptions } -export { JsonRenderDockRenderer } -// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.js deleted file mode 100644 index 423bdf50..00000000 --- a/tests/__snapshots__/tsnapi/@devframes/json-render-ui/index.snapshot.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Generated by tsnapi — public API snapshot of `@devframes/json-render-ui` - */ -// #region Functions -export function createActionBridge(_, _) {} -export function createJsonRenderDockRenderer(_) {} -export function createRenderer(_) {} -export function sanitizeSpec(_, _) {} -// #endregion - -// #region Variables -export var Badge /* const */ -export var baseRegistry /* const */ -export var Button /* const */ -export var Card /* const */ -export var CodeBlock /* const */ -export var DataTable /* const */ -export var Divider /* const */ -export var ERROR_COMPONENT_TYPE /* const */ -export var Icon /* const */ -export var JsonRenderView /* const */ -export var KeyValueTable /* const */ -export var Link /* const */ -export var Progress /* const */ -export var Select /* const */ -export var Stack /* const */ -export var Switch /* const */ -export var Tabs /* const */ -export var Text /* const */ -export var TextInput /* const */ -export var Tree /* const */ -export var UNSUPPORTED_COMPONENT_TYPE /* const */ -// #endregion \ No newline at end of file