Supabase Studio 从 Next.js Pages Router 到 TanStack Start 的渐进式迁移实践
发布时间:2026/9/8 20:46:02 作者:尧图编辑部 阅读量:1,286

Supabase Studio 从 Next.js Pages Router 到 TanStack Start 的渐进式迁移实践【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabaseSupabase Studio 是 Supabase 的开源控制台Dashboard应用代码位于 apps/studio源码规模庞大历史代码基于 Next.js Pages Router 编写。为了把前端运行时迁移到 Vite TanStack StartTanStack Router 的文件路由体系团队采用了一份双运行时并存的渐进式迁移路线图即 TANSTACK_MIGRATION.md。本文将完整还原这份迁移文档的策略、路由清单、兼容层设计与构建层 workaround并结合仓库真实源码说明每条规则背后的原因。读完本文你既能理解大规模 SPA 从 Next.js Pages Router 迁移到 TanStack Start 的完整套路共享布局建模、withAuth迁移、API 路由 shim、chunk 循环依赖防护也能直接在 Supabase 仓库中对照真实文件逐条验证。一、迁移背景为什么采用双运行时并存Supabase Studio 规模大、页面多组织管理、项目数据库/Auth/Storage/Functions/Logs/设置等产品线一次性切文件 删代码风险极高。因此迁移文档见 TANSTACK_MIGRATION.md 开头 Runtime model规定迁移期间Next.js pages router 与 TanStack route tree 同时上线Next.js 的pages/...目录与 TanStack 的routes/...目录在迁移期同时发布日常跑的是 Vite/TanStack 构建build:next/dev:next/start:next等脚本见 apps/studio/package.json 中 scripts继续存活作为兜底运行时用于回归二分bisect与随时切换发布。由 apps/studio/package.json 可看到两组脚本并存dev:next: next dev -p ${STUDIO_PORT:-8082}, build:next: next build if [ \$SKIP_ASSET_UPLOAD\ ! \1\ ]; then ./../../scripts/upload-static-assets.sh; fi, start:next: next start -p 8082, dev:tanstack: NODE_OPTIONS--max-old-space-size8192 vite dev --port ${STUDIO_PORT:-8082}, build:tanstack:vite build --mode ${MODE:-production} pnpm smoke:tanstack, start:tanstack:node scripts/serve.js,迁移期间的三条铁律绝不删除任何apps/studio/pages/...文件。Path A 页面下文详解是从pages/中 re-export 默认导出Next 文件对两个运行时都是承重墙删了既破坏 Next 构建也破坏对应 TanStack 路由。页面 body 的移动与pages/...删除只允许发生在最后的 cleanup pass——在所有路由都已在routes/...表示、准备彻底退役 Next 运行时之后这是独立、刻意的阶段不能揉进单个路由 PR。Next 兼容 shimapps/studio/compat/next/同样保留到 cleanup pass与迁移进度无关。PR 护栏用 CodeRabbit 规则强制双写检查因为两套运行时同时发布任何对pages/...文件的改动都可能让routes/...里的镜像失同步。仓库根目录的 .coderabbit.yaml 中定义了一条作用域为apps/studio/pages/**的path_instructions规则任何触碰 page 的 PRCodeRabbit 都会提醒作者检查对应 route 是否需要同步修改。该规则是提醒核对、不阻断verify-not-block机制纯页面 body 改动Path A 页面是 re-export自动传播无需镜像涉及getLayout/ layout 包装、staticDataprops、withAuth、重定向路径、或全新页面的改动必须手工镜像到routes/**新页面还需要在迁移文档追加清单项规则明确禁止建议删除pages/**文件其删除由 FE-3106 跟踪的 cleanup pass 统一处理。这条 guardrail 同样是临时脚手架清理阶段删除pages/**时一并移除.coderabbit.yaml中该指令注释也指向本迁移文档。二、迁移策略最小 diff 的 re-export迁移的核心目标是把 URL 归属权切换给 TanStack而暂不重写页面内部实现。文档对每个页面给出两条路径Path A——从pages/re-export默认TanStack route 从apps/studio/pages/...导入页面的 default export并在一个薄包装组件route 的component中渲染getLayout被丢弃——由 TanStack 的布局链pathless 的_app.tsx/_auth.tsx sibling-file 布局负责外层包装页面里 Next 专属 import 通过compat/next/shim 继续工作由于NextPageWithLayout把{ dehydratedState: any }声明为必填props包装组件里要显式传dehydratedState{undefined}。Path B——直接组件导入当pages/...文件本质上只是export default SomeComponent薄包装页面的常见形态时跳过中间层直接在 TanStack route 中导入SomeComponent。典型例子如routes/index.tsx、routes/authorize.tsxstandalone 页面以及pages/api/ai/docs.ts本身已是 edge-runtime / Web Response 原生写法直接 re-export。共享布局必须先落地在逐页迁移前先把共享布局建好Pathless 布局路由_app.tsx、_auth.tsx承载共享外壳不贡献 URL 段Sibling-file 布局紧挨segment/目录放置的segment.tsx用Outlet/为该目录下子路由提供布局例如_app/account.tsx包裹_app/account/me.tsx。不产生route.tsx文件每个产品布局DatabaseLayout、AuthLayout、SQLEditorLayout……变成一个 sibling-file 布局。其他规则新代码直接使用原生 TanStack API不再用next/router、next/linkNext compat shim 仅为被 re-export 的旧页面保留withAuth()HOC 转换为布局/路由上的 TanStackbeforeLoad尽量在共享布局层级统一处理绝不在迁移中途删除pages/...本清单不覆盖pages/api/**Next API 路由单独迁移、_app.tsx、_document.tsx、_error、以及两个 catch-allpages/org/_/[[...routeSlug]].tsx、pages/project/_/[[...routeSlug]].tsx最后专门处理。三、布局体系的重建shell 目录逐层拆解迁移文档对布局的落位标注了非常细致的Delta vs plan相对原计划的偏差这些偏差是理解 Studio 页面组合关系的关键。App shellpathless 层账号/组织/通用页面布局文件内容与计划的偏差Deltaroutes/_app.tsxAppLayout DefaultLayout读取叶子staticData的defaultLayoutHeaderTitle/hideMobileMenu—routes/_app/account.tsxAccountLayout读accountLayoutTitle—routes/_app/org.tsxOrganizationLayout读orgLayoutTitle同时包裹/org/index 与/org/$slug/*原计划放在_app/org/$slug.tsx现改为_app/org.tsxPageLayout仅/org/$slug/index.tsx使用内联在叶子中routes/_app/new.tsx不建只有_app/new/index.tsx在 _app 下内联 WizardLayoutnew/$slug是顶层无 AppLayout子 shell 不共享状态routes/integrations/vercel.tsx仅透传Outlet/放在顶层而非_app/下三个 Vercel 叶子各自内联渲染InterstitialLayout旧的VercelIntegrationWindowLayout已删除Project shell/project/$ref产品线核心这是最复杂的 shell 层。要点如下对应routes/project/目录下文件routes/project/$ref.tsx只提供 DefaultLayout。关键决策省略ProjectLayoutWithAuth——因为各产品布局DatabaseLayout、AuthLayout 等内部已渲染withAuth(... ProjectLayout ...)再加会双重包裹首页/project/$ref/index.tsx没有产品布局自己在叶子内包裹ProjectLayoutWithAuth。各产品子 shell 均从叶子staticData读取标题并处理跳过外层布局的 opt-out典型模式是skipXxxLayout: true用于避免二次包裹二次包裹会连带withAuthProjectLayout双跑Shell布局特殊机制database.tsxDatabaseLayout读databaseLayoutTitle—database/triggers.tsx子 shellPageLayout 权限门 nav 内联自DatabaseTriggersLayout复用原组件会在database.tsx外壳内再包一层 DatabaseLayout二次包裹故只内联其内层auth.tsxAuthLayout读authLayoutTitle支持叶子skipAuthLayout: trueAuthProvidersLayout、AuthEmailsLayout内部已自包 AuthLayoutstorage.tsxStorageLayout StorageBucketsLayout默认两层都包bucket 详情页设skipStorageBucketsLayout: true/storage/s3用storageBucketsLayout{Title,HideSubtitle}覆盖内层头部functions.tsxEdgeFunctionsLayout支持skipFunctionsLayout: truefunctions/$functionSlug.tsx子 shell 提供 EdgeFunctionDetailsLayout 给 5 个 slug 叶子branches.tsx仅 BranchLayoutper-page 的 PageLayout 留在各叶子BranchesPageWrapper/MergeRequestsPageWrapper提升为pages/...文件顶层导出供 route 复用logs.tsxLogsLayoutlogs/index设skipLogsLayout: trueUnifiedLogs 自己处理 ProjectLayout原pages/.../logs/index.tsx把内联DefaultLayout移入getLayout避免重复advisors.tsxAdvisorsLayout支持skipAdvisorsLayout: truerules 子 shell 扫描整个 match 链advisors/rules.tsx子 shell内联AdvisorRulesLayout内层原组件自带 DefaultLayout AdvisorsLayout复用会双包两层故只内联内层settings.tsxSettingsLayout支持skipSettingsLayout: truesettings/api纯重定向页settings/api-keys.tsx子 shell 提供 ApiKeysLayoutjwt/index内联 JWTKeysLayoutintegrations.tsxProjectIntegrationsLayout4 个叶子共享同一布局shell 只包一次Outlet/sql.tsxEditorBaseLayout SQLEditorLayout四个叶子布局 props 相同外壳硬编码EditorBaseLayout 自带 ProjectLayoutWithAuthSQLEditorLayout 另有withAuthHOC认证跑两次但不重复渲染editor.tsxEditorBaseLayout TableEditorLayout三个叶子共享TableEditorLayout happy path 只是 fragment banner仅在无权限分支才包 ProjectLayoutWithAuthAuth shellpathlessroutes/_auth.tsx提供 AuthenticationLayout承载/sign-in、/sign-up、MFA、找回密码、合作伙伴登录等认证相关页面。用staticData传递页面元数据上述布局反复出现一个关键词staticData。TanStack Router 允许在 route 上声明静态数据叶子路由通过staticData声明标题类 propsdatabaseLayoutTitle、authLayoutTitle、orgLayoutTitle、hideMobileMenu等父级 shell 布局读取后决定渲染什么标题/菜单。这是对 NextNextPageWithLayoutgetLayout模式的直接替代——迁移时把页面标题、是否隐藏移动端菜单、是否跳过某层布局等页面级装饰信息统一挪进staticData让布局链数据驱动而非组件嵌套驱动从而根治多层组件互相包裹造成的二次包裹问题。四、页面迁移清单导读迁移文档的主体是一份庞大的路由清单。概括其分类结构每条均标记 Path A/B 与完成状态App shell/account/*me / security / audit / tokens含 scoped。App shell/org/$slug/*index / apps / audit / audit-log-drains / billing / documents / general / integrations / security / sso / team / usage / private-apps / webhooks含$endpointId外加_app/org/index.tsx重定向页。其中 audit-log-drains 等页面内联 OrganizationSettingsLayout。App shell 顶层页面organizations.tsx、_app/new/index.tsx内联 WizardLayoutstaticData设defaultLayoutHeaderTitle: New organizationhideMobileMenu: true、new/$slug.tsx、aws-marketplace-onboarding.tsx、claim-project.tsx、join.tsx、_app/support/new.tsx、_app/support/link.tsx。后三者因页面自带独立布局自绘Head/main/居中 div而放在根目录避免被 AppLayout 包裹造成行为变化。integrationsVercel 的 install / marketplace choose-project / deploy-button new-project以及 GitHub authorize原 Next 页面无 getLayout故放顶层避免行为变化。Project shell下按产品线组织home、/api/*、/database/*schemas/extensions/functions/indexes/migrations/policies/roles/settings/types/column-privileges/tables/publications/replication/triggers/backups、/auth/*、/storage/*、/realtime/*、/workers/*、/functions/*、/branches/*、/logs/*约 20 个子页含 explorer、/observability/*、/advisors/*、/settings/*、/integrations/*、/sql/*、/editor/*、/explorer/*。Auth shellsign-in、sign-up、sign-in-sso、sign-in-partner、sign-in-mfa、forgot-password(mfa)、reset-password、cli/login、partners/stripe/projects/login。Standalone无共享 shellroutes/index.tsx纯重定向根路由镜像next.config.ts中redirects()逻辑平台版进/org、deep-link?nextnew-project进/new/new-project、self-hosted 进/project/defaultauthorize / redeem / logout / maintenance / verify-email。错误页__root.tsx的notFoundComponent接到pages/404.tsxerrorComponent接到pages/500.tsx并保留react-error-boundary的 Sentry 上报scope.setTag(routerErrorComponent, true)使路由级错误在树内 boundary 挂载前的 loader/组件渲染失败仍被记录。这里有一个值得注意的文件命名决策迁移文档 Deferred / revisit 部分两个 catch-allorg 与 project 的[[...routeSlug]]落地时没有用routes/org/[_]/index.tsx这种 index-file 形式而是采用path-as-filename形式routes/org.[_].tsxroutes/org.[_].$.tsx。原因是一个 router-generator 的 bug当index.tsx含方括号转义的父段时getRouteNodes.js会把originalRoutePath整个抹掉、丢失转义信息导致_被当作 pathless 段剥离。path-as-filename 形式让末段保持非 index绕开该 bug 分支。这也是为什么根目录下会出现org.[_].tsx、project.[_].tsx这类非常规文件名。五、API 路由迁移shim re-export 与toWebHandlerStudio 有大量 Next.js API routespages/api/**。迁移文档的 API 策略是shim re-exportcompat/next/api.ts暴露toWebHandler(nextHandler)把(req, res) …形态的 Next handler 适配成 TanStack Start 的 Web-fetch handler每个routes/api/...文件导入pages/api/...的 default export包一层toWebHandler后用createFileRoute(...).server.handlers注册。apiWrapper与apiAuthenticate原样不动它们在 shim 内执行看到的是 NextApiRequest 形状的req与代理res。路径约定对应仓库中routes/api/真实文件pages/api/foo/[bar]/baz.ts → routes/api/foo/$bar/baz.ts pages/api/foo/[[...slug]].ts → routes/api/foo/$.tsshim 覆盖的能力面从 apps/studio/compat/next/api.ts 源码看buildRequest/buildResponse完整复刻了 pages-router handler 的两类用法Buffered 响应res.status/setHeader/json/send/write/end累积进缓冲handler 返回时finalize()拼装成一个Response。Streaming 响应handler 调用res.writeHead(status, headers?)或res.flushHeaders()即切入流模式——打开 WebReadableStream先把已缓冲的 chunk 冲入后续res.write(chunk)实时入队res.end()关闭。finalize()先返回Responsehandler 随后仍可继续推 chunk。这正是 AI SDK 的result.pipeUIMessageStreamToResponse(res, …)能逐 token 流式输出到浏览器的原因。客户端 abortWebRequest.signal被接通为req.on(close | aborted, …)——依赖这些事件调用abortController.abort()的 AI handler 照常工作。EventEmitter 表面req.on/once/off/emit中只有close/aborted真实其他事件名被接受但 no-op。res.on等为 no-op stub避免 pipe 辅助函数挂drain/close/error监听时崩溃。Body 解析JSON 与application/x-www-form-urlencoded解析进req.body其余一律 raw textmultipart 入站未实现studio 没有读取 multipart 的 handler。绕过 shim 的特例迁移文档明确列出两条因Web 原生写更简单而绕过 shim 的路由仓库routes/api/下可直接对照routes/api/v1/projects/$ref/functions/$slug/body.tsmultipart 流式出站产物下载。把 Response body 构建为ReadableStream每个产物文件经Readable.toWeb(createReadStream(...))转换后逐 chunk 拉入流。routes/api/mcp/index.ts直接使用 MCP SDK 的WebStandardStreamableHTTPServerTransport——handleRequest(request)直接返回Response。pages/api/ai/docs.ts本来就是 edge-runtime / Web-Response 原生直接 re-export无 shim。六、Next 兼容 shim 面compat/next/全貌只要还有pages/...文件承重这些 shim 就必须活着。仓库 apps/studio/compat/next/ 目录结构即文档所列 shim 的落地router.ts——为 hook 调用方提供useRouter()由 TanStack 的useRouteruseLocationuseMatchesuseParamsuseSearch拼装另有 default exportSingletonRouter形状给唯一一个模块作用域import router from next/router的消费方Support/DiscordCTACard 在 React 外读router.basePath。源码注释里展示了大量语义对齐细节TanStack route id 的$param要转回 Next 的[param]路径模式要剥掉 TanStack 给 index route 追加的尾部斜杠——否则router.pathname.split(/)[3]对 index 页返回而非undefined项目侧边栏的高亮判断就失效还要剥掉_app/_auth等 layout 段否则下游按pathname.split(/)[N]取段会错位。_router-events.ts——把router.events.on(event, handler)适配到router.subscribe(tsEvent, …)转发 Next 的(url, { shallow })参数映射routeChangeStart/routeChangeComplete/beforeHistoryChange/hashChangeStart/hashChangeComplete。已知缺口Next 从 routeChangeStart 抛异常来取消导航的模式无法支持subscribe是 fire-and-forget依赖它的usePreventNavigationOnUnsavedChanges需要另行迁移到 TanStack 的useBlocker列入清理清单。api.ts——toWebHandler(nextHandler)即上一节的 API shim。link.tsx、navigation.ts、dynamic.tsx、image.tsx、legacy/image.tsx、script.tsx、head.tsx、server.ts——对 studio 所 import 的next/*模块做 drop-in 替换。全部经 apps/studio/vite.config.ts 中的nextCompat()插件 alias 收口并配ssr.noExternal: [/^next(\/|$)/]保证 shim 一定胜过真实 Next 包。nextCompat()插件vite.config.ts中定义本身还承担迁移守门人角色应用源码若 import 一个未被 shim 的next/*id构建直接抛错提示去补 shim 或用框架无关替代node_modules内 import如sentry/nextjs探入 next不受影响。也就是说构建期就能拦截任何漏网的 Next import。七、构建 / 打包层的坑与 workaroundapps/studio/vite.config.ts 是本次迁移工程量最大的单文件承载了六类为迁移而存在的构建期防护。1.manualChunks固定消除 chunk 级循环依赖Rolldown 对 studio 庞大依赖图的分块方式会产生chunk 级循环——组件 chunk 从会传递地反向 import 它的 chunk 里导入了绑定浏览器端表现为模块加载期的TypeError: name is not a function。配置里的固定项class-variance-authority——TreeView 单独成 chunk 并从uichunk importcva而ui又反向 re-export TreeView导致 TreeView 顶层cva(...)在 SSR 预渲染时拿到 undefinedlucide-react——防止图标被按图标拆成 importcreateLucideIcon的 chunkcanaryfolder-open-hash.jsreact-vendorreact react-dom scheduler jsx-runtime——必须先于lucide-react固定否则 Rolldown 会把 React 卷进 lucide chunk 做 CJS interop把 live-binding 打散到整个依赖图canaryAlert-hash.js。2.assertNoChunkCycles插件把循环变成构建错误该插件在generateBundle阶段对产物 chunk 图跑Tarjan 强连通分量SCC发现任何未知 chunk 循环就 fail 构建。历史遗留的 CVA 循环按 chunk basename 白名单放行KNOWN_CHUNK_CYCLES常量其中可看到[LoadingLine, TreeView, ui]等变体任何新增循环都会被阻断并提示去vite.config.ts注册。文档特别强调这个插件迁移完成后也应保留——它不是 Next 相关 shim而是对整类 bug 的通用防护只需在底层循环消除后清空白名单。3.sentry/nextjs→sentry/reactaliasresolve.alias把裸sentry/nextjs导入重写到 compat/sentry-nextjs.ts后者 re-exportsentry/react同版本sentry/nextjs客户端本就包裹它并补了 Next-only API 的显式替身captureRouterTransitionStart、captureRequestError、withSentryConfig。原因sentry/nextjs客户端入口 import 了next/dist/shared/lib/constants其模块作用域会求值...(process?.features?.typescript ? [next.config.mts] : [])——可选链保护不了未声明的process标识符导致每个含它的客户端 chunkcanary表格编辑器在模块加载时抛ReferenceError: process is not defined。dev 不受影响dev 管线 shim 了process只在生产/测试构建暴露。应用源码继续 importsentry/nextjs因此 Next 构建build:next不受影响。4. GraphiQL Monaco workersetup-workers/webpack→setup-workers/viteGraphiQLTab.tsx源码 import 的是graphiql/setup-workers/webpack它用new Worker(new URL(monaco-editor/..., import.meta.url))注册MonacoEnvironment.getWorker——这是 webpack/turbopack 会在构建期重写的 URL 形式Vite 不会改写new URL里裸模块说明符worker URL 404 后 Monaco 退回主线程跑 json/editorWorkerService/graphql worker控制台出现 Could not create web worker(s)... 警告。graphiqlViteWorkers插件在客户端构建把该 import 解析到 graphiql 自带的setup-workers/vite变体同三个 worker走 Vite?workerimportSSR 解析不受影响应用源码的 import 说明符保持.../webpack以保 Next 构建。整个 setup-workers 链还在optimizeDeps.exclude——Rolldown 依赖预构建加载不了?workeridUNLOADABLE_DEPENDENCY须让模块走常规 transform 管线由 Vite 内置 worker 插件处理。5. Raw-text imports*.mdpublic/deno/*.d.tsrawTextLoaderNext 侧由next.config.ts的turbopack.rules把*.md与 Deno 类型文件按 raw text 模块提供rawTextLoader插件为 Vite 管线复刻该行为*.md普通transformdefault export 文件文本用于static-data/integrations/*/overview.md经static-data/integrations/overviews.ts的 literal-import registry 引用两个 Deno.d.tspublic/deno/edge-runtime.d.ts、public/deno/lib.deno.d.ts被components/ui/AIEditor作为 Monaco extra libs用精确说明符白名单解析到\0-virtual id 并由loadhook 提供文本。它们不能走transform——Rolldown 原生依赖扫描器会跳过 JS 插件 hook把 TSdeclaration语法如get stdin(): ...;当运行时代码硬解析而整体失败连带整个依赖预构建崩溃。注释与文档共同强调绝不要把白名单放宽到*.d.ts——全局劫持声明文件解析会破坏所有JS 旁带.d.ts的包。AIEditor/index.tsx里的as string强转则让 tsc 不去把.d.ts当声明文件解析TS2846擦除后两个 bundler 都能静态分析为普通字面量。6. 其他迁移期构建改动pnpm-workspace.yaml的 catalog 新增tanstack/react-router、tanstack/react-start、tanstack/react-table让 studio 与库保持对齐react-query暂不入 catalog——studio/docs/library 三个消费方在 5.x 不同 range统一是单独决策。studio 的dev:tanstack脚本设了NODE_OPTIONS--max-old-space-size8192——watch 模式下 Vite 的 Rolldown-RC 前端啃 studio 模块图会顶到默认 4 GB 上限该配置同样可见于 apps/studio/package.json。八、清理清单收尾阶段的路线图当每个pages/...文件都被删除后文档列出清理项内部跟踪 FE-3106可视为迁移完成的定义routes/index.tsx的重定向从href整页刷新改为to——目标现已全部在 TanStack 树内usePreventNavigationOnUnsavedChanges从router.events.on(routeChangeStart, …)的 throw-to-cancel 模式迁移到 TanStackuseBlocker删除两个 catch-all 页中的_splat/routeSlug归一化块仅为让两运行时挂载同一 body 而存在从__root.tsx移除RouteValidationWrapper与next/routercompat shim 使用compat/next/目录整体删除当工作区源码不再有next/*import 时解除manualChunks固定class-variance-authority、lucide-react、react-vendor——前提是packages/ui的结构性修复落地assertNoChunkCycles保留仅清空KNOWN_CHUNK_CYCLES删除pages/_app.tsx、pages/_document.tsx、pages/_error.jsx、pages/500.tsx、pages/404.tsxNext-only catch-allTanStack 等价物在__root.tsx从 apps/studio/package.json 移除dev:next/build:next/start:next脚本移除 .coderabbit.yaml 中apps/studio/pages/**的path_instructionsguardrail从 apps/studio/AGENTS.md 删除 TanStack Start migration 一节仅双运行时共存期适用删除本迁移文档自身。九、从这份迁移清单可以复用哪些方法论阅读 TANSTACK_MIGRATION.md 最大的收获是它把大规模前端框架迁移拆成了可逐步验证的工程步骤双运行时 逐路由所有权移交老代码继续承重新树逐步接管 URL用最小 diff 的 re-export先把 URL 归属切换过来body 迁移推迟到单独阶段——避免单次重构同时承担路由语义变化与组件内部重写两个风险。以布局链重构替代getLayout共享 shell 落地前置、每个产品布局一个 sibling-file layout用staticData承载标题/隐藏菜单/跳过某层布局等页面元数据二次包裹double-wrap是被反复识别并规避的头号反模式——连withAuth都会随之双跑。API 兼容层做能力面分析而非逐函数重写toWebHandler清楚列出 buffered / streaming / client-abort / EventEmitter 表面 / body 解析这五类 pages-router handler 真实用到的能力再决定哪些能 shim、哪些必须 Web 原生重写multipart 下载、MCP transport。把构建期隐患变成构建期错误unshimmed Next import 直接报错、chunk 循环用 Tarjan SCC 扫描 fail 构建、Sentry/worker/raw-text 等坑全部沉淀为带 canary 例子的注释——这些注释本身就是一份极好的迁移踩坑手册。把完成定义成可勾选的清理清单兜底脚本、shim、guardrail、catalog 变更全部有明确去处避免迁移结束留一堆死代码。如果你正计划把大型 Next.js Pages Router 应用迁到 TanStack Start建议先通读这份文档的运行时模型与共享布局章节再对照 apps/studio/vite.config.ts 的各插件注释和 apps/studio/compat/next/ 的实现理解每一条规则背后对应的真实故障模式——迁移的成败往往不取决于路由文件搬得多快而取决于这些边界与兜底是否提前想清楚。【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考