fix(sdui): lazy public blocks reach a kind:'react' page scope; ReactRunner keeps its errors - #2976
Merged
Merged
Conversation
…unner keeps its errors (#2953, #2954) Two defects in the trusted `kind:'react'` page tier. objectui#2953 — the contract skipped lazily-registered blocks. `getPublicConfigs()` resolved every curated `PUBLIC_BLOCKS` tag through `getConfig()`, which reads loaded registrations only, so a block registered via `registerLazy()` was absent from the contract until its plugin chunk happened to be imported. In apps/console that silently dropped `object-kanban`, `object-calendar`, `object-gantt`, `object-timeline`, `object-map` and `markdown` from every react page's scope: `<ObjectKanban/>` threw a ReferenceError even though the tag is a first-class contract member, and whether it threw depended on load order. `getPublicConfigs()` now resolves pending lazy stubs too, returning them with `lazy: true` and no `component` (new `PublicComponentConfig` type). The injected wrapper renders through `SchemaRenderer`, which already triggers the loader and shows its placeholder, so the scope is complete at build time and never has to be rebuilt. `getConfig()` stays loaded-only by design — callers read `.component` off it — and now says so. objectui#2954 — ReactRunner discarded the errors it caught. `getDerivedStateFromProps` re-transpiled and re-evaluated the page source on every render and unconditionally set `error: null`. React runs it before the re-render that follows `getDerivedStateFromError`, so the boundary threw away the error it had just caught, rebuilt an identical throwing element, and the throw escaped past its own `fallback` to the renderer's generic panel; `onError` was gated on state that had already been cleared, and never fired at all for a compile-time error; and every eval minted a fresh page function — a new element type — remounting the subtree and wiping the page's `useState`. The transpile+eval is now memoised on `(code, scope)`, errors persist until those inputs actually change, and `onError` reports each error exactly once (including at mount). That also makes the scope's identity stability load bearing, which is why the react page deliberately does not subscribe to registry changes — the #2953 fix means it no longer needs to. Tests: `@object-ui/react-runtime` had none; adds a suite covering the boundary, recovery, onError, compile memoisation and mount stability (6 of 12 fail against the old runner). Adds lazy-contract cases to the registry's public-tier tests and an end-to-end lazy block to the react-page scope tests, and pins the "React page error" panel the negative control previously had to route around. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4mrr1ihhwnfEHFSWmGoMp
… tests The comments in react-page-scope.test.tsx cite react-page.tsx by line, and the previous commit shifted those lines. Re-point them at the current anchors. 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
|
os-zhuang
marked this pull request as ready for review
July 30, 2026 04:46
This was referenced Jul 30, 2026
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.
Fixes #2953, fixes #2954.
Two defects in the trusted
kind:'react'page tier. They're coupled — the second fix only works because of the first — so they land together.#2953 — the public contract skipped lazily-registered blocks
getPublicConfigs()resolved every curatedPUBLIC_BLOCKStag throughgetConfig(), which reads the eagercomponentsmap and never consultslazyEntries. So a block registered viaregisterLazy()was absent from the contract until its plugin chunk happened to be imported.In
apps/consolethat silently dropped sixPUBLIC_BLOCKStags from every react page's scope —object-kanban,object-calendar,object-gantt,object-timeline,object-map,markdown. Writing<ObjectKanban/>threwReferenceErroreven though the tag is a first-class contract member, and whether it threw depended on load order, so the same page could work in one session and fail in another.Took direction 2 from the issue (fix the contract surface, not just
buildComponentScope), since the same skew reaches every consumer of the public tier:Registry.getContractConfig(tag)— the loaded registration when there is one, otherwise the pendingregisterLazystub's metadata. It mirrorsregisterLazy's key derivation so the canonicaltypeused for dedupe matches whatregister()will store once the loader runs (and doesn't double-prefix a namespace onto an already colon-shaped curated tag likerecord:details).getPublicConfigs()now returnsPublicComponentConfig[]: same shape as before, but a not-yet-loaded entry comes back withlazy: trueand nocomponent. The lazytier:'public'opt-in is honoured too, mirroring the eager one.getConfig()is left alone — callers read.componentoff it, and a stub has no renderer. Its doc now states that split explicitly and points athasLazy/loadLazy.react-page.tsxneeded no logic change: the injected wrapper already defers toSchemaRenderer, which fires the loader, renders theLoading <tag>…placeholder, and re-renders on the registry's notify.#2954 — ReactRunner discarded the errors it caught
getDerivedStateFromPropsre-transpiled and re-evaluated the page source on every render and unconditionally seterror: null. React runs it before the re-render that followsgetDerivedStateFromError, so the boundary threw away the error it had just caught, rebuilt an identical throwing element, and the throw escaped past its ownfallback.The transpile+eval is now memoised on
(code, scope)— unchanged inputs returnnulland keep the existing state. One change, three fixes:fallbackis reachable again.ReactKindPage's styled "React page error" panel was dead code for render-phase errors; authors sawSchemaRenderer's genericComponent "home" failed to renderinstead.onErrorfires correctly. It was gated on anerrorthe nextgetDerivedStateFromPropshad already cleared. Now it reports on the transition (once per error, not once per render), andcomponentDidMountcovers compile-time errors — which previously never reported at all, sincecomponentDidUpdatedoesn't run for the first render.Pagefunction — a new element type — so recompiling per render remounted the page subtree and wiped itsuseState. That was the latent hazard flagged in the issue; memoising removes it.Errors are no longer sticky: new
code/scoperecompiles and clears them.Why the two are coupled
The #2953 fix means the scope is complete at build time, so it never has to be rebuilt — which is exactly why
ReactKindPagedeliberately does not subscribe toComponentRegistrychanges the waySchemaRendererdoes. If it did, a lazy plugin finishing registration would notify, rebuild the scope, change its identity, and (via #2954's memo key) remount every interactive react page on screen. Both call sites now carry that reasoning in comments.Tests
packages/react-runtimehad no tests at all. Adds 12 covering the boundary, recovery,onError, compile memoisation and mount stability. 6 of them fail against the old runner (verified by reverting the fix and re-running).packages/core— 6 new lazy-contract cases: stub included, load-order independence (same entry before and after the load), dual-key dedupe, colon-shaped tag, lazytier:'public'opt-in, andgetConfig()staying loaded-only.packages/components— an end-to-end lazy block through the real scope builder +SchemaRenderer(fails against the old registry), and the negative control now pins the "React page error" panel directly instead of routing around the ReactRunner re-transpiles on every render, discarding its own error state #2954 quirk it had to document.Verification
692 files passed | 1 skipped,8138 tests passed | 24 skipped.turbo run type-checkacross the repo and for@object-ui/console: clean.lintoncore/react-runtime/components: 0 errors.changeset:check: clean. Changeset included (@object-ui/core,@object-ui/react-runtime,@object-ui/components— patch).No manifest impact: both generators (
apps/console/dev/manifest-dump.tsx,packages/sdui-parser/scripts/gen-manifest.ts) import every plugin eagerly, so no lazy stubs exist at generation time. Were one to slip in,validateTreewould downgrade a hardunknown-componenterror to anunknown-propwarning for a block that genuinely is in the contract — the correct direction, and never stricter than before.🤖 Generated with Claude Code
https://claude.ai/code/session_01N4mrr1ihhwnfEHFSWmGoMp
Generated by Claude Code