Skip to content

fix(layout): page-header 注册补 isContainer: true,校验器不再对文档承诺的 children 写法报 not-a-container (#3900) - #3973

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3900-pageheader-iscontainer
Aug 9, 2026
Merged

fix(layout): page-header 注册补 isContainer: true,校验器不再对文档承诺的 children 写法报 not-a-container (#3900)#3973
yinlianghui merged 1 commit into
mainfrom
claude/issue-3900-pageheader-iscontainer

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3900

按维护者 2026-08-09 的 A 案裁定(issue 评论 5230001265)执行:packages/layout/src/index.tspage-header 注册补 isContainer: true

为什么补 flag,而不是反过来退掉 children

PageHeader 一直有意schema.children 渲染进右侧动作槽(packages/layout/src/PageHeader.tsx:182:207),record:quick_actions 嵌在 page:header.children 下就是靠它;content/docs/layout/page-header.mdx 把该槽的优先级(action → React children → actions → schema children)写成公开契约;该文档页唯一的 live demo(examples/schema-catalog/src/schemas/layout-page-header/pageheader-with-actions.json)正是这个形状,而且实测正常渲染。

漏掉 isContainer 从来没有挡住任何渲染 —— 渲染路径根本不读它。它的消费者在别处:sdui-parsernot-a-container 诊断、Studio 调色板元数据、react-page 标签表。所以真正的后果是校验器在说谎:作者照文档写出能正常渲染的 schema,却拿到一条 "page-header does not accept children";信了这条 warning 去掉 children,右槽就空掉。而会说谎的 warning 比缺一条更贵 —— 它训练作者(尤其 AI 作者)连真实的 not-a-container(那些确实不收子节点的组件)一起无视。

这不是在 spec 之外新开作者面:children 是 objectui JSON 协议里每个节点的基础属性(packages/sdui-parser/src/validate.tsBASE_PROPS 把它和 type / id / className 并列),不是 PageHeaderProps 的键。所以这个 flag 回答的是协议层面的「该节点是否接受子节点列表」,而对这个组件,答案一直是「是」。

改动

  • packages/layout/src/index.ts:page-header 注册补 isContainer: true,并把该注册块的缩进/尾逗号对齐同文件其它容器组件(原来是 6 空格 / 10 空格,邻居都是 2 空格步进)。注释写清「为什么这不是扩 spec 面」,免得下一个读者把它当越界。
  • examples/schema-catalog/test/pageheader-with-actions.test.tsx:模块头原来有一句「注册没有声明 isContainer: true」—— 本 PR 把这句话变成假的,所以必须改;顺带在这里补上两个方向的钉子(见下)。
  • packages/layout/src/__tests__/page-header-authorable-keys.test.tsx:声明面的注册级断言(bare 键与 namespaced 键各一条),与同文件既有的 inputs 收窄断言同处一室 —— 那条管「不要多声明」,这条管「不要否认」。
  • examples/schema-catalog/package.json:新增 devDependency @object-ui/sdui-parser(测试要真的跑那条诊断)。
  • changeset:@object-ui/layout patch。

两个方向的钉子

  1. 文档 demo 走应用真实构建的 manifest 后不再产生 not-a-container manifest 按 packages/components/src/renderers/layout/page.tsxgetJsxManifest() 同样的方式构造(getKnownTypes() + getMeta()),不是 getAllConfigs() —— 后者的 .type 恒为 namespaced 形式,bare 的 page-header 标签根本不在 manifest 里,断言会以 unknown-component 的方式假绿。所以这条钉子先断言「没有 unknown-component」且「demo 确实带 children」(诊断分支由 node.children?.length 守卫),再断言「没有 not-a-container」—— 缺席只有在证明检查真的跑过之后才算证据。
  2. 阳性对照:一个真正不收子节点的组件带 children 时诊断照旧触发。 用同一个注册文件里的 navigation-renderer(完全由 items 驱动,从不读 schema.children)。少了这条,把诊断整个弄哑、或者让 isContainer 默认为真,钉子 1 都会继续绿 —— 那就等于什么都没量。

未改动 / 已核对

  • canonical 的 page:header(@object-ui/components)刻意不动。 那个渲染器(containers.tsxPageHeaderRenderer)完全不读 schema.children(该范围内 grep children 零命中),所以它没有 isContainer正确的,不是同一个 bug。而且它在 PUBLIC_BLOCKS 里,给它加 flag 会把它从每个 kind:'react' 页面的组件作用域里剔掉(react-page.tsx:66 跳过容器)—— 那是另一件事,需要另一次裁定。
  • 不需要重新生成任何生成物。 codegen(packages/sdui-parser/scripts/gen-manifest.ts)只序列化 getPublicConfigs(),即 PUBLIC_BLOCKS 加显式 tier:'public' 的集合;legacy kebab 键 page-header 两者都不属于(PUBLIC_BLOCKS 里是 page:header,注册也没有 tier),而且 sdui.manifest.json / sdui-intrinsics.d.ts / sdui-blocks.md 三个产物本仓一个都没入库(git ls-files 无命中),都是构建期写到 outDir。
  • 消费半径已扫。 isContainer 的读点:sdui-parser/src/validate.ts:89components/.../react-page.tsx:66components/.../page.tsx:464;app-shellPageBlockCanvas.tsx:683 是同名局部变量,与注册元数据无关。packages/components/src/__tests__/react-page-scope.test.tsx 那条 isContainer 断言只覆盖 list-view / object-formpackages/layout/src/__tests__/side-effects-manifest.test.tsComponentRegistry.register( 的正则从源码抽注册键,register( 那一行未动。
  • 不动 PageHeader.tsx、不动文档、不动 spec(A 案下三者本已一致)。

反向验证(先预判后跑)

预判:临时撤掉 isContainer: true —— 钉子 1 应精确翻红(多出一条且仅一条 page-headernot-a-container),钉子 2 与四条既有的渲染/形状断言应全部保持绿(渲染路径不读该 flag,这正是本 issue 的不对称之处;渲染断言若跟着红,说明前提理解错了)。

实测与预判一致:撤掉 flag 后 3 failed | 13 passed,红的恰好是本 PR 新增的三条(layout 两条 expected undefined to be true,demo 一条多出

{ "code": "not-a-container", "message": "page-header does not accept children", "severity": "warning", "tag": "page-header" }

),阳性对照与四条既有渲染/形状断言全绿。还原后与撤改前逐字节一致,同样三个文件重跑 16 passed。变异未提交。

顺手发现(已另开单,不在本 PR 修)

  • objectui#3972:同一注册文件的 inputs 声明面还有两处与实现不符 —— page-header 漏声明渲染器真读、spec 也声明的 icon(所以本 PR 的实测输出里,这份 demo 今天就带一条 unknown-prop,修 flag 前后都在),以及 navigation-rendereritems 声明成 type: 'object' 而 prop 是数组。钉子 1 刻意按 code 过滤、没把它们一起钉住,就是为了那单被修时本 PR 的钉子不会翻红。
  • objectui#3968(finding):examples/schema-catalog/test/ 的测试文件没有任何 tsc 编译过,而本该抓这件事的 scripts/check-type-check-coverage.mjs 因为 countTestFiles 只走 src/ 而看不见它们 —— 本 PR 的钉子有一半正好落在这个盲区里。

验证

  • pnpm exec turbo run type-check --concurrency=2(仓根):Tasks: 78 successful, 78 total,exit 0。
  • pnpm exec vitest run packages/layout packages/sdui-parser examples/schema-catalog --maxWorkers=2(仓根):Test Files 16 passed (16) / Tests 1204 passed (1204)
  • node scripts/check-control-bytes.mjs:OK(3854 个文件)。check-changeset-presence / check-changeset-no-major:OK。

Generated by Claude Code

…法报 not-a-container (#3900)

PageHeader 一直有意把 schema.children 渲染进右侧动作槽(PageHeader.tsx:182),
docs 把该槽的优先级写成公开契约,文档页唯一的 live demo 正是这个形状且实测正常
渲染;但 packages/layout/src/index.ts 的注册漏了 isContainer: true。

漏这个 flag 从来没挡住任何渲染 —— 渲染路径根本不读它,消费者是 sdui-parser 的
not-a-container 诊断、Studio 调色板、react-page 标签表。所以后果是校验器在说谎:
作者照文档写出能正常渲染的 schema 却拿到一条 warning,而会说谎的 warning 会训练
作者(尤其 AI 作者)连真实的 not-a-container 一起无视。

children 是 objectui JSON 协议里每个节点的基础属性(validate.ts 的 BASE_PROPS),
不是 PageHeaderProps 的键,所以补这个 flag 不在 spec 之外新开作者面 —— 维护者
2026-08-09 就 #3900 的 A/B 分叉按此理由裁定 A 案。

两个方向的钉子:文档 demo 走应用真实构建的 manifest 后不再产生 not-a-container
(先断言无 unknown-component 且 demo 确实带 children,避免缺席假绿);阳性对照
navigation-renderer 带 children 时诊断照旧触发。

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

vercel Bot commented Aug 9, 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 9, 2026 6:46pm

Request Review

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-D_Yv1dZq.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 482.58KB 106.41KB
core (index.js) 3.00KB 1.14KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 139.61KB 35.99KB
fields (index.js) 227.45KB 56.31KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.54KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.17KB 17.35KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 118.02KB 30.47KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 236.63KB 59.02KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 187.63KB 49.66KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 109.73KB 26.55KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 22.31KB 7.59KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

PM 验收(session_01GTRjn8xBqp75dk7kFupVRt):通过,转 ready 并挂 auto-merge。#3900 按维护者 A 案落地。

核验记录(head a0c35dd7d,基 ea41a595a,实物核验 + CI 亲读):

  1. A 案精确执行:page-header 注册补 isContainer: true(缩进/尾逗号随手对齐同文件惯例);前提在基上逐条复核(PageHeader 读 children、validate.ts 以 flag 判 containment、注册缺 flag),并实测该 flag 不在渲染路径(撤掉四条渲染断言全绿)—— 改动面与裁定范围一字不差。
  2. canonical page:header 刻意不动的论证成立:该渲染器不读 schema.children(函数体 grep 零命中),且它在 PUBLIC_BLOCKS,加 flag 会把它从 react 页组件作用域剔掉 —— 正确的边界判断。codegen 生成物无需重生的证明(getPublicConfigs 序列化面不含 legacy kebab 键、manifest 均非入库物)核验成立。
  3. 钉子:两方向(demo 无 not-a-container / 真无子组件仍报)+ 缺席断言的双可达性护栏(无 unknown-component + demo 确实带 children);verbose reporter 逐条确认真的跑过。该诊断此前零测试覆盖 —— 钉落 examples/schema-catalog(devDependency 无环)+ layout 注册级断言,落点论证成立。
  4. fable 门处置正确:首提交含 harness 默认 codename trailer,push 前 amend 去除(未 force-push,当时未推送)—— 计数 0。
  5. 门与规程:1204 测试绿;type-check 78/78;控制字节门;changeset patch 双门绿;⛔ releases/ 未触碰;锁竞争下只按自记 PID 清理自己的作业。
  6. CI 亲读终态:20/20 全 completed、0 失败(Test shard×4 至 18:52:22Z)。

out-of-scope 两条处置得当:#3972(layout 声明面两处不符,同族合一单)由 PM 分诊;#3968(type-check 盲区 finding,本 PR 钉子恰在其中的弱点已如实披露)进分诊轮存量。


Generated by Claude Code

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

Projects

None yet

2 participants