Skip to content

fix(plugin-form): consume submitBehavior.url as objectstack#7496 ruled it (#4989) - #5032

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4989-plugin-form-redirect-contract
Aug 17, 2026
Merged

fix(plugin-form): consume submitBehavior.url as objectstack#7496 ruled it (#4989)#5032
yinlianghui merged 2 commits into
mainfrom
claude/issue-4989-plugin-form-redirect-contract

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4989

ObjectForm / WizardForm 现在按 objectstack#7496 的裁定消费 submitBehavior: { kind: 'redirect' }(该裁定由 objectstack#7657 落地,活在本仓 @objectstack/spec 17.0.0 GA 钉子上)。卡面五点里的 2、3、5 机械修完;第 4 点(挂载盲)实测后判定不可按派发的机制落地,已升级为决策题,详见下文"未修的一半"。

契约判据不在本包重写一份

两个消费点原来都是 isSameOriginUrl(behavior.url) 把 URL 解析到 window.location.href 再比 origin。新的 packages/plugin-form/src/submitRedirect.ts 不再复述规则,而是在使用的那一刻拿 spec 自己的 FormViewSchema 解析最小 form view —— 与 PR #4992(console 侧同裁定实施件)同一份来源。因此绝对 URL、协议相对 //host、反斜杠、空白与控制字符走私、畸形 token、文档相对路径,全部用 spec 自己的作者向措辞拒绝;裁定日后放宽(它自己写了绝对 origin 白名单"等实测需求")时,本包跟着版本钉子走,不需要再改这里。

⛔ 没有 import console 的模块(已发布包不能依赖 app),⛔ 没有手写第二份七族规则镜像 —— 那正是 scripts/check-spec-symbol-derivation.mjs 反对的形状。两包唯一重复的是 token 替换与转义那约 20 行(spec 不导出替换器),docblock 里把两处的差异逐条写明了,便于将来收敛。

三处修正

  • 缺陷 2 —— 静默丢弃改响亮拒绝。 旧代码 guard 答否时 if 直接不进:无 toast、无错误、无确认面板。写入已经成功了,提交者面对的是一张仍然填满的表单和零反馈,而最自然的下一步 —— 再提交一次 —— 会写出第二条记录。现在拒绝同时说两件事:记录确实写成了(保留确认面板,顺带把填满的表单换掉,没有可重复提交的东西了),以及为什么没有跳转(spec 的作者向措辞,role="alert" 面板 + toast.error)。拒绝挂在 submitted 上而不是 error 上 —— error 是加载/提交失败通道,契约外的目的地不是提交失败,放进去等于告诉用户记录丢了。
  • 缺陷 3 —— 同源绝对地址不再被跟随。isSameOriginUrl('https://own-host/thanks') 为真,所以本消费点接受了一个 spec 已在录入门口拒绝的拼法 —— 被拒拼法就是这样活在语料里的。relative-only 严格强于 same-origin(带根的相对路径天然同源),所以这条 arm 上保留 isSameOriginUrl 作纵深毫无增益,已移除;函数本身留着,因为 resolveSuccessNavigate 还需要它(见下)。
  • 缺陷 5 —— token 现在会替换。 作用域是"这次提交刚写的记录":提交的值,叠上 DataSource 回答的内容(id、默认值、计算列在那边才是权威)。两个组件走同一个 submitRedirectScope,所以 {{record.id}} 不会在 wizard 里解析成一个样、在平表单里另一个样。每个插值都过 encodeURIComponent —— token 是路径里的,永远不能变成新的路径结构。

delayMs 语义一字未动。navigateOnSuccess 是另一个声明键(自己的 {id} 方言、自己尚未裁定的契约问题),按卡面划界一字未动。

未修的一半:缺陷 4(挂载盲)—— 升级为决策题,未擅自选路

派发给我的中间路线是"可选读 React Router context"。React 那一半实测成立:provider 外 useInRouterContext() 返回 falseuseContext(UNSAFE_NavigationContext) 返回 null(不抛);挂 basename="/_console" 时 context 在场、basename 读得到,且 useNavigate()('/thanks') 确实吃掉 basename 落在 /thanks 路由上。

依赖那一半不成立,这是停下的原因:

  • @object-ui/plugin-form 在 dependencies / peerDependencies / optionalDependencies 里都没有 react-router,本仓能解析到它纯粹因为它是仓根 devDependency 被抬上来了 —— 这正是 scripts/check-phantom-dependencies.mjs(objectui#4394)存在的理由,它的文件头还记着 plugin-detail 那次同类真缺陷。
  • 本包 vite.config.ts 把所有裸 specifier 全部 external,所以静态 import 会在发布的 dist/index.js 里留下裸 react-router-dom,每个消费者都必须解析它。而确实存在没有 react-router 的消费者:apps/site@object-ui/plugin-form 列为真依赖并在 app/components/PluginLoader.tsx 里 import,自身任何字段都没有 react-router;packages/plugin-view 同样。
  • 改成 dependencies/optionalDependencies 反而自败:那会给本包一份自己的 react-router 实例,而 React context 是模块实例级的,读到的 context 对象和宿主的不是同一个,永远答 null。要保单实例只能是 peer,而 peer 恰恰就是强迫消费者安装。
  • 惰性 import() 也不成立:消费者的打包器仍要在构建期解析它;而用打包器分析不到的 specifier 绕过,是 AGENTS.md 准则 Fix documentation deployment for www.objectui.org #6 明令禁止的。

按派发纪律,⛔ 没有改走注入回调/新 prop,也没有擅自换第三种机制。选项与两轴分析写在报告的 open_questions 里交维护者裁。今天的行为保持不变(相对路径交 window.location.assign),submitRedirect.mountBlind.test.tsx 把这个开放缺陷的实测钉住 —— 包括对照:同一个字符串按 app-relative 解析确实会落进挂载点内。钉它是为了不让一个"衡量过、故意留着"的缺陷退化成口头传说。

⚠️ 因此请维护者注意:本 PR 写的是 Fixes #4989(按派发要求逐字),但合并即关闭会连带关掉尚未裁定的第 4 点。合并前请按裁定结果改成 Refs、或先把第 4 点拆成后续 issue。

验证

  • pnpm exec vitest run packages/plugin-form/(仓根,与 CI 同路径):51 文件 / 535 测试全绿。新增 3 个测试文件、1 个实测记录文件。
    • 注:submitRedirect.mountBlind 一开始写成 .test.ts,从仓根跑时被分到 node 环境的 unit project,document is not defined 3 红 —— 只在包目录里跑是看不到的。按 vitest.config.mts 自己写的约定改名 .test.tsx(而不是去改共享的根配置)。
  • 消费半径:packages/plugin-view/ + packages/react/src/spec-bridge/(17 文件 / 222 测试全绿);app-shell 里承载这两个组件的 4 个文件(41 测试全绿)。
  • 仓根 pnpm exec turbo run type-check --concurrency=2:81/81 successful
  • node scripts/check-control-bytes.mjs OK;改动文件另做了含 0x09 的越门自扫(测试里有制表符走私用例,一律写成转义序列)。check-phantom-dependencies.mjscheck-spec-symbol-derivation.mjs、三个 changeset 门:全绿。改动文件 eslint 0 error。

反向验证(先书面预判,再变异,git checkout -- 还原;预判全部命中)

变异预判实测
退回裁定前消费(isSameOriginUrl + 直接 assign 原串)组件用例大面积红;单元文件全绿12 红(ObjectForm 6/9、Wizard 6/7),单元文件与 mountBlind 全绿
只删拒绝 arm(保留 spec 解析,目的地静默丢弃)每个组件恰好 3 个拒绝用例红,契约内全绿6 红,10 个契约内用例全绿
拒绝换本地文案文案出处断言红18 红(单元文件 14:13 个拒绝族 + 外部替代项)
encodeURIComponent转义与恶意值块红10 红(单元 8 + 每组件 1)

两条值得单独记下来的:①退回裁定前消费时单元文件全绿 —— 它杀的是调用点而非模块,这就是消费语义必须钉在组件旁边的原因;②"只删拒绝 arm"这个变异就是原始的缺陷 2,只是 guard 已经变严了 —— 所以"guard 现在是 relative-only 了"本身并不构成修复。

派发模板里的第三条反向验证(撤 router 分支 → basename 用例红)在本 PR 不适用:没有 router 分支可撤。照直写出来,而不是造一个符合模板形状的证据。


Generated by Claude Code

…d it
ObjectForm and WizardForm read a declared `submitBehavior: { kind: 'redirect' }`
through `isSameOriginUrl` and navigated when that answered yes. Measured against
the ruled shape (objectstack#7496, landed by objectstack#7657, live on the
`@objectstack/spec` 17.0.0 GA pin) that diverged three ways, all fixed here
(objectui#4989 defects 2, 3 and 5):
- an out-of-contract destination was dropped in SILENCE after a successful
write, leaving the submitter facing a still-filled form whose obvious next
move wrote a second record. A refusal is now visible: the spec's own
author-facing prescription in an alert beside the confirmation that the record
WAS written, toasted as well, with the filled form replaced so there is
nothing left to resubmit.
- a same-origin ABSOLUTE url was followed, where the contract is relative-only,
so this renderer accepted a spelling the authoring door refuses. The verdict
is no longer restated here at all: `resolveSubmitRedirect` asks the spec's own
`FormViewSchema` at the moment of use, so a later widening of the ruling is
followed by the version pin rather than by an edit.
- `{{record.field_name}}` was never substituted. It now is, from the values as
submitted with whatever the DataSource answered layered on top, every
interpolated value through `encodeURIComponent` so a token is a value in the
path and never a way to add path structure.
`delayMs` semantics are unchanged. `navigateOnSuccess` is a different declared
key with its own dialect and its own open contract question and is untouched;
`isSameOriginUrl` survives because that key still needs it.
Defect 4 (mount-blindness) is deliberately NOT fixed and is escalated on the
issue: applying a host's mount means learning it, and every mechanism available
to a published renderer changes its contract. `submitRedirect.mountBlind.test.tsx`
pins the measurement so the open defect stays visible.
Refs objectui#4989
Co-authored-by: Claude <noreply@anthropic.com>
The four mutations were predicted in these docblocks and then run. The
predictions held; the docblocks now carry the MEASURED numbers rather than
prose, including the two facts a reader would otherwise have to rediscover:
- restoring the pre-ruling consumption (`isSameOriginUrl` + assign the authored
string) turns 12 of the 16 component tests red and leaves the unit file
ENTIRELY green — it kills the call site, not the module, which is why the
consumption is pinned per component.
- deleting only the refusal arm while keeping the stricter parse turns exactly
the 3 refusal tests in each component red. That mutation IS the original
defect 2, reproduced with a relative-only guard in place, so "the guard is
stricter now" is not by itself the fix.
Also measured: swapping the spec's refusal prose for a local sentence turns 18
red (14 of them in the unit file), which is the load-bearing probe that the
sentence on screen comes from the live schema parse; and deleting the
`encodeURIComponent` turns 10 red.
Refs objectui#4989
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-BsjihAB4.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)502.13KB112.19KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.03KB44.08KB
fields (index.js)233.27KB58.22KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.85KB32.73KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)241.12KB60.43KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)122.61KB29.71KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)197.61KB53.03KB
plugin-kanban (index.js)52.72KB14.54KB
plugin-list (index.js)111.17KB26.99KB
plugin-map (index.js)18.72KB6.09KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.97KB11.33KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.53KB9.41KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 验收 ✅ ACCEPT(#4989 的缺陷 2/3/5 半边,批次 21)

实物核验(merge-base e71c854ce 与报告一致):11 files,+1210/−14 —— submitRedirect.ts 从 spec FormViewSchema 现场解析取裁决(零第二份规则、零 console import),两组件契约外可见拒绝(spec 处方 + role=alert + toast,写入确认保留、表单替换掉重复提交那条路),同源绝对拒绝,插值 + 转义 + fail-closed;navigateOnSuccess 一字未动守住卡面边界;delayMs 语义不变。模型标识 msg/diff 双 0;releases/ 零触碰。

CI 亲读:20/20 check runs completed,零失败(两项 path-filter skipped 计绿)。

缺陷 4 的升级处置采信:PM 裁定的中间路线被两点实测拆解 —— React 层可行(context 读法、basename 吞吃均量到),依赖层不可行(读 context 必须 import react-router;包四个依赖字段均未声明;apps/site/plugin-view 两个真实消费者未安装,发布产物裸 external 会打断其构建;dependency/optional 因 pnpm 实例隔离拿到自己的 context 实例永远 null;惰性 import 被 #6 诫令禁止且救不了同步渲染读)。dev 按停止条款升级、未擅自换机制、把测量写进 #4989 评论 5318710832 —— 正确执行。mountBlind 钉把「刻意未修」变成实测记录防口传。变异 A 替换模板不适用的变异 3 并如实说明,C 变异 18 红重证文案出处承重。.test.ts.tsx 的仓根/包内跑差异陷阱自查修正,控制字节双物化自纠(含二次修复产出双反斜杠的 od 级复核)—— 读数纪律全程在线。

合并卫生(PM 裁定):保留 Fixes #4989 原样合并;落 main 后本 PM 立即 reopen #4989needs-user-decision,由它继续持有缺陷 4 的机制决策(dev 的 B 方案推荐 + A/C 权衡与「SPA 转场是否为需求」这个产品判断都在其评论里)—— #5034 的 Blocked-by 指向保持正确,无需改任何 body。#5033(裸 setTimeout 竞态)定级入队;#5035 finding 入池。undraft + auto-merge(squash)。


Generated by Claude Code

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

Labels

Projects

None yet

2 participants

@yinlianghui@claude