Skip to content

fix(site): SchemaNode crosses to SchemaRenderer through the bridge — the docs site builds again (#4617) - #4621

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4617-site-schemanode-bridge
Aug 14, 2026
Merged

fix(site): SchemaNode crosses to SchemaRenderer through the bridge — the docs site builds again (#4617)#4621
yinlianghui merged 1 commit into
mainfrom
claude/issue-4617-site-schemanode-bridge

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Fixes #4617.

main is red and has been since PR #4608 merged: Build Docs runs pnpm turbo run build --filter='@object-ui/site', next build type-checks the site, and the site's five SchemaRenderer call sites stopped compiling the moment SchemaNode became a union. Every open PR inherits it.

⚠️ Note on one character in the quoted output below. GitHub's body sanitizer strips a < immediately followed by a letter, treating it as an HTML tag — it would delete < SchemaRenderer … /> and turbo's < NONEXISTENT > marker outright. Every such token here therefore carries one space after the <, following PR #4608's convention. The space is an artifact of the sanitizer, not part of the real text.

Red first — measured against origin/main, not inherited from the card

CI, run 31740683664 (main push, c1d939f7f = the merge of #4608), job Build Docs, step Build Site. The immediately preceding main run 31736774667 was green:

  Creating an optimized production build ...
✓ Compiled successfully in 40s
  Running TypeScript ...
app/components/InteractiveDemo.tsx(74,43): error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'.
  Type 'number' is not assignable to type 'string | BaseSchema | null | undefined'.
app/components/InteractiveDemo.tsx(119,35): error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'.
  Type 'number' is not assignable to type 'string | BaseSchema | null | undefined'.
app/components/LiveSplitDemo.tsx(286,37): error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'.
  Type 'number' is not assignable to type 'string | BaseSchema | null | undefined'.
app/components/SchemaThumbnail.tsx(138,35): error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'.
  Type 'number' is not assignable to type 'string | BaseSchema | null | undefined'.
app/playground/page.tsx(1413,39): error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'.
  Type 'number' is not assignable to type 'string | BaseSchema | null | undefined'.
Failed to type check.

Reproduced locally on a worktree at origin/main — same five, byte-identical:

$ pnpm --filter @object-ui/site types:check
app/components/InteractiveDemo.tsx(74,43): error TS2322: Type 'SchemaNode' is not assignable to type 'string | BaseSchema | null | undefined'.
  Type 'number' is not assignable to type 'string | BaseSchema | null | undefined'.
… (identical for 119,35 · LiveSplitDemo 286,37 · SchemaThumbnail 138,35 · playground 1413,39)
 ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL  @object-ui/site@3.1.0 types:check
Exit status 2

One correction to the card, kept rather than quietly fixed. #4617's body transcribes the fourth error as SchemaThumbnail.tsx(145,35). Both the CI log and the local reproduction say 138,35; apps/site has not changed since c1d939f7f (git log c1d939f7f..HEAD -- apps/site is empty), so 145 is not a stale line number — most likely it came from the filer's own #4615 branch, which edits that file. The other four line numbers match exactly.

The fix — the bridge, at five sites

All five values are typed SchemaNode from @object-ui/core, which since #4608 re-exports @object-ui/types' union BaseSchema | string | number | boolean | null | undefined. SchemaRenderer's prop is narrower on purpose (SchemaRenderer.tsx:226): schema: BaseSchema | string | null | undefined. The sanctioned crossing is toRenderableSchema, which #4608 explicitly preserved for this — quoting its own must-not-change section:

toRenderableSchema in packages/react remains. SchemaRenderer's component-level union deliberately excludes number/boolean (#4548 ruling Q2), so the bridge still normalizes those after reconciliation.

It is imported from @object-ui/react's published surface — packages/react/src/index.ts:10 is export * from './schema-input', so no deep import is involved.

# File : line Expression Was
1 app/components/InteractiveDemo.tsx:74 toRenderableSchema(example.schema) example.schema
2 app/components/InteractiveDemo.tsx:119 toRenderableSchema(schema) schema
3 app/components/LiveSplitDemo.tsx:286 toRenderableSchema(lastValidSchema) lastValidSchema
4 app/components/SchemaThumbnail.tsx:138 toRenderableSchema(schema) schema
5 app/playground/page.tsx:1413 toRenderableSchema(schema) schema

Plus one import line per file (four files). No prop widening in @object-ui/react, no as cast, and toRenderableSchema's implementation is untouched — this PR consumes it.

must-not-change: rendered behavior, and the primitive is genuinely reachable

The bridge is identity for every object schema and for null/undefined; it maps only number/boolean to their text form. That this changes nothing is read from the renderer, not assumed — SchemaRenderer.tsx:483 and :489:

  if (typeof evaluatedSchema === 'string') return < >{evaluatedSchema}< / >;
  // Any other primitive that reached here renders as its text too
  // (objectui#4548). …
  if (typeof evaluatedSchema !== 'object') return < >{String(evaluatedSchema)}< / >;

So 42 took the second branch and rendered String(42); bridged, it takes the first branch as "42". Same text node either way. The two branches are the same function composed in the other order.

Two of the five sites can really receive a bare primitive today, so this is not a formality:

  • LiveSplitDemo.tsx:153setLastValidSchema(parsed as SchemaNode) where parsed = JSON.parse(text) from the live-editable textarea. Typing 42 in that box is valid JSON.
  • playground/page.tsx:1244setSchema(parsed), same shape, from the playground editor.

The other three (InteractiveDemo's two props, SchemaThumbnail's prop) are authored by MDX/gallery callers, where a bare string is the plausible input and passes through untouched. The bridge is total at all five, which is why the ruling's "still the right spelling" holds even where a primitive is unlikely.

The gate gap — why this reached main green

apps/site's type-check script was named types:check, but the turbo task is type-check. Turbo therefore listed the package in the graph with no command to run:

  site entry: @object-ui/site#type-check | command: '< NONEXISTENT >'

Renamed to type-check (nothing else in the repo referenced the old name — one grep hit, its own definition). Measured with turbo run type-check --dry=json, counting entries whose command is not the < NONEXISTENT > marker:

Filter Graph entries Runnable before Runnable after apps/site runs
...@object-ui/react 70 (unchanged) 67 68 no → yes
...@object-ui/types 80 (unchanged) 77 78 no → yes

Those before-counts are worth a second look: 67 and 77 are exactly the "67 / 67 successful" and "77 / 77 successful" that PR #4608's own canary table reported as full green. The canary was honest about what it ran; apps/site simply was not in it, in either direction.

scripts/check-type-check-coverage.mjs carried @object-ui/site in CHECKED_BY_OWN_BUILD. That gate's own ratchet (line 532) requires the entry to die once the package gains a type-check script, so it is deleted and the table is now empty, with a comment recording why it should stay that way:

before: ✅  type-check coverage: 44/46 via `type-check`, 1 via their own build, 0 known-broken (0 errors outstanding), 1 not compiled.
after:  ✅  type-check coverage: 45/46 via `type-check`, 0 via their own build, 0 known-broken (0 errors outstanding), 1 not compiled.

The retired entry's own caveat had predicted this failure in the abstract — "a PR that only touches a workspace package in transpilePackages therefore does not re-check the site until it lands" — and called it a cost/coverage call. #4608 is that PR, and the answer turned out to be ~5 hours of red main. The exemption was accurate about what checked the site and silent about when; "when" was the half that mattered. verifyNoIgnoreBuildErrors retires with it rather than being orphaned: it protected a coverage claim that rested on next build, and coverage no longer rests there.

The remaining "1 not compiled" is @object-ui/example-hello-world, unchanged. (#4617's second half describes apps/site as that "1 not compiled" package; it was in fact the "1 via their own build". No consequence for the fix.)

Green

$ pnpm --filter @object-ui/site type-check
> fumadocs-mdx && next typegen && tsc --noEmit
[MDX] generated files in 20.86880099999999ms
[MDX] generated files in 13.919562000000042ms
Generating route types...
✓ Types generated successfully
### exit: 0 ###

Build Docs' exact step, run verbatim on this branch:

$ pnpm turbo run build --filter='@object-ui/site' --concurrency=2
@object-ui/site:build: Route (app)
@object-ui/site:build: ┌ ○ /
@object-ui/site:build: ├ ○ /_not-found
…
@object-ui/site:build: └ ○ /playground

 Tasks:    29 successful, 29 total
Cached:    28 cached, 29 total
  Time:    1m10.29s
### exit: 0 ###

And the newly-aligned turbo task, which now actually executes rather than being skipped:

$ pnpm exec turbo run type-check --filter='...@object-ui/react' --concurrency=2
@object-ui/site:type-check: cache miss, executing 3964711d030aa302
@object-ui/site:type-check: > fumadocs-mdx && next typegen && tsc --noEmit
@object-ui/site:type-check: ✓ Types generated successfully
 Tasks:    68 successful, 68 total
Cached:    34 cached, 68 total
  Time:    3m18.652s
### exit: 0 ###

cache miss, executing is the line that matters: before this PR that package contributed no task to run at all.

Verification

Check Result
check-control-bytes.mjs PASS — 4393 tracked text files scanned
check-phantom-dependencies.mjs PASS — every in-scope import declared
check-changeset-presence.mjs PASS — 6 files changed, 0 under a released package's src/, 0 changesets owed
check-changeset-no-major.mjs PASS
check-changeset-fixed.mjs PASS
check-type-check-coverage.mjs PASS — 45/46 (was 44/46)
check-lint-coverage.mjs PASS — 46/46
check-doc-links.mjs PASS — 13 scan roots
vitest run scripts/ 42 files, 957 tests passed — includes the #3904 site-host guard and this gate's own suite
eslint, 4 touched files 4 problems (0 errors, 4 warnings) on both sides — byte-identical to origin/main in a compare worktree; net zero

Control-byte self-scan (grep -naP over the widened class, not just NUL) clean on all six touched files.

CI on this branch — converged

All 18 check runs completed; 16 success, 2 skipped by their own conditions (dependabot, Test (coverage)). The headline:

Job Conclusion
Build Docs success — the job that has been red on every main push since #4608
Type Check success — and it now reaches apps/site for the first time
Lint success
Changeset Declaration success
Build & E2E · Test shards 1-4/4 success
Control Byte Scan · Internal Docs Link Check · Changeset Fixed Group · Skill Guide Path success

Changesets

None, and the gate agrees rather than being overridden:

Compared the working tree with 8640cec19 (merge-base with origin/main): 6 file(s) changed,
0 of them under the src/ of a package the release covers, 0 under a package changesets
ignores, 0 changeset(s) added.
✅  No source of a released package changed in this range, so no changeset is owed.

apps/site is "private": true and publishes nothing; the script rename and the gate-table edit are tooling. Never major.

No skip-changeset label is applied, deliberately — that label does not exist in this repo. changeset-presence.yml runs node scripts/check-changeset-presence.mjs with no label escape hatch, and scripts/__tests__/ci-cd-pipeline-doc.test.ts:184 records that a "changeset gate skippable with a skip-changeset label" was one of the five phantom workflows objectui#3724 deleted — "neither the workflow nor the label was ever real". Applying it here would be re-creating that phantom.

Scope

packages/react untouched — prop types and toRenderableSchema's implementation alike. packages/plugin-dashboard/src/** untouched (#4614). The five sites are fixed as they exist on origin/main; #4615's branch was not pulled. content/docs/releases/** untouched. No git stash at any point.


Generated by Claude Code

…the docs site builds again (#4617)

`Build Docs` (`next build`, which type-checks apps/site) has failed on every
push to `main` since PR #4608 landed, with 5x TS2322 at the five call sites
that hand a `SchemaNode` to `SchemaRenderer`'s `schema` prop.

Route all five through `toRenderableSchema`, the adapter #4548-Q2 kept for
exactly this crossing: `SchemaNode` deliberately includes `number`/`boolean`,
SchemaRenderer's component union deliberately excludes them. Not a prop
widening, not a cast.

Also close the gate gap that let this reach `main` green: apps/site's
type-check script was named `types:check`, so `turbo run type-check` listed it
as `<NONEXISTENT>` and never ran it. Renamed to `type-check`; its
CHECKED_BY_OWN_BUILD exemption retires with the rename, as that gate's own
ratchet requires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 14, 2026 1:41am

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

main is RED: Build Docs fails on SchemaNode vs SchemaRenderer's schema prop — the docs site has not built since #4608 landed

2 participants