Uh oh!
There was an error while loading. Please reload this page.
fix(components): a form field resolves a field widget or the default input — never a ui-namespace SDUI node renderer - #5326
Merged
os-support-ai merged 2 commits intoAug 19, 2026
Conversation
…amespace SDUI node renderers A form field's `type` now resolves a `field:`-namespaced widget or takes the builtin `default` input branch — it no longer falls back to whatever holds the bare name in any namespace. Maintainer ruling of 2026-08-19 on objectui#5254 (option B). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
…minor) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-support-ai
marked this pull request as ready for review
August 19, 2026 15:03
os-support-ai
commented
Aug 19, 2026
CollaboratorAuthor
ACCEPT — PM review, round 16.
Merging. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
os-support-ai
deleted the
claude/issue-5254-form-field-crossnamespace-fallback
branch
August 19, 2026 15:04
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 freeto 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#5254
Implements the maintainer ruling of 2026-08-19 (verbatim 「全部接受」), option B: form-field type resolution stops falling back to
ui-namespace SDUI node renderers.The located surface
The claim deliberately left the file surface unstated. Located by reading, it is one function in one file:
packages/components/src/renderers/form/form.tsx—renderFieldComponent, plus thedefaultarm of its field switch.That is the only site in the repo doing this resolution: a
ComponentRegistry.geton afield:-prefixed key appears exactly twice acrosspackages/**, both in this file (the other isresolvesToRegisteredFieldWidget, which only mirrors the rule and already spelled it the way the ruling prescribes). No overlap with the in-flight #5269 (packages/plugin-view), #5299 (packages/plugin-dashboard) or #5257 (packages/core).Re-derived on current
origin/mainfirstThe fallback still existed and still had the shape the card recorded. Measured at
3fbbea1f3, built-in path (noregisterAllFields()), field{ name: 'contact', type: 'email', max_length: 50 }:Two things the card recorded, and a third it did not: because the
uinode renderer draws its own label, the control got a secondlabelelement on top of the form's own — two labels naming one control.I also measured how wide the fallback actually was. It answered 126 bare names on the built-in path (
div,h1,card,button,form,alert,badge, the displaytextwidget …), and 116 with the fields package registered too.The change
A form field's
typenow resolves afield:-namespaced widget or takes the builtindefaultinput branch. The|| ComponentRegistry.get(type)tail is gone, and with it the whole "non-field component reached through the bare-name fallback" branch — sorenderFieldComponentnow dispatches to exactly one contract (FieldWidgetComponentProps). A colon-qualified type resolves only when it names thefieldnamespace, closing the same hole forui:emailrather than only the bare spelling.A spelling that resolved yesterday stops resolving. A form field whose
typenames a non-field component renders the default input instead of that component. That is the substance of the ruling, not a side effect of it, and the changeset isminorfor that reason (nevermajor— objectui's major is pinned to@objectstack's).Not softening it, but scoping it honestly: with
registerAllFields()— the production configuration — every affected type already resolved its ownfield:widget, and object-derived forms go throughmapFieldTypeToFormType, which has always emitted thefield:-prefixed id. Rendering these components as top-level SDUI nodes is untouched; this rule governs field resolution only.The verification the ruling put on the implementer
Measured answer: on its own, it does not.
inputTypeon that branch is whatever the author wrote, and a plain{ name, type: 'password' }authors none — so the branch renderedtype="text"and put a secret on screen in clear text. That is the "worse than the leak" outcome, and it is why this PR also addsNATIVE_INPUT_FIELD_TYPESto the default branch:emailandpasswordare declared field types in@object-ui/types(EmailFieldMetadata/PasswordFieldMetadata) and keep the native input they always rendered. An explicitly authoredinputTypestill wins.So the visible rendering of an
email/passwordfield is unchanged; what goes away is the leak, the duplicate label and the dead ceiling. After the fix, same field as above:The table is deliberately those two only. Other declared types with a native HTML equivalent (
url,phone,number,color,date) already took this branch astype="text"and are untouched — widening it would change fields this card neither moved nor measured.Reverse verification — predicted before running, both legs
Fix committed first, then reverted, so both legs ran from a known state.
Leg 1 — restore the bare-name fallback, keep all tests. Predicted 8 red, named in advance; observed exactly those 8, no others:
fieldmetadata objectmax_length, ceiling capscard)inputTypewinsLeg 2 — revert only
NATIVE_INPUT_FIELD_TYPES. Predicted 2 red; observed exactly 2, with the failure text that is the ruling's answer in one line:Assertions green on BOTH legs — pinning nothing on their own, said rather than counted: the registry-reading premise test,
still resolves a field: widget,ui node renderers still reachable as NODES,builtin type on its own branch, and (leg 1 only) the three "usable input" assertions, since the old fallback also produced a native email/password input. They are counter-probes and routing guards, not evidence of the fix.Every zero is counter-probed with a known-present neighbouring term —
ComponentRegistry.get('form')/get('card')/get('barenameprobe')/get('plaindisplay')are asserted truthy next to eachtoBeUndefined()/toBeNull(), so no assertion can pass on an empty registry.Build artifact between the edit and the thing under test
None for the tests. The root
vitest.config.mtsaliases every@object-ui/*specifier to that package'ssrc/, so vitest reads the edited source directly; nodist/sits in between. A build is required fortype-check(it reads dependencies'.d.ts), and the first run in this fresh worktree failed withTS2307: Cannot find module '@object-ui/core'for that reason alone. Afterpnpm --filter '@object-ui/components^...' buildit passes.Fixture triage
Two existing tests pinned the removed branch and were rewritten rather than re-spelled, because their assertions passed precisely because the fallback existed:
form-field-carrier.test.tsx— the block asserting a bare-name component receives the universalschemanode. Correct about a rule that no longer exists; now asserts the field is not routed there at all, and takes the builtin input.form-readonly-host-group.test.tsx— kept [fields] 非 group-labelled widget 的只读替换显示丢掉 host 下发的整份 plumbing —— label 的for悬空、description 零消费者(email / url / phone 实测) #4788's own assertion (no group wrapper for a non-field-widget) and replaced only the half that asserted the field reached the bare-name probe.Both keep a counter-probe showing the component is still registered and still resolvable — it is simply no longer reachable as a field.
Tests (all from repo root, canonical
pnpm exec vitest run+ the file path; no--)Run on the final commit
e20ea0d22:packages/components/src/renderers/form+ the two form-renderer suitespackages/components(full)packages/plugin-form+packages/plugin-detailpackages/plugin-viewplugin-listreactlayoutfieldspackages/app-shellpnpm --filter @object-ui/components type-checkcheck:control-bytes/check:self-import/check:esm-specifierseslinton the changed filesany-count unchanged at 45)Vitest was run from the repository root every time —
packages/componentsowns a standalonevitest.config.tsand is one of the 11 packages objectui#5313 measuredassertCanonicalVitestInvocationas NOT covering, so the guard was not relied on.Filed separately, not fixed here
#5322 — a
field:-prefixedpassword/emailfield (whatmapFieldTypeToFormTypeemits) renderstype="text"when its widget is not registered, showing a secret in clear text. Measured as pre-existing and unchanged by this PR on both sides of the change. Left to its own card exactly as the ruling directs, rather than widened into this one.Generated by Claude Code