feat!: swappable UI packages — renderer-module manifest + missing-renderer fallback - #194
Merged
Conversation
…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()] }).
✅ Deploy Preview for devfra ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Make the headless/UI split fully swappable, both ways, independently (plan 033 — written and implemented in this PR):
devframe/@devframes/hub/@devframes/json-renderstay headless,@devframes/hub-uiand@devframes/json-render-uibecome 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/hubinitHub({ renderers })— the composition seam for prebuilt viewers: each registration's self-contained browser ES module is served at<base>__renderers/<type>.mjsand published in a renderer manifest (devframe:dock-renderersshared state). Validated fail-fast with new structured diagnosticsDF8108–DF8110.createDockRenderersContext()is now the shared registry factory (used bycreateDevframeClientHostand hub-ui alike) — local registrations win, manifest modules are lazy-imported on first mount and cached, andmount()resolves a typedDockRendererMountResult(mounted/missing-renderer/load-error) instead of silently no-opping.has()answers for both sources.@devframes/json-render— owns the renderer contract:JsonRenderDockRenderer/JsonRenderDockMountOptionsexported from./hub(previously declared structurally inside the implementer).@devframes/json-render-ui— shipsdist/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 livedarkclass, derives brand color from the inherited--devframe-primary), plusjsonRenderUiRenderer()on the new node-safe./hubentry.@devframes/hub-ui— deletes its bundled Vue json-render components (~20 files); every non-native dock type now routes through the registry via the newViewDockRenderer.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 ademo-unrendereddock type to witness the fallback.Docs —
hub-initiaterenderer-modules section, updatedclient-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 aDockRendererMountResultinstead of a bare disposer.@devframes/hub-uino longer bundles a json-render renderer — compose one viainitHub({ 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.