Skip to content

feat!: swappable UI packages — renderer-module manifest + missing-renderer fallback - #194

Merged
antfu merged 12 commits into
mainfrom
feat/swappable-ui-renderer-manifest
Aug 13, 2026
Merged

feat!: swappable UI packages — renderer-module manifest + missing-renderer fallback#194
antfu merged 12 commits into
mainfrom
feat/swappable-ui-renderer-manifest

Conversation

@antfubot

Copy link
Copy Markdown
Collaborator

Intent

Make the headless/UI split fully swappable, both ways, independently (plan 033 — written and implemented in this PR): devframe / @devframes/hub / @devframes/json-render stay headless, @devframes/hub-ui and @devframes/json-render-ui become opinionated reference implementations that any community package following the same contracts can replace — and independently of each other. When no json-render renderer is composed, hub-ui shows a designed fallback (No renderer for "json-render" in the current environment) instead of a dead panel.

What changed

@devframes/hub

  • initHub({ renderers }) — the composition seam for prebuilt viewers: each registration's self-contained browser ES module is served at <base>__renderers/<type>.mjs and published in a renderer manifest (devframe:dock-renderers shared state). Validated fail-fast with new structured diagnostics DF8108DF8110.
  • Client: createDockRenderersContext() is now the shared registry factory (used by createDevframeClientHost and hub-ui alike) — local registrations win, manifest modules are lazy-imported on first mount and cached, and mount() resolves a typed DockRendererMountResult (mounted / missing-renderer / load-error) instead of silently no-opping. has() answers for both sources.

@devframes/json-render — owns the renderer contract: JsonRenderDockRenderer / JsonRenderDockMountOptions exported from ./hub (previously declared structurally inside the implementer).

@devframes/json-render-ui — ships dist/renderer/json-render.mjs, a self-contained, self-styling, shadow-root-safe renderer module (attaches its own shadow root, injects its compiled CSS, follows the viewer's live dark class, derives brand color from the inherited --devframe-primary), plus jsonRenderUiRenderer() on the new node-safe ./hub entry.

@devframes/hub-ui — deletes its bundled Vue json-render components (~20 files); every non-native dock type now routes through the registry via the new ViewDockRenderer.vue, which renders the generic missing-renderer fallback and a load-error variant with retry (Storybook stories included). No json-render rendering dependency remains.

Examples — the minimal hosts (vite/next/rsbuild) compose json-render-ui with the one-liner renderers: [jsonRenderUiRenderer()] and register an inline-spec dock; the protocol witnesses stay at parity: hub-vite consumes the manifest module (no renderer compiled in), hub-next overrides the same manifest with its local React renderer (witnessing local-wins precedence and frontend swappability), and both register a demo-unrendered dock type to witness the fallback.

Docshub-initiate renderer-modules section, updated client-context / json-render, two new guides (Build Your Own Hub UI, Build Your Own JSON-Render Frontend), error pages for the new codes, and 0.9 migration notes.

Breaking (0.9 wave)

  • renderers.mount() resolves a DockRendererMountResult instead of a bare disposer.
  • @devframes/hub-ui no longer bundles a json-render renderer — compose one via initHub({ renderers: [jsonRenderUiRenderer()] }).

Verified with pnpm lint && pnpm knip && pnpm test && pnpm typecheck && pnpm build (API snapshots updated with the breaking flag) and a runtime smoke test of the served renderer module + hub endpoints on hub-vite-minimal.


Created with the help of an agent.

…derer fallback

The headless/UI split becomes fully swappable, both ways, independently
(plan 033):

- @devframes/hub: initHub({ renderers }) serves prebuilt renderer modules at
  <base>__renderers/<type>.mjs and publishes a renderer manifest over shared
  state; the client registry lazy-imports manifest modules (local
  registrations win) and mount() resolves a typed result
  (mounted / missing-renderer / load-error). DF8108–DF8110 diagnostics.
- @devframes/json-render: owns the renderer contract
  (JsonRenderDockRenderer / JsonRenderDockMountOptions on ./hub).
- @devframes/json-render-ui: ships a self-contained, self-styling,
  shadow-root-safe renderer module plus the jsonRenderUiRenderer()
  registration helper on the new ./hub entry.
- @devframes/hub-ui: drops its bundled json-render components; every
  non-native dock type routes through the registry, with a generic
  missing-renderer fallback view (load-error variant with retry).
- Examples: minimal hosts compose json-render-ui via the manifest one-liner;
  hub-vite consumes the manifest, hub-next overrides it with a local React
  renderer, and both witness the fallback with an unrendered dock type.
- Docs: renderer-modules guide section, build-your-own-hub-ui and
  build-your-own-json-render-frontend guides, error pages, 0.9 migration
  notes.

BREAKING CHANGE: renderers.mount() resolves a DockRendererMountResult instead
of a bare disposer, and @devframes/hub-ui no longer bundles a json-render
renderer — compose one via initHub({ renderers: [jsonRenderUiRenderer()] }).
@netlify

netlify Bot commented Aug 13, 2026

Copy link
Copy Markdown

Deploy Preview for devfra ready!

Name Link
🔨 Latest commit dcf4952
🔍 Latest deploy log https://app.netlify.com/projects/devfra/deploys/6a7d70fd91fa0900086f8fe0
😎 Deploy Preview https://deploy-preview-194--devfra.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

antfubot and others added 11 commits August 13, 2026 03:49
…n hub-next

Turbopack bundled the static `@devframes/json-render-ui/hub` import into the
Next server chunk, so the helper's `import.meta.url`-based bundle-path
resolution pointed inside `.next/…/assets/dist/` and tripped DF8109. Load it
through the same bundler-ignored dynamic `import()` as the plugins, so Node
resolves the published `dist` at request time and the renderer module resolves
correctly.

Also type the React example's dock renderer against the moved
`JsonRenderDockRenderer` contract, and widen the client renderer registry's
`register`/`renderers` inputs to `DockRenderer<any>` so a renderer narrowed to
a specific entry variant plugs in.
Each hub host example (hub-vite, hub-next, and the vite/next/rsbuild/hono/nitro
minimal hosts) now mounts all nine built-in plugins — a11y, assets, code-server,
data-inspector, git, inspect, messages, og, terminals — so every example
dogfoods the full plugin surface end to end.

- Node-config hosts (vite/rsbuild/hono/nitro) import the plugin factories
  directly; nitro adds each package to its externalized list so their
  import.meta.url dist lookups resolve.
- Bundled Next hosts load the node-side plugins through the existing
  bundler-ignored dynamic import() (variable specifiers) so Turbopack never
  inlines their node-only code.
- data-inspector gets a colon-free id override (its default id carries ':',
  invalid as a hub mount segment), and the assets watcher is disabled since
  these hosts demonstrate mounting, not authoring.

Verified all nine frames mount with no diagnostics on both a node-config host
(hub-vite-minimal) and a bundled host (hub-next-minimal) at runtime.
… surfaces

Wind4 emits its theme as `@property` registrations and cascade `@layer`s on
the document root, which don't apply reliably to a stylesheet adopted into a
web-component shadow tree — the exact surfaces these two packages render into
(hub-ui's dock custom element, json-render-ui's renderer module, both of which
inject their compiled UnoCSS into their own shadow root). Swapping their base
utility preset to Wind3 emits plain utilities that style shadow content
correctly.

- design/uno.config.ts: add a createDesignConfig({ base }) factory so a
  surface can pick its base preset; the shared `designConfig` constant stays
  on Wind4, so every other plugin and example is unchanged.
- hub-ui composes createDesignConfig({ base: presetWind3() }).
- json-render-ui's package + SPA configs swap presetWind4 → presetWind3 (both,
  so its components render identically in the standalone SPA and the
  shadow-root renderer module).
- Regenerated the committed shadow-root stylesheets (no more @layer output).
Keep every capability worth demonstrating while making both reference hosts
easier to read, at parity:

- Drop the redundant second demo tool (Demo Tool B) from each — one plain
  demo SPA + the Tabbed Tool (shared-frame soft-nav) already show the mount
  path.
- hub-next: merge the json-render React frontend's two files (registry.tsx +
  dock-renderer.tsx, ~496 lines) into one compact react-renderer.tsx (~350),
  trimming the sanitizeSpec/error-isolation nicety and rendering Tree as a JSON
  dump. Still a faithful local renderer that overrides the manifest-served Vue
  module (the swap seam).
- hub-vite: restructure main.ts into small labelled helpers (el map,
  dockButton, wireDockRail, wireDrawer) and replace the bespoke icon
  cache/hydrate reconciliation with a one-shot fetch-and-patch.
- hub-next: lift the client-only dock registration and drawer polling out of
  the boot effect into reusable helpers.
- READMEs updated in lockstep.

Kept (worth demonstrating): all nine built-in plugins, the Tabbed Tool
soft-nav, both client-only docks (Client Notes + the interactive Client
Playground), the renderer manifest + local-override swap seam, and the
missing-renderer fallback.

Verified: lint, knip, test, typecheck, build all pass, and both hubs boot
with 11 frames (no Demo Tool B), the json-render renderer module served, and
no diagnostics.
… root

The earlier Wind3 switch was the wrong remedy: @antfu/design is Wind4-native,
and under Wind3 its core semantic utilities (`bg-base`, `color-base`,
`bg-primary`, `color-white`) aren't generated at all — so the dock / renderer
surfaces lost their background and text colors, which is what made borders
look wrong in context.

The real Wind4 shadow-DOM problem was narrower: Wind4 emits @antfu/design's
theme (`--colors-*`) in one `:root {}` block, and `:root` matches only the
top-level document, never a shadow host — so `color-mix(var(--colors-*))`
utilities resolved to nothing inside the dock's / renderer module's shadow
root. Fix it at the source: revert both packages to Wind4 and have their
`build-css` scripts retarget that theme block at `:root, :host`, so the
variables cascade into the shadow tree (`@property` opacity defaults are
document-global already). Regenerated both shadow-root stylesheets.

Reverts the Wind3 change in 3ee8155; the standalone json-render SPA (light
DOM) stays on plain Wind4.
… surface tokens

Per the web-component constraint, both shadow-root packages build on Wind3
(not Wind4): @antfu/design's semantic utilities compile to concrete rgb() +
.dark variants under Wind3, self-contained inside a shadow tree, whereas Wind4
keeps its theme/--un-* behind a document :root {} block and
@Property { inherits: false } that never reach the shadow root.

The remaining breakage was a Wind3 shortcut+variant interaction: when a
semantic shortcut also appears variant-prefixed in the sources (@antfu/design's
Tabs emits data-[state=active]:bg-base), the single-pass build-css generate
dropped the *plain* .bg-base / .color-base rules — so the dock / renderer
surfaces lost their background and text colors, which is what made borders look
wrong in context. Fix: generate the shadow-surface tokens (shadowSurfaceSafelist,
shared from design/uno.config) in a dedicated pass and append them, so the plain
(and .dark) rules are always present. The standalone SPA (Vite unocss path)
isn't affected and keeps the tokens via safelist.

- design/uno.config: reintroduce createDesignConfig({ base }) + export
  shadowSurfaceSafelist; the shared designConfig stays Wind4 for every other
  (non-web-component) surface.
- hub-ui / json-render-ui: Wind3 base + safelist; build-css appends the
  dedicated surface pass. Regenerated both shadow-root stylesheets.

Reverts the Wind4 :root->:host attempt in cc987b2.
…sign system

Record the shadow-root preset rule the two web-component packages follow:
Wind4 for ordinary surfaces, Wind3 for anything injected into a shadow root
(hub-ui dock, json-render-ui renderer module), plus the dedicated-pass
workaround for the Wind3 shortcut+variant gotcha and the shared
createDesignConfig({ base }) / shadowSurfaceSafelist helpers.
…ive a Wind4 host

The dock / renderer surfaces still rendered wrong colors because `@property`
registrations are document-global: a host page built on Wind4 (e.g. the
vite-hub example) registers `--un-border-opacity` / `--un-bg-opacity` /
`--un-text-opacity` as `@property { syntax: '<percentage>' }` for the whole
document — including our Wind3 shadow tree, where those vars are written
unitless (`--un-border-opacity: 0.13`). The global `<percentage>` registration
makes each declaration invalid ('Invalid property value, expected
<percentage>') and collapses the dependent `rgb(… / var(--un-*))` color.

Rename every `--un-` in the shadow-injected stylesheet to a private `--dfun-`
prefix (design/uno.config's `namespaceShadowCssVars`) so the self-contained
shadow CSS is immune to whatever the host registered. Regenerated both
stylesheets; the standalone SPAs (own the document) are untouched.

Also memoed both shadow-root gotchas (this one + the plain-vs-variant shortcut
drop) in AGENTS.md.
…n-jr-, --un-hub-)

Give each shadow-root stylesheet its own namespaced Wind vars — json-render-ui
uses `--un-jr-*`, hub-ui `--un-hub-*` — instead of a shared `--dfun-`. The
readable `--un-` root stays; the per-surface segment keeps the names distinct
from a Wind4 host's document-global `@property --un-*` registrations (and from
each other). `namespaceShadowCssVars` now takes the prefix; regenerated both
stylesheets.
# Conflicts:
#	AGENTS.md
#	docs/guide/migration-0.9.md
#	examples/hub-hono-minimal/package.json
#	examples/hub-next/README.md
#	examples/hub-next/package.json
#	examples/hub-next/src/client/app/page.tsx
#	examples/hub-next/src/client/devframe/demo-devframe-b.ts
#	examples/hub-next/src/client/devframe/next-devframe-hub.ts
#	examples/hub-next/src/client/json-render/dock-renderer.tsx
#	examples/hub-next/src/client/json-render/registry.tsx
#	examples/hub-nitro-minimal/package.json
#	examples/hub-vite/README.md
#	examples/hub-vite/src/client/env.d.ts
#	examples/hub-vite/src/client/main.ts
#	examples/hub-vite/src/devframe-b.ts
#	pnpm-lock.yaml
@antfu
antfu merged commit 369cb55 into main Aug 13, 2026
12 checks passed
@antfu
antfu deleted the feat/swappable-ui-renderer-manifest branch August 13, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants