Skip to content

fix(app-shell): 零应用分支补 component/metadata 别名路由与 catch-all (#3610) - #3636

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3610-noapp-component-routes
Aug 7, 2026
Merged

fix(app-shell): 零应用分支补 component/metadata 别名路由与 catch-all (#3610)#3636
yinlianghui merged 1 commit into
mainfrom
claude/issue-3610-noapp-component-routes

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3610

现象与成因

零应用部署下,系统兜底侧栏的两项点下去是纯白屏 —— 没有 404、没有报错、没有空态:

  • sys-datasources/apps/setup/component/metadata/resource?type=datasource
  • sys-objects/apps/setup/system/metadata/object,由 console 宿主的 MetadataRedirect 改写成上面同一条 URL

AppContent.tsx:185isMetadataRoute 是对 /metadata 的子串判断,两条 URL 都为真,于是越过 :568 的「No Apps Configured」守卫,进入 :616 的无-activeApp 路由表。该表只声明 create-appsystem/marketplace*metadata* 与宿主传入的 extraRoutesNoApp,没有任何 component/*;而且末尾没有 catch-all(全文件 path="*" 只有一处,在有-activeApp 分支的 :775)。无匹配的路由表渲染 null = 白屏。

两种拼法的关系(实测,与派发预设相反)

派发单假设 component/metadata/resource?type= 才是规范拼法(理由是宿主的 MetadataRedirect 往它改写)。实测结论正好相反:

  • AppContent.tsx:753-754(有-activeApp 分支)把这两条 component/metadata/* 声明为 LegacyMetadataRedirect,上方注释原文:"Legacy: old metadata routes built before the REST-style nesting landed. Redirect to the new /metadata/:type/... shape."
  • LegacyMetadataRedirect 自己的 docblock(:927-933)写的是同一件事:把 pre-refactorcomponent/metadata/resource/:name?type=:type 翻译成 new REST-stylemetadata/:type/:name

所以两种拼法不是同一页面的两个入口,而是一个别名指向一个页面:metadata/:type 才是规范拼法(两个分支都把它声明为真正的 MetadataResourceListPage),component/metadata/resource 是旧别名 —— 在有-app 分支里它根本不渲染页面,只做 302。

这个结论不改修法,只改叙述:「镜像有-app 分支的 component/metadata 形态、渲染同一组件」落地下来就是把同样两条 LegacyMetadataRedirect 声明进无-app 分支,而它们转向的 metadata/:type 该分支本来就有。这也正是契约优先的选择:别名保持唯一的规范落点,不为零应用另造一个拼法。导航 URL 一个没改。

改动

packages/app-shell/src/console/AppContent.tsx,只动无-activeApp 分支(+22 行,全部为新增):

  1. component/metadata/directorycomponent/metadata/resource/* 两条别名路由,element 与有-app 分支完全一致。
  2. 分支末尾补 path="*"RouteNotFound,与有-app 分支的 :775 对齐 —— 让这一类缺陷从「白屏不可报告」变成「可读的没找到」。

零应用下的完整链路(测试逐条钉住):

入口 URL链路落点
/apps/setup/component/metadata/resource?type=datasource别名 302/apps/setup/metadata/datasource 资源列表页
/apps/setup/system/metadata/object宿主 MetadataRedirect…/component/metadata/resource?type=object → 别名 302/apps/setup/metadata/object 资源列表页
/apps/setup/system/metadata宿主 → …/component/metadata/directory → 别名 302/apps/setup/metadata 目录页
/apps/setup/system/no-such-pagecatch-allRouteNotFound(原白屏)

sys-objects 那条绕行链穿过三张路由表、两次重定向,是最不显眼、最会静默回归的一条,按分诊裁定单独钉了一条测试。

逆向验证(先预测后运行)

预测:新测试保留、AppContent.tsx 停在 origin/main 状态 → 五条新断言全红,方向是查无元素(白屏),而不是渲染错误。

实测吻合 —— Tests 5 failed | 1 passed (6)。失败现场的 DOM dump 里 body 只剩测试自己的 pathname 探针,一个业务节点都没有,这是白屏的字面证据:

TestingLibraryElementError: Unable to find an element with the text: Page not found.
body
div
div data-testid="pathname"
/apps/setup/system/no-such-page

第 6 条预期即为绿 —— 它是边界测量,不是回归钉子,见下一节。

一处验收口径修正

验收条目里的「访问 /apps/setup/no-such-page → 渲染 RouteNotFound」这条 URL 选错了:它不含 /system、不含 /metadata、也不以 /create-app 结尾,三个开关全不亮,所以它压根进不了这个分支 —— :568 的守卫先命中,渲染的是「No Apps Configured」空态(既有行为,AppContent.noAppsCta.test.tsx 已有同族钉子)。真正需要 catch-all 的,是进得了分支却匹配不上的 URL,例如 /apps/setup/system/no-such-page。测试按后者写,并把前者作为边界单独钉了一条 MEASUREMENT 用例,免得这两块屏日后被混为一谈。

测试

新增 packages/app-shell/src/console/__tests__/AppContent.noAppComponentRoutes.test.tsx(6 条)。宿主的 MetadataRedirectapps/console 的模块私有函数、且属于另一个 vitest project,app-shell 无法 import,因此在测试里逐字转写了它的 prefix 正则与 target 构造,并在文件头注明出处 —— 没有它,sys-objects 那一跳在 app-shell 内不可测量。

pnpm exec vitest run packages/app-shell/ → Test Files 288 passed (288) / Tests 2516 passed | 1 skipped (2517)
pnpm exec vitest run apps/console/ → Test Files 23 passed (23) / Tests 212 passed (212)
pnpm --filter @object-ui/app-shell type-check → clean
node scripts/check-control-bytes.mjs → OK (3650 tracked text files)
eslint(三个改动文件) → 0 errors(37 warnings 均为既有 no-explicit-any)

有-activeApp 分支零改动,现有测试全绿。

范围

只碰 AppContent.tsx 的无-app 分支、其测试与 changeset。未碰UnifiedSidebar.tsx(在途 #3609)、AppSidebar.tsx(#3611)、ConsoleShell.tsxQuickActions.tsxapps/console/** —— 导航侧 URL 一个未改。

顺带把 AppContent.noAppsCta.test.tsx 里两处「该分支没有 catch-all,因此渲染空白」的注释改写为现状(断言未动)—— 本 PR 正是让那句话过期的改动。


🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt


Generated by Claude Code

零应用部署下,系统兜底导航的 sys-datasources 指向
/apps/setup/component/metadata/resource?type=datasource,sys-objects 经
console 宿主的 MetadataRedirect 改写后落到同一条 URL。isMetadataRoute 是对
`/metadata` 的子串判断,两条 URL 因此都越过「No Apps Configured」守卫进入
AppContent 的无-activeApp 路由表 —— 而该表既没有任何 component/* 声明,也没有
有-activeApp 分支末尾那条 catch-all。无匹配的 <Routes> 渲染 null,于是全新环境
的管理员点进去只看到一块纯白屏:没有 404、没有报错、没有空态。
测量结论(与派发时的猜测相反):component/metadata/resource?type= 不是规范拼法,
而是别名。有-activeApp 分支把它声明成 LegacyMetadataRedirect(注释原文
"Legacy: old metadata routes built before the REST-style nesting landed"),
302 到规范的 metadata/:type。两种拼法不是两个页面,是一个别名指向一个页面。
因此修法是纯镜像,导航 URL 一个不改:把同样两条别名路由声明进无-activeApp 分支,
它们转向的 metadata/:type 该分支本来就有;并补上与有-app 分支对齐的 path="*" →
RouteNotFound,让这一类缺陷从「白屏不可报告」变成「可读的没找到」。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 7, 2026 4:41pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-DdvD29O7.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
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)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
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)480.72KB105.64KB
core (index.js)2.96KB1.13KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)137.51KB35.11KB
fields (index.js)230.87KB56.83KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
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)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)115.50KB29.96KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)232.79KB57.42KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)186.61KB49.34KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)105.12KB25.48KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
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 (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)2.71KB1.34KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 7, 2026 16:48
@yinlianghui
yinlianghui added this pull request to the merge queueAug 7, 2026
Merged via the queue into main with commit fa3ba5bAug 7, 2026
19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3610-noapp-component-routes branch August 7, 2026 16:49
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