Skip to content

fix(types): open the BulkActionParam.options entry the renderer already forwards (#3309) - #3772

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3309-bulk-param-options-open
Aug 8, 2026
Merged

fix(types): open the BulkActionParam.options entry the renderer already forwards (#3309)#3772
yinlianghui merged 1 commit into
mainfrom
claude/issue-3309-bulk-param-options-open

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3309

问题

BulkActionParam.optionsentry 是封闭的 { label, value },而这条路径上其余每一层都不同意它:

位置 现状
类型(本 PR 修改) packages/types/src/objectql.ts entry 封闭,无索引签名
运行期 packages/plugin-grid/src/components/bulkParamToField.ts options?.map(o => ({ ...o, value: String(o.value) })) —— 整体 spread,额外键全部存活
目的地 packages/types/src/field-types.ts SelectOptionMetadata 声明 color / icon / disabled / visibleWhen
消费 packages/fields/src/index.tsx option?.color 两处命中,这些键真被读
规格 @objectstack/spec BulkActionParamSchema 同一个 entry 已是 .passthrough()(objectstack#4001,裁决 A)
父级 同文件 BulkActionParam 本身 一直有 [key: string]: unknown 兜底(带 catch-all 注释)

于是作者写 options: [{ label, value, color: '#8B5CF6' }] 会拿到一个 excess-property 编译错误,而渲染器其实会 honour 它。今天不是运行期 bug —— 但类型是唯一在拒绝一份能正常工作的配置的那一层,这正是 machine-readable surface 说谎的形状,对绝对信任类型的(AI)元数据作者杀伤最大。

改动

给 entry 补 [key: string]: unknown,注释沿用父级 catch-all 的写法。惯用形不是本 PR 发明的:#3559(PR #3764,已合 main)在一个接口之外走的正是同一条 catch-all 路线(core 导出 ActionParamOption),本单照同答案执行。

  • 没有SelectOptionMetadata,没有objectql.ts 绑到 field-types.ts
  • 没有改成引用 core 的 ActionParamOption,尽管两者结构完全一致。@object-ui/types 是协议层,按 AGENTS.md §3 拓扑「Zero deps」,不能吃 @object-ui/core 的依赖(方向也反了 —— core 依赖 types)。因此是结构等价的 inline entry + 一条交叉引用注释,注释里点名 ActionParamOption 与 objectui#3559,让下一个读者看到的是刻意的并置而不是重复。
  • label / value 保持必填且类型不变:open 不等于 optional。注释里也写清了兜底不是新增选项键的邀请函 —— authoring gate 仍是 spec 严格的 SelectOptionSchema

用户可见(对 TS 作者而言),已带 @object-ui/types 的 patch changeset。

前置事实(分诊要求核验,已实读)

objectstack#4001 的 .passthrough() 确在 objectstack origin/main,不是只在 PR 里:packages/spec/src/ui/bulk-action.zod.tsoptionsz.array(z.object({ label, value }).passthrough()),describe 文案明写「the renderer forwards unknown option keys to the field widget, which reads color / icon / disabled / visibleWhen beyond the declared pair」。本仓 node_modules 里已安装的 @objectstack/spec@17.0.0-rc.5 运行期实测也放行并保留了 color / visibleWhen(不是 stale install)。

钉子(packages/types/src/__tests__/bulk-action-param-options.test.ts)

这些钉子真的会跑:packages/types/tsconfig.test.json 编译本包每一个测试文件(#3009),所以 @ts-expect-errorAssert< … > 违反时是 tsc 错误 —— 与 @object-ui/core 把自己测试排除在 tsc 之外的情况不同(PR #3764 的同类钉子在头注释里如实记录了那一点)。

  1. color / visibleWhen / icon / disabled 的 entry 通过编译(能工作的配置现在写得出来);
  2. label / value 仍必填 —— 两条必须命中的 @ts-expect-error(与 fix(app-shell): keep a field's own option keys when an action param inherits them (#3559) #3764 同款);
  3. 索引签名本身:BulkParamOption['color'] 解析为 unknown;声明对 Pick< …, 'label' | 'value' > 未被放松;
  4. 结构兼容:把 entry 经渲染器自己的投影({ ...o, value: String(o.value) })后赋给 SelectOptionMetadata,无需 cast 即通过 —— objectql.ts 自身不 import field-types.ts,只有测试文件同时 import 两者;
  5. spec 侧 .passthrough() 这个前置事实被断言而非假定:spec 若哪天回封,这条会失败并指名要复查的层。

反向验证(先写预测,再跑)

预测:撤掉索引签名 → 第 1 条与第 3 条转红(excess-property / 属性不存在),第 2 条的两条 @ts-expect-error 不受影响(它们钉的是本次不动的声明对)。

实测 npx tsc -p packages/types/tsconfig.test.json(已撤销索引签名):

bulk-action-param-options.test.ts(61,3):  error TS2353: Object literal may only specify known properties, and 'color' does not exist in type '{ label: string; value: string | number | boolean; }'.
bulk-action-param-options.test.ts(71,49): error TS2353: ... 'color' does not exist ...
bulk-action-param-options.test.ts(86,27): error TS2339: Property 'color' does not exist on type '{ label: string; value: string | number | boolean; }'.
bulk-action-param-options.test.ts(87,27): error TS2339: Property 'visibleWhen' does not exist ...
bulk-action-param-options.test.ts(110,51): error TS2339: Property 'color' does not exist ...
bulk-action-param-options.test.ts(131,65): error TS2353: ... 'color' does not exist ...

方向与预测一致。两处补充说明,如实记录:

  • 86 / 87 行是运行期断言读 authoredOption.color / .visibleWhen,同因转红 —— 预测里没单列,但同向,不构成意外。
  • 两条 @ts-expect-error 在两个方向都没报错:恢复索引签名后 tsc 全绿,这本身就证明它们命中了(未命中的 @ts-expect-error 会以 TS2578 报错);撤销后也未出现在错误清单里。它们对本次改动确实免疫,与预测一致。

验证

  • pnpm --filter @object-ui/types type-check(含 tsconfig.test.json)—— 绿。
  • turbo run type-check --filter @object-ui/plugin-grid --filter @object-ui/fields --filter @object-ui/app-shell --concurrency=2 —— 31 tasks successful(含整条 build 闭包)。加宽是向后兼容的,亦无 exactOptionalPropertyTypes 之类意外(仓库根 tsconfig 未开)。
  • vitest run --maxWorkers=2 packages/types/src/__tests__ packages/plugin-grid/… —— 34 files / 451 tests passed。
  • 消费半径清扫:全仓无任何 fixture 钉着封闭的 bulk options 形状(app-shell resolveActionParams.test.ts:508/510fix(app-shell): keep a field's own option keys when an action param inherits them (#3559) #3764ActionParamOption 必填钉,同族、不受影响,且在上面的 app-shell type-check 里一并绿)。
  • 门禁:check-control-bytes(OK)+ 手工 grep -naP 扫本次三个文件(clean);check-changeset-presence / check-changeset-no-major / check-changeset-fixed 全绿;eslint 改动文件 0 errors(两条 _CatchAll / _DeclaredPair unused 警告与 packages/auth/src/__tests__/auth-spec-parity.test.ts 的既有惯用形一致,该文件同类警告 23 条)。

Generated by Claude Code

…dy forwards (#3309)

`BulkActionParam.options` 的 entry 之前是封闭的 `{ label, value }`,而这条路径上
其余三层都不同意它:

- 运行期 `bulkParamToField` 把每个 entry 整体 spread 进交给 widget 的 field
  metadata(`{ ...o, value: String(o.value) }`),额外键全部存活;
- 目的地 `SelectOptionMetadata` 声明了 `color` / `icon` / `disabled` /
  `visibleWhen`,`@object-ui/fields` 确实在读(`option?.color` 两处);
- `@objectstack/spec` 的 `BulkActionParamSchema` 已把同一个 entry 转成
  `.passthrough()`(objectstack#4001,维护者裁决 A),服务端照收;
- 它自己的父级 `BulkActionParam` 一直有 `[key: string]: unknown` 兜底注释。

于是作者写 `options: [{ label, value, color: '#8B5CF6' }]` 会拿到一个
excess-property 编译错误,而渲染器其实会 honour 它 —— 类型是唯一在拒绝一份能
正常工作的配置的那一层。今天不是运行期 bug,但这是 machine-readable surface
说谎的形状,对绝对信任类型的(AI)作者杀伤最大。

按 #3559(PR #3764)先落地的那一半已经定下的惯用形处理:给 entry 补
`[key: string]: unknown`,注释沿用父级 catch-all 的写法。⛔ 不动
`SelectOptionMetadata`,不把 `objectql.ts` 绑到 `field-types.ts`。**没有**改成
引用 core 的 `ActionParamOption`:`@object-ui/types` 是协议层、不吃 workspace
依赖(AGENTS.md §3 拓扑),因此是结构等价的 inline entry 加一条交叉引用注释。

`label` / `value` 保持必填且类型不变 —— open 不等于 optional;兜底也不是新增
选项键的邀请函,authoring gate 仍是 spec 的严格 `SelectOptionSchema`。

新增 `packages/types/src/__tests__/bulk-action-param-options.test.ts` 钉住四件事:
带 `color`/`visibleWhen` 的 entry 通过编译;`label`/`value` 仍必填(两条必须命中
的 `@ts-expect-error`);entry 经渲染器自己的投影后仍落进 `SelectOptionMetadata`
(无需 cast);spec 侧 `.passthrough()` 这个前置事实被断言而非假定。这些钉子是
真会跑的 —— `packages/types/tsconfig.test.json` 编译本包每个测试文件(#3009)。

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

vercel Bot commented Aug 8, 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 8, 2026 1:39pm

Request Review

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-CCusf2DY.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) 481.28KB 105.96KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 9.85KB 3.18KB
data-objectstack (index.js) 138.22KB 35.46KB
fields (index.js) 230.90KB 56.84KB
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.53KB 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.04KB 17.31KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 117.06KB 30.24KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 232.79KB 57.42KB
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.71KB 49.68KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
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) 19.28KB 6.38KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 8, 2026 14:07
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit d229dfa Aug 8, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3309-bulk-param-options-open branch August 8, 2026 14:07
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.

BulkActionParam.options is typed closed while its own runtime path and destination type are open

2 participants