fix(sdui): a kind:'html' page can use lazily-registered blocks, and recovers when one registers late - #2988
Merged
Conversation
…ecovers when one registers late
objectui#2953 had a twin one tier over, unreported. The whitelist a kind:'html'
page's source compiles against was built from `getAllTypes()` + `getConfig()` —
both loaded-only — so any block registered via `registerLazy()` was rejected as
"not an allowed component".
The blast radius is worse than the react tier's. There a missing block cost one
identifier; here a compile diagnostic fails the WHOLE page, so a single
`<object-kanban>` replaced the entire page with "HTML page failed to compile (2)",
`<flex>` wrapper included. And it never recovered: `layoutElement` was memoised
on `[schema, pageType]` with no registry signal, so the cached error panel
outlived the plugin landing — broken for the rest of the session. Measured, not
inferred: a probe rendering that page showed the same panel before and after the
loader resolved.
ComponentRegistry gains three lazy-aware reads:
- `getKnownTypes()` — loaded registrations PLUS pending lazy stubs, deduped; the
set a whitelist or manifest should be built from. `getAllTypes()` keeps its
loaded-only meaning ("what can render right now") and now documents it.
- `getMeta(type, namespace?)` — metadata from the loaded registration, else from
a pending stub. `getConfig()` stays loaded-only, since callers read
`.component` off it.
- `getVersion()` — monotonic counter of changes to the known set, bumped on
register / unregister / registerLazy. The previous cache key was the type
COUNT, which one registration plus one unregistration leaves untouched while
the set changed.
`getJsxManifest()` builds from those; `PageRenderer` subscribes to the registry
so a page that could not compile retries when the registry grows. Only the
kind:'html' branch acts on that tick — every other branch rebuilds the same
element type with the same props, which React reconciles in place, as
react-page-state.test.tsx pins.
A stub carries no `inputs` yet, so its props surface as `unknown-prop` warnings
rather than errors: the page compiles and renders, and the inner SchemaRenderer
triggers the loader and swaps in the real block. Authoring-time prop validation
is unaffected — sdui.manifest.json is generated with every plugin eagerly loaded
and asserts as much (#2979), so the runtime whitelist being permissive and the
build-time artifact being strict stay deliberate, not accidental.
Tests: lazy block no longer takes the page down, a late registration recovers
(both fail against the old page.tsx), and a tag nothing registered is still
rejected — admitting stubs must not turn the whitelist into "anything goes",
since that whitelist is what keeps a parsed page from rendering arbitrary tags.
Plus 6 unit tests for the new registry reads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4mrr1ihhwnfEHFSWmGoMp
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
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.
#2953 had a twin one tier over, unreported. Found by checking whether the
kind:'html'path shared the root cause — it does, with a worse blast radius.Measured, not inferred
A probe rendering
<flex><object-kanban object="showcase_project" /></flex>as akind:'html'page, withobject-kanbanregistered lazily (exactly howapps/consoleregisters it):Root cause, and why it's worse here
getJsxManifest()built the whitelist fromgetAllTypes()+getConfig()— both loaded-only, same as #2953. So aregisterLazy()'d block was rejected as unknown, load-order dependently.Two things make this worse than the react tier:
<object-kanban>anywhere in the source replaced everything with an error panel — the<flex>wrapper included. In the react tier a missing block cost one identifier.layoutElementwas memoised on[schema, pageType]with no registry signal, so the cached error panel outlived the plugin actually landing. Broken for the rest of the session.Also spotted on the way:
getJsxManifest()'s cache key was the type count. One registration plus one unregistration leaves the count untouched while the set changed.The fix
Three lazy-aware reads on
ComponentRegistry:getKnownTypes()getMeta(type, ns?)getVersion()getAllTypes()andgetConfig()keep their loaded-only meaning ("what can render right now" / "can I render this") and now document it —getConfig()'s callers read.componentoff it, and a stub has none.getJsxManifest()builds from the new reads;PageRenderersubscribes to the registry so a page that could not compile retries when the registry grows. Only thekind:'html'branch acts on that tick — every other branch rebuilds the same element type with the same props, which React reconciles in place, asreact-page-state.test.tsx(#2984) pins.On the strictness tradeoff
A stub has no
inputsyet, so its props surface asunknown-propwarnings rather than errors: the page compiles and renders, and the innerSchemaRenderertriggers the loader and swaps in the real block.That is the same call made in #2979 for the manifest generators, pointing the other way — and deliberately so. The runtime whitelist should be permissive, because the alternative is the whole page failing over a block that is genuinely part of the app. The build-time artifact must be strict, and
assertFullyLoadedenforces thatsdui.manifest.jsonis generated with every plugin eagerly loaded. So authoring-time prop validation keeps its fullinputs; only the in-browser render path relaxes.Tests
The first two fail against the old
page.tsx; the control passes throughout. Plus 6 unit tests for the new registry reads, including thatgetVersion()moves where a count would not.Verification
701 files passed | 1 skipped,8234 tests passed | 24 skipped.turbo run type-check: 76/76.lintoncore/components: 0 errors.changeset:check: clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01N4mrr1ihhwnfEHFSWmGoMp
Generated by Claude Code