Skip to content

fix(app-shell): 伪路由开关改按路径段判断,拼错 app 名不再静默渲染别的 app (#3638) - #3654

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3638-segment-route-flags
Aug 7, 2026
Merged

fix(app-shell): 伪路由开关改按路径段判断,拼错 app 名不再静默渲染别的 app (#3638)#3654
yinlianghui merged 1 commit into
mainfrom
claude/issue-3638-segment-route-flags

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3638

问题

AppContent 在决定「渲染哪个 app」之前先判断 URL 是不是内建伪路由。三个开关里有两个是子串判断:

const isSystemRoute   = location.pathname.includes('/system');
const isMetadataRoute = location.pathname.includes('/metadata');

includes('/system') 对任何system 开头的段都为真(system_log / system_setting / systems),metadata 同理(metadata_import / metadata-export)。isSpecialRoute 喂给 requestedAppMissing,于是 /apps/{拼错的app名}/system_log:

  1. matchedApp 为 undefined;
  2. isSystemRoutesystem_log 误命中 → isSpecialRoute 为真;
  3. requestedAppMissing 因此为 false,「App not available」守卫不触发;
  4. activeApp 回退成默认 app,渲染它的 shell,并把 system_log 当作该 app 下的 :objectName

用户看到的是另一个 app 的界面,没有任何提示说所请求的 app 不存在 —— 正是 fallback 上方那段注释("A normal unmatched appName must NOT silently render a DIFFERENT app")要防的事。

改法

路径段判断,不是正则:

const pathSegments = location.pathname.split('/');
const isCreateAppRoute = location.pathname.endsWith('/create-app');   // 不动
const isSystemRoute = pathSegments.includes('system');
const isMetadataRoute = pathSegments.includes('metadata');

选段判断而不是 /\/system(\/|$)/ 的理由:本文件相邻的 isSetupRoute 已经是 === / startsWith 的字面量比较风格,全文件没有第二处路由正则;段判断读起来即是「某一段就叫 system」,与 issue 里三条伪路由的语义一字对应。isCreateAppRouteendsWith 未改动(它判的是末段,本来就没有这个歧义)。

顺带把 no-app 分支里一句已经过时的注释("a substring test on /metadata")改写为段判断的说法。

先量后收:今天依赖宽松判断的真实输入面

路由声明 + 导航目标为准枚举(grep path= / navigate( / url: / href:),不臆测。逐条都写成了「收紧后仍为真」的回归断言:

# 输入 来源(file) system/metadata 是否整段 收紧后
1 /apps/{app}/system/marketplace{,/installed,/:packageId} console/AppContent.tsx 路由表(两个分支都声明) 仍为真 ✅
2 /apps/{app}/metadata{,/_diagnostics,/:type,/:type/new,/:type/:name,/:type/:name/history} console/AppContent.tsx 路由表 仍为真 ✅
3 /apps/{app}/component/metadata/{directory,resource/*}(#3610 的别名,?type= 在 query 上不参与判断) console/AppContent.tsx 两个分支 是(第 3 段) 仍为真 ✅
4 /apps/{app}/systemsystem/{apps,profile,approvals,ai-approvals,audit-log,settings,settings/:namespace} 宿主 apps/console/src/AppContent.tsxextraRoutes 仍为真 ✅
5 /apps/{app}/system/{objects,objects/:objectName,metadata,metadata/:type,metadata/:type/:itemName}(legacy → 引擎的两跳重写) 同上 systemRoutes + MetadataRedirect 仍为真 ✅
6 侧栏 sys-* 簇:/apps/setup/system{,/apps,/marketplace,/users,/organizations,/roles,/settings}/apps/setup/system/metadata/object/apps/setup/component/metadata/resource?type=datasource AppSidebar.tsx / UnifiedSidebar.tsx 仍为真 ✅
7 QuickActions / HomePage / InboxPopover:/apps/setup/system/{metadata/object,marketplace,approvals} 各自文件 仍为真 ✅
8 /system 书签经 SystemRedirect(#3637)落到 /apps/setup/system console/SystemRedirect 仍为真 ✅
9 宿主 extraRoutesdeveloper{,/api-console,/flow-runs,/public-forms,/integrations}docs{,/:slug,/:slug/:name} apps/console/src/AppContent.tsx 不含 system/metadata,今天就不翻这两个 flag 不受影响(已钉)
10 /apps/setup{,/**} 整族 isSetupRoute isSetupRoute,与本 PR 无关 不受影响(已钉)

结论:今天没有任何一条活路由把 system / metadata 写成段的前缀,所以段判断不黑洞任何一条。第 9、10 行是边界,专门钉住「本 PR 没有顺手改它们」。

两个 flag 到底在哪里承重

不是「为真的地方」都承重。app 段能匹配上时两个 flag 什么都不改变(activeApp 已由 matchedApp 决定)。它们只在两种情形下决定行为,测试两种都覆盖:

消费面行为矩阵(行号以本 PR 基线实测)

isSpecialRoute 的消费点::198(activeApp 回退)、:207(requestedAppMissing);requestedAppMissing 再被 :527(preview 空态)与 :547(App not available)消费;:568(零应用空态)与 :616(零应用伪路由分支)直接读两个 flag。

输入 apps 修前 flag 修前屏幕 修后 flag 修后屏幕
/apps/ghost/system_log 有(crm 默认) isSystemRoute=true crm 的 shell + ObjectView(system_log) ← bug false 「App not available」(:547)
/apps/ghost/metadata_import isMetadataRoute=true crm 的 shell + ObjectView ← bug false 「App not available」(:547)
/apps/ghost/system_log/record/abc123 true crm 的 shell false 「App not available」
/apps/crm/system_log(app 存在) true crm + ObjectView(system_log) false 不变 — crm + ObjectView(matchedApp 命中,flag 不参与)
/apps/ghost/system/marketplace 等表 1–7 全部 true 回退默认 app 渲染该伪路由 true 不变
表 1–7 全部 true :616 伪路由分支 true 不变
/apps/setup/system_log isSystemRoute=true :616 分支的 catch-all →「Page not found」 false 「No Apps Configured」(:568) ← 见下

最后一行是实测出来的连带变化,不是设计目标:零应用下 /apps/setup/system_log 这种「以 system 开头的臆造 URL」从 404 变成空态。两块屏幕都不撒谎,变化在于 system 前缀的错字不再是特例 —— 它现在和 /apps/setup/no-such-page 落到同一块屏幕(那条已被 AppContent.noAppComponentRoutes.test.tsx 的 MEASUREMENT 用例钉住)。已在新测试里以 MEASUREMENT: 用例原样钉住并写明原委。

逆向验证(先预测,后跑)

预测写在改代码之前:

跑出来与预测一致。修前(未改 AppContent.tsx,只加测试):

Tests  8 failed | 28 passed (36)

8 红正是 P1/P2 的 7 条 + P4 的 1 条;28 绿正是 P3 的全部回归断言。修前失败现场直接把 bug 的形态打印了出来 —— /apps/ghost/system_log 渲染的是:

data-active-app="crm"
data-testid="console-layout"
  ...
  data-testid="object-view"   (内容: system_log)

修后:

Tests  36 passed (36)

测试证据

$ pnpm exec vitest run packages/app-shell/src/console/__tests__/AppContent.pseudoRouteSegments.test.tsx
 Test Files  1 passed (1)
      Tests  36 passed (36)

$ pnpm exec vitest run packages/app-shell            # 全量
 Test Files  293 passed (293)
      Tests  2581 passed | 1 skipped (2582)

$ pnpm exec vitest run apps/console                  # 宿主(extraRoutes 的声明方)
 Test Files  23 passed (23)
      Tests  212 passed (212)

$ pnpm --filter @object-ui/app-shell type-check      # 先 build 依赖(全新 worktree)
 (无输出,通过)

$ pnpm --filter @object-ui/app-shell lint
 ✖ 2205 problems (0 errors, 2205 warnings)           # 全部为既存 warning;新增文件 eslint 零输出

$ node scripts/check-control-bytes.mjs
 ✅ OK (scanned 3661 tracked text file(s))

新测试文件按「消费半径」扫过:两个 flag 只在 AppContent 内被读,但它们服务的路由由宿主 apps/console 声明,所以宿主那 23 个测试文件也跑了一遍。

文件面

  • packages/app-shell/src/console/AppContent.tsx — 两行判断 + 注释
  • packages/app-shell/src/console/__tests__/AppContent.pseudoRouteSegments.test.tsx — 新增(36 条)
  • .changeset/pseudo-route-segment-flags-3638.md — patch

未改动:#3636AppContent.noAppComponentRoutes.test.tsx#3573/#3590AppContent.noAppsCta.test.tsx(只读不改,两者全绿)、导航侧文件、locale 包、content/docs/releases/


🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt


Generated by Claude Code

… not substrings (#3638)

`isSystemRoute` / `isMetadataRoute` were `pathname.includes('/system')` /
`includes('/metadata')`, so any segment merely STARTING with the word
(`system_log`, `systems`, `metadata_import`) flipped `isSpecialRoute`.
That suppressed `requestedAppMissing`, bypassing the "App not available"
guard, and `/apps/<mistyped-app>/system_log` silently rendered the DEFAULT
app's shell with the near-miss segment as its `:objectName`.

Both flags now test whole path segments. `isCreateAppRoute`'s `endsWith`
is unchanged. The live input surface was enumerated from the route tables
and navigation targets first and pinned as regression assertions — every
real pseudo-route spells `system` / `metadata` as a full segment, so all
of them stay special, including the zero-app branch that keys on these
flags directly (#3590 / #3610).

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

vercel Bot commented Aug 7, 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 7, 2026 5:29pm

Request Review

@github-actions github-actions Bot added the tests label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-DchYFue2.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) 480.72KB 105.64KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 137.51KB 35.11KB
fields (index.js) 230.87KB 56.83KB
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) 26.14KB 6.07KB
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) 115.50KB 29.96KB
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) 186.61KB 49.34KB
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 7, 2026 17:34
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 5f752a0 Aug 7, 2026
19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3638-segment-route-flags branch August 7, 2026 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: isSystemRoute / isMetadataRoute 的子串判断把 /apps/未知app/system_log 认成伪路由,静默回退渲染另一个 app

2 participants