news 2026/9/8 20:45:58

Supabase Studio 从 Next.js Pages Router 到 TanStack Start 的渐进式迁移实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Supabase Studio 从 Next.js Pages Router 到 TanStack Start 的渐进式迁移实践

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/supabase

Supabase Studio 是 Supabase 的开源控制台(Dashboard)应用(代码位于 apps/studio),源码规模庞大,历史代码基于 Next.js Pages Router 编写。为了把前端运行时迁移到 Vite + TanStack Start(TanStack 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-size=8192 vite dev --port ${STUDIO_PORT:-8082}", "build:tanstack":"vite build --mode ${MODE:-production} && pnpm smoke:tanstack", "start:tanstack":"node scripts/serve.js",

迁移期间的三条铁律

  1. 绝不删除任何apps/studio/pages/...文件。Path A 页面(下文详解)是从pages/中 re-export 默认导出,Next 文件对两个运行时都是"承重墙":删了既破坏 Next 构建,也破坏对应 TanStack 路由。
  2. 页面 body 的移动与pages/...删除只允许发生在最后的 cleanup pass——在所有路由都已在routes/...表示、准备彻底退役 Next 运行时之后,这是独立、刻意的阶段,不能揉进单个路由 PR。
  3. Next 兼容 shim(apps/studio/compat/next/)同样保留到 cleanup pass,与迁移进度无关。

PR 护栏:用 CodeRabbit 规则强制双写检查

因为两套运行时同时发布,任何对pages/...文件的改动都可能让routes/...里的镜像失同步。仓库根目录的 .coderabbit.yaml 中定义了一条作用域为apps/studio/pages/**path_instructions规则:任何触碰 page 的 PR,CodeRabbit 都会提醒作者检查对应 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.tsxroutes/authorize.tsx(standalone 页面),以及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/routernext/link);Next compat shim 仅为被 re-export 的旧页面保留;
  • withAuth()HOC 转换为布局/路由上的 TanStackbeforeLoad,尽量在共享布局层级统一处理;
  • 绝不在迁移中途删除pages/...
  • 本清单不覆盖:pages/api/**(Next API 路由,单独迁移)、_app.tsx_document.tsx_error、以及两个 catch-all(pages/org/_/[[...routeSlug]].tsxpages/project/_/[[...routeSlug]].tsx,最后专门处理)。

三、布局体系的重建:shell 目录逐层拆解

迁移文档对布局的落位标注了非常细致的"Delta vs plan"(相对原计划的偏差),这些偏差是理解 Studio 页面组合关系的关键。

App shell(pathless 层,账号/组织/通用页面)

布局文件内容与计划的偏差(Delta)
routes/_app.tsxAppLayout + DefaultLayout(读取叶子staticDatadefaultLayoutHeaderTitle/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 下(内联 WizardLayout)new/$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,用于避免二次包裹(二次包裹会连带withAuth+ProjectLayout双跑):
Shell布局特殊机制
database.tsxDatabaseLayout(读databaseLayoutTitle
database/triggers.tsx子 shell:PageLayout + 权限门 + nav 内联自DatabaseTriggersLayout复用原组件会在database.tsx外壳内再包一层 DatabaseLayout(二次包裹),故只内联其内层
auth.tsxAuthLayout(读authLayoutTitle支持叶子skipAuthLayout: trueAuthProvidersLayoutAuthEmailsLayout内部已自包 AuthLayout)
storage.tsxStorageLayout + StorageBucketsLayout默认两层都包;bucket 详情页设skipStorageBucketsLayout: true/storage/s3storageBucketsLayout{Title,HideSubtitle}覆盖内层头部
functions.tsxEdgeFunctionsLayout支持skipFunctionsLayout: truefunctions/$functionSlug.tsx子 shell 提供 EdgeFunctionDetailsLayout 给 5 个 slug 叶子
branches.tsx仅 BranchLayoutper-page 的 PageLayout 留在各叶子;BranchesPageWrapper/MergeRequestsPageWrapper提升为pages/...文件顶层导出供 route 复用
logs.tsxLogsLayoutlogs/indexskipLogsLayout: true(UnifiedLogs 自己处理 ProjectLayout);原pages/.../logs/index.tsx把内联<DefaultLayout>移入getLayout避免重复
advisors.tsxAdvisorsLayout支持skipAdvisorsLayout: true;rules 子 shell 扫描整个 match 链
advisors/rules.tsx子 shell,内联AdvisorRulesLayout内层原组件自带 DefaultLayout + AdvisorsLayout,复用会双包两层,故只内联内层
settings.tsxSettingsLayout支持skipSettingsLayout: truesettings/api纯重定向页);settings/api-keys.tsx子 shell 提供 ApiKeysLayout;jwt/index内联 JWTKeysLayout
integrations.tsxProjectIntegrationsLayout4 个叶子共享同一布局,shell 只包一次<Outlet/>
sql.tsxEditorBaseLayout + SQLEditorLayout四个叶子布局 props 相同,外壳硬编码;EditorBaseLayout 自带 ProjectLayoutWithAuth,SQLEditorLayout 另有withAuthHOC(认证跑两次但不重复渲染)
editor.tsxEditorBaseLayout + TableEditorLayout三个叶子共享;TableEditorLayout happy path 只是 fragment + banner,仅在无权限分支才包 ProjectLayoutWithAuth

Auth shell(pathless):routes/_auth.tsx提供 AuthenticationLayout,承载/sign-in/sign-up、MFA、找回密码、合作伙伴登录等认证相关页面。

staticData传递页面元数据

上述布局反复出现一个关键词:staticData。TanStack Router 允许在 route 上声明静态数据,叶子路由通过staticData声明标题类 props(databaseLayoutTitleauthLayoutTitleorgLayoutTitlehideMobileMenu等),父级 shell 布局读取后决定渲染什么标题/菜单。这是对 NextNextPageWithLayout+getLayout模式的直接替代——迁移时把页面标题、是否隐藏移动端菜单、是否跳过某层布局等"页面级装饰信息"统一挪进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(内联 WizardLayout,staticDatadefaultLayoutHeaderTitle: 'New organization'+hideMobileMenu: true)、new/$slug.tsxaws-marketplace-onboarding.tsxclaim-project.tsxjoin.tsx_app/support/new.tsx_app/support/link.tsx。后三者因页面自带独立布局(自绘<Head>/<main>/居中 div)而放在根目录,避免被 AppLayout 包裹造成行为变化。
  • integrations:Vercel 的 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 shell:sign-in、sign-up、sign-in-sso、sign-in-partner、sign-in-mfa、forgot-password(+mfa)、reset-password、cli/login、partners/stripe/projects/login。
  • Standalone(无共享 shell):routes/index.tsx(纯重定向根路由,镜像next.config.tsredirects()逻辑:平台版进/org、deep-link?next=new-project/new/new-project、self-hosted 进/project/default);authorize / redeem / logout / maintenance / verify-email。
  • 错误页__root.tsxnotFoundComponent接到pages/404.tsxerrorComponent接到pages/500.tsx,并保留react-error-boundary的 Sentry 上报(scope.setTag('routerErrorComponent', true)),使路由级错误(在树内 boundary 挂载前的 loader/组件渲染失败)仍被记录。

这里有一个值得注意的文件命名决策(迁移文档 "Deferred / revisit" 部分):两个 catch-all(org 与 project 的[[...routeSlug]])落地时没有用routes/org/[_]/index.tsx这种 index-file 形式,而是采用path-as-filename形式routes/org.[_].tsx+routes/org.[_].$.tsx。原因是一个 router-generator 的 bug:当index.tsx含方括号转义的父段时,getRouteNodes.js会把originalRoutePath整个抹掉、丢失转义信息,导致_被当作 pathless 段剥离。path-as-filename 形式让末段保持非 index,绕开该 bug 分支。这也是为什么根目录下会出现org.[_].tsxproject.[_].tsx这类"非常规"文件名。

五、API 路由迁移:shim + re-export 与toWebHandler

Studio 有大量 Next.js API routes(pages/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注册。apiWrapperapiAuthenticate原样不动,它们在 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/$.ts

shim 覆盖的能力面

从 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()先返回Response,handler 随后仍可继续推 chunk。这正是 AI SDK 的result.pipeUIMessageStreamToResponse(res, …)逐 token 流式输出到浏览器的原因。
  • 客户端 abort:WebRequest.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 text;multipart 入站未实现(studio 没有读取 multipart 的 handler)。

绕过 shim 的特例

迁移文档明确列出两条因"Web 原生写更简单"而绕过 shim 的路由,仓库routes/api/下可直接对照:

  • routes/api/v1/projects/$ref/functions/$slug/body.ts:multipart 流式出站(产物下载)。把 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 的useRouter+useLocation+useMatches+useParams+useSearch拼装),另有 default export(SingletonRouter形状)给唯一一个模块作用域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.tsxnavigation.tsdynamic.tsximage.tsxlegacy/image.tsxscript.tsxhead.tsxserver.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

七、构建 / 打包层的坑与 workaround

apps/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 预渲染时拿到 undefined;
  • lucide-react——防止图标被按图标拆成 importcreateLucideIcon的 chunk(canary:folder-open-<hash>.js);
  • react-vendor(react + react-dom + scheduler + jsx-runtime)——必须先于lucide-react固定,否则 Rolldown 会把 React 卷进 lucide chunk 做 CJS interop,把 live-binding 打散到整个依赖图(canary:Alert-<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/reactalias

resolve.alias把裸@sentry/nextjs导入重写到 compat/sentry-nextjs.ts,后者 re-export@sentry/react(同版本,@sentry/nextjs客户端本就包裹它),并补了 Next-only API 的显式替身(captureRouterTransitionStartcaptureRequestErrorwithSentryConfig)。原因:@sentry/nextjs客户端入口 import 了next/dist/shared/lib/constants,其模块作用域会求值...(process?.features?.typescript ? ['next.config.mts'] : [])——可选链保护不了未声明的process标识符,导致每个含它的客户端 chunk(canary:表格编辑器)在模块加载时抛ReferenceError: process is not defined。dev 不受影响(dev 管线 shim 了process),只在生产/测试构建暴露。应用源码继续 import@sentry/nextjs,因此 Next 构建(build:next)不受影响。

4. GraphiQL Monaco worker:setup-workers/webpacksetup-workers/vite

GraphiQLTab.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?workerimport),SSR 解析不受影响;应用源码的 import 说明符保持.../webpack以保 Next 构建。整个 setup-workers 链还在optimizeDeps.exclude——Rolldown 依赖预构建加载不了?workerid(UNLOADABLE_DEPENDENCY),须让模块走常规 transform 管线由 Vite 内置 worker 插件处理。

5. Raw-text imports:*.md+public/deno/*.d.tsrawTextLoader

Next 侧由next.config.tsturbopack.rules*.md与 Deno 类型文件按 raw text 模块提供;rawTextLoader插件为 Vite 管线复刻该行为:

  • *.md:普通transform,default export 文件文本(用于static-data/integrations/*/overview.mdstatic-data/integrations/overviews.ts的 literal-import registry 引用);
  • 两个 Deno.d.tspublic/deno/edge-runtime.d.tspublic/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-size=8192——watch 模式下 Vite 的 Rolldown-RC 前端啃 studio 模块图会顶到默认 4 GB 上限(该配置同样可见于 apps/studio/package.json)。

八、清理清单:收尾阶段的路线图

当每个pages/...文件都被删除后,文档列出清理项(内部跟踪 FE-3106),可视为"迁移完成的定义":

  1. routes/index.tsx的重定向从href(整页刷新)改为to——目标现已全部在 TanStack 树内;
  2. usePreventNavigationOnUnsavedChangesrouter.events.on('routeChangeStart', …)的 throw-to-cancel 模式迁移到 TanStackuseBlocker
  3. 删除两个 catch-all 页中的_splat/routeSlug归一化块(仅为让两运行时挂载同一 body 而存在);
  4. __root.tsx移除RouteValidationWrappernext/routercompat shim 使用;
  5. compat/next/目录整体删除(当工作区源码不再有next/*import 时);
  6. 解除manualChunks固定(class-variance-authoritylucide-reactreact-vendor)——前提是packages/ui的结构性修复落地;assertNoChunkCycles保留,仅清空KNOWN_CHUNK_CYCLES
  7. 删除pages/_app.tsxpages/_document.tsxpages/_error.jsxpages/500.tsxpages/404.tsx(Next-only catch-all;TanStack 等价物在__root.tsx);
  8. 从 apps/studio/package.json 移除dev:next/build:next/start:next脚本;
  9. 移除 .coderabbit.yaml 中apps/studio/pages/**path_instructionsguardrail;
  10. 从 apps/studio/AGENTS.md 删除 "TanStack Start migration" 一节(仅双运行时共存期适用);
  11. 删除本迁移文档自身。

九、从这份迁移清单可以复用哪些方法论

阅读 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 20:45:44

LightGBM实战:Learning to Rank排序学习全流程解析

简介&#xff1a;这是一份利用LightGBM实现Learning to Rank排序学习的完整项目实践&#xff0c;面向推荐系统、搜索引擎等场景的数据科学开发者与算法学习者&#xff0c;尤其适合对排序学习、搜索排序或推荐召回排序有需求的初中级工程师。项目内容覆盖数据预处理、模型训练、…

作者头像 李华
网站建设 2026/9/8 20:45:35

RPCS3 汉化实战教程:10 分钟装好中文补丁的新手完整指南

RPCS3 汉化实战教程&#xff1a;10 分钟装好中文补丁的新手完整指南 【免费下载链接】rpcs3 PlayStation 3 emulator and debugger 项目地址: https://gitcode.com/GitHub_Trending/rp/rpcs3 想给 PS3 模拟器加中文&#xff1f;这篇 RPCS3 汉化教程带你走完整条路&#…

作者头像 李华
网站建设 2026/9/8 20:44:28

TRL完整教程:从零开始掌握大模型微调,SFT/GRPO/DPO一次跑通

TRL完整教程&#xff1a;从零开始掌握大模型微调&#xff0c;SFT/GRPO/DPO一次跑通 【免费下载链接】trl Train transformer language models with reinforcement learning. 项目地址: https://gitcode.com/GitHub_Trending/tr/trl 想让模型学会解题、学会你的写作风格、…

作者头像 李华
网站建设 2026/9/8 20:43:01

FastAPI 条件化 OpenAPI:用环境变量按需启用与禁用接口文档

FastAPI 条件化 OpenAPI&#xff1a;用环境变量按需启用与禁用接口文档 【免费下载链接】fastapi FastAPI framework, high performance, easy to learn, fast to code, ready for production 项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi 导读 在生产环…

作者头像 李华
网站建设 2026/9/8 20:41:32

从vibe coding到SDD:我用AI Agent开发npm中文排版包的经验

1. 为什么我不再"一句话甩给 AI"&#xff0c;改回先写规格再写代码vibe coding 刚火的那阵&#xff0c;我的节奏基本是&#xff1a;在聊天窗口里描述一个需求&#xff0c;AI 直接吐出一坨代码&#xff0c;我粘贴、运行、报错、继续让它改。做两三屏的小脚本还好&…

作者头像 李华