在实现 objectui#3232(PR #3244,把 TextAreaField 的 mobile_fullscreen 收敛到唯一生产者)时,为了确认「保留下来的那条读取确实活着」而把生产链路走了一遍,发现对自动生成的字段这条链路是断的。不在 #3244 修 —— 修点在 packages/components / packages/plugin-form,#3244 的 scope 被限制在 packages/fields,且真正的修法要改公共契约(见下)。
链路
以一个 type: 'textarea' 的对象字段、表单开了 mobile: { fullscreenLongText: true } 为例:
plugin-form/src/ObjectForm.tsx:567 — 自动生成 FormField 时 type: mapFieldTypeToFormType(field.type),而 fields/src/field-type-alias.ts:21 把 textarea 映射为 'field:textarea'。
plugin-form/src/ObjectForm.tsx:584 — 同时把原始元数据挂在 field: field 这个槽上(注释:"Pass the original field metadata so widgets can access properties like precision, currency, etc.")。
plugin-form/src/ObjectForm.tsx:1043-1049 — mobile 那一步把 flag 盖在 FormField 自己身上:({ ...f, mobile_fullscreen: true })。没有盖到 f.field 上。
components/src/renderers/form/form.tsx:1719-1727 — 'field:textarea' 不在 BUILTIN_FIELD_TYPES 里,所以走已注册 widget 分支,解析到 field:textarea → TextAreaField。
components/src/renderers/form/form.tsx:1336 — 转发给 widget 的是 field: field.field || field。第 2 步让 f.field 存在,于是 widget 拿到的是原始元数据,而第 3 步的 flag 在 FormField 上,在这里被丢掉。
components/src/renderers/form/form.tsx:266 — 唯一还带着 flag 的载体(prop mobile_fullscreen)被 stripRegisteredFieldProps() 显式剔除。
结果:TextAreaField 三个可能的入口(field / schema / prop)全部拿不到 flag,全屏入口不渲染。
注意这不是 #3244 引入的:在 #3244 之前,四条读取里 ① 恒 undefined、③ 被剥离、④ 与 ② 同对象,所以那时同样是断的。#3244 只是把死读取删掉,没有改变这条链路的行为。
还活着的是哪几条
- 内置分支:
customFields 里手写 type: 'textarea'(不带 : 前缀)时 BUILTIN_FIELD_TYPES 命中,走 form.tsx:1761-1763 的 mobile_fullscreen || fullscreen + 它自己的 FullscreenTextarea —— 这条读的是 FormField props(未经 strip),正常。
- 已注册 widget:只有当 FormField 没有
.field 子对象时(手写 customFields 且 type: 'field:textarea'),field.field || field 落到 FormField 本身,flag 才到得了 TextAreaField。
也就是说:手写 customFields 可以工作,自动生成字段不行 —— 而自动生成正是绝大多数表单的路径。
证据等级
以上是对四处字面代码的静态推导(行号均针对 origin/main @ ca58e45),未做浏览器复现。落地修之前建议先补一个跑通 ObjectForm → form.tsx → widget 的集成用例把现状钉住 —— 现在整个 mobile.fullscreenLongText 特性没有任何集成测试,这也是它能一直断着没人发现的原因。
修的方向(需维护者定夺,不要照着猜)
两条路都动公共契约,所以这里只摆选项,不预设结论:
A. 生产者侧:ObjectForm 把 flag 盖到元数据载体上({ ...f, field: { ...f.field, mobile_fullscreen: true } }),让「元数据是唯一载体」这条约定真正成立。
B. 边界侧:form.tsx 停止剥离 mobile_fullscreen,把它作为显式声明的 widget prop 转发。
倾向 A:它让载体收敛为一个,而 B 会重新造出「prop 与元数据两套写法」——正是 AGENTS.md #0.1 和主令 #12 要避免的分叉。但 A 需要先回答「mobile_fullscreen 是不是字段元数据的一等属性」,这是 spec 层的决定,故不自行推进。
关联:objectui#3232、PR #3244。
在实现 objectui#3232(PR #3244,把
TextAreaField的mobile_fullscreen收敛到唯一生产者)时,为了确认「保留下来的那条读取确实活着」而把生产链路走了一遍,发现对自动生成的字段这条链路是断的。不在 #3244 修 —— 修点在packages/components/packages/plugin-form,#3244 的 scope 被限制在packages/fields,且真正的修法要改公共契约(见下)。链路
以一个
type: 'textarea'的对象字段、表单开了mobile: { fullscreenLongText: true }为例:plugin-form/src/ObjectForm.tsx:567— 自动生成 FormField 时type: mapFieldTypeToFormType(field.type),而fields/src/field-type-alias.ts:21把textarea映射为'field:textarea'。plugin-form/src/ObjectForm.tsx:584— 同时把原始元数据挂在field: field这个槽上(注释:"Pass the original field metadata so widgets can access properties like precision, currency, etc.")。plugin-form/src/ObjectForm.tsx:1043-1049— mobile 那一步把 flag 盖在 FormField 自己身上:({ ...f, mobile_fullscreen: true })。没有盖到f.field上。components/src/renderers/form/form.tsx:1719-1727—'field:textarea'不在BUILTIN_FIELD_TYPES里,所以走已注册 widget 分支,解析到field:textarea→TextAreaField。components/src/renderers/form/form.tsx:1336— 转发给 widget 的是field: field.field || field。第 2 步让f.field存在,于是 widget 拿到的是原始元数据,而第 3 步的 flag 在 FormField 上,在这里被丢掉。components/src/renderers/form/form.tsx:266— 唯一还带着 flag 的载体(propmobile_fullscreen)被stripRegisteredFieldProps()显式剔除。结果:
TextAreaField三个可能的入口(field/schema/ prop)全部拿不到 flag,全屏入口不渲染。注意这不是 #3244 引入的:在 #3244 之前,四条读取里 ① 恒
undefined、③ 被剥离、④ 与 ② 同对象,所以那时同样是断的。#3244 只是把死读取删掉,没有改变这条链路的行为。还活着的是哪几条
customFields里手写type: 'textarea'(不带:前缀)时BUILTIN_FIELD_TYPES命中,走form.tsx:1761-1763的mobile_fullscreen || fullscreen+ 它自己的FullscreenTextarea—— 这条读的是 FormField props(未经 strip),正常。.field子对象时(手写customFields且type: 'field:textarea'),field.field || field落到 FormField 本身,flag 才到得了TextAreaField。也就是说:手写 customFields 可以工作,自动生成字段不行 —— 而自动生成正是绝大多数表单的路径。
证据等级
以上是对四处字面代码的静态推导(行号均针对
origin/main @ ca58e45),未做浏览器复现。落地修之前建议先补一个跑通ObjectForm→form.tsx→ widget 的集成用例把现状钉住 —— 现在整个mobile.fullscreenLongText特性没有任何集成测试,这也是它能一直断着没人发现的原因。修的方向(需维护者定夺,不要照着猜)
两条路都动公共契约,所以这里只摆选项,不预设结论:
A. 生产者侧:ObjectForm 把 flag 盖到元数据载体上(
{ ...f, field: { ...f.field, mobile_fullscreen: true } }),让「元数据是唯一载体」这条约定真正成立。mobile_fullscreen从此只有一个合法位置,写错位置的 flag 不会有任何路径悄悄接住。mobile_fullscreen目前没有出现在@object-ui/types的字段元数据类型上(FormField那侧是as FormField硬转进去的),要补声明;若要做到「declared = enforced」,还得决定它属不属于@objectstack/spec的字段元数据。B. 边界侧:
form.tsx停止剥离mobile_fullscreen,把它作为显式声明的 widget prop 转发。TextAreaField读取的mobileFullscreenprop 全仓无人生产,注释却说"宿主表单传入" #3232 的裁决明确说了「不要凭空发明没有生产者的键」—— 这条路要成立,必须同时在FieldWidgetComponentProps上正式声明它、并说清它与元数据 flag 的优先级。两个载体并存正是TextAreaField读取的mobileFullscreenprop 全仓无人生产,注释却说"宿主表单传入" #3232 要消灭的东西。倾向 A:它让载体收敛为一个,而 B 会重新造出「prop 与元数据两套写法」——正是 AGENTS.md #0.1 和主令 #12 要避免的分叉。但 A 需要先回答「
mobile_fullscreen是不是字段元数据的一等属性」,这是 spec 层的决定,故不自行推进。关联:objectui#3232、PR #3244。