Uh oh!
There was an error while loading. Please reload this page.
refactor(types,react,components,fields)!: converge the widget metadata carrier to field, retiring schema (#3233) - #3296
Merged
Conversation
…a carrier to `field`, retiring `schema` (#3233) `schema` was a second carrier for what `field` already means. Two producers fed it — `SchemaRenderer` passed the authored node as `schema`, and the form renderer's `renderFieldComponent` passed `schema={props.field || props.schema || props}` alongside `field` — so ~30 widgets resolved their config as `field || schema`: one concept, two spellings, a de-facto second contract (AGENTS.md #0.1). Producer-side convergence, per the 2026-08-03 maintainer ruling (route B, landed in v17 rather than behind a deprecation window): - `FieldWidgetComponentProps` no longer declares `schema`; reading `props.schema` is a compile error. - The form renderer passes `field` only when the resolved entry is a `field:` widget. It still passes `schema` when the bare-name fallback answers with a plain SDUI component (the display `text` widget, `alert`, …) — that key is the universal node contract and is NOT retired. - The SDUI node → `field` translation happens exactly once, in a new `withFieldCarrier` adapter that every built-in registration goes through. It is exported so out-of-repo widgets can reach the same guarantee instead of re-growing a `field || schema` read. - All `field || schema` reads in `@object-ui/fields` become plain `field` reads. Payload equivalence is pinned by object identity on both paths: `form-field-carrier.test.tsx` (form) and `field-carrier-sdui.test.tsx` (SDUI). Closes#3233 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NVPjPzmmAJ2Ngtvgg5MSRa
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
xuyushun441-sys
marked this pull request as ready for review
August 3, 2026 12:56
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 3, 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 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.
Closes#3233
按 2026-08-03 维护者裁决走路线 B 直接落 v17:生产者侧收敛到
field,从 widget 契约删除schema,删掉全部消费端field || schema容忍。不做弃用期。前提核对(实施前)
FieldWidgetComponentProps上schema仍是已声明键packages/fields/src/widgets/types.ts,#3221/PR #3230 封口的那版)field || schema读法仍存在field || (props as any).schema,只有 4 处写成字面field || schema,所以按字面量 grep 会严重低估)两条生产者路径
1. 表单路径 ——
renderFieldComponent(packages/components/src/renderers/form/form.tsx)原状
schema={props.field || props.schema || props}与field同时下发。唯一调用点无条件设field: field.field || field(永远真值),所以链上后两项不可达,schema恒=== props.field。改为只传field。但这里有一个 issue 未提及、且必须区分的事实(本单最大的一个坑,被
form-write-error-message.test.tsx抓了个正着):renderFieldComponent的注册表查找会命中两类不同的组件——field:<type>命中 → field widget,契约是FieldWidgetComponentProps,载体field;text、alert、badge…),契约是SchemaRenderer给每个注册组件的通用schema节点。这个键不在本次退役范围内。无差别删掉
schema会让后一类渲染成undefined.className直接崩。所以查找被拆成两步并显式判定isFieldWidget,只在 field widget 分支省略schema,兜底分支逐字保留原解析。这条区分本身有专门测试钉住(见下)。2. SDUI 路径 —— 注册适配器(
packages/fields/src/withFieldCarrier.tsx,新增)SchemaRenderer没有 field-widget 专用分支:它给所有注册组件统一传schema。而registerField()是把 widget 裸注册进field:<type>(及裸名),所以 SDUI 节点走到 widget 时只有schema没有field—— 这才是那 32 处field || schema里schema分支的真实来源。翻译点收在唯一的接缝上:
withFieldCarrier,registerField()/capability-multiselect全部经它注册。它按引用转发节点(不拷贝、不裁剪、不改名),并吃掉schema(契约里没有这个键了,继续转发只会随 widget 的...props展开变成 DOM 上的野属性)。withFieldCarrier已导出为公共 API —— 这是仓外 widget 作者的迁移工具:裸注册的第三方 widget 在 SDUI 路径上拿不到field,若不给它这个适配器,它们只能自己重新长出一个(props as any).schema读法,等于把刚删掉的容忍原样搬到仓外。载荷等价性证明
不变量:过去经
schema送达的每条载荷,必须以field送达完全相同的对象。两条路径各有一个按对象同一性(不是结构相等)断言的测试 —— 结构相等在渲染器改为下发重建副本时依然会绿,而副本会打断按 metadata 引用做记忆化的 widget。packages/components/src/renderers/form/__tests__/form-field-carrier.test.tsxprops.fieldtoBe作者写下的那个 metadata 对象(含.field已声明槽与无.field的内联 config 两种形态)packages/fields/src/__tests__/field-carrier-sdui.test.tsxSchemaRenderer实际传下来的schema对象,再转发给被测适配器;断言 widget 的fieldtoBe那个对象(不能对作者字面量断言 ——SchemaRenderer求值时会浅拷贝节点)另外钉住的:
'schema' in props === false(键缺失,不是undefined—— 否则field || schema在实践中依然可解,类型说没了而运行时还在);schema:键也无法把第二载体从元数据侧复活;schema(上面那条区分的反向断言);registerField()真的过了适配器 —— 用真SchemaRenderer+ 真BooleanField端到端跑:节点上的widget: 'checkbox'/name只能经载体到达,能选中 Checkbox 而非 Switch 且 id 正确,就证明节点完整落到了field;props.schema现在是编译错误(widget-props-contract.test.tsx/TextAreaField.mobileFullscreen.test.tsx各加一条@ts-expect-error)。每条新增/改动的测试都做了 sabotage 验证(改坏源码或断言 → 看它红 → 还原 → 看它绿),共 6 次:还原旧的双传(2 条红)、把
field改成下发副本(2 条红)、适配器改为转发schema(1 条红)、适配器改为下发副本(1 条红)、registerField跳过适配器(1 条红,症状正是「渲染成 Switch,找不到 checkbox」——即本次收敛被指控的静默失效)、isFieldWidget恒真(1 条红)。迁移(面向 widget 作者,宿主元数据不受影响)
SDUI JSON / 对象元数据零改动。变的只是 widget 的编写契约:
仓外 widget 若继续读
props.schema且未经适配器重新注册,在 v17 会读到undefined并静默渲染空态 —— 该风险维护者已知并接受,缓解是大版本边界 + 变更集里的高声量迁移说明。变更集
.changeset/field-widget-single-metadata-carrier.md,含完整迁移说明。minor而非major,尽管这是破坏性变更:AGENTS.md「版本号策略」明令 objectui 的 changeset 不得声明major(fixed 组任一 major 会把 39 个包整体推上 18,脱离@objectstack的节奏),破坏性语义写在正文里。这一条由scripts/check-changeset-no-major.mjs+changeset-guard.yml机械强制;本地两个 guard 均已跑过通过。文档
content/docs/guide/plugin-development.md与skills/objectui/guides/plugin-development.md:删掉schema作为已声明 pass-through 键的说法,新增「field是唯一载体(v17,breaking)」小节 +withFieldCarrier注册示例。未触碰content/docs/releases/。验证
vitest run packages/fields packages/componentsvitest run packages/plugin-form packages/plugin-grid packages/plugin-detail packages/reactvitest run packages/app-shell packages/plugin-designer packages/sdui-parser packages/plugin-reportvitest run packages/core packages/layout packages/mobile packages/plugin-dashboard packages/plugin-list packages/plugin-kanban packages/plugin-calendar packages/providers packages/runner packages/i18nturbo run type-check(fields / components / react / plugin-form / app-shell)turbo run lint(fields / components / react)turbo run build(fields / components)测试一律从仓根用
pnpm exec vitest run <paths>跑(#3288:pnpm --filter <pkg> test -- --run <paths>会静默忽略路径过滤)。范围外发现
pnpm docs:check-linksreports a broken link and runs in no workflow (content/docs/core/enhanced-actions.mdx → /docs/components/form) #3295 ——pnpm docs:check-links在干净的main上就报一条坏链接(content/docs/core/enhanced-actions.mdx → /docs/components/form),且该脚本没有挂进任何 workflow。已git stash验证与本 PR 无关,单独开 issue(未指派),未并入。未触碰 #3291(
toDomProps)与 #3290(aria-required)。Generated by Claude Code