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

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

2026-09-07 11:40:53作者:乔或婵

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 包装、staticData props、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 转换为布局/路由上的 TanStack beforeLoad,尽量在共享布局层级统一处理;
  • 绝不在迁移中途删除 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.tsx AppLayout + DefaultLayout(读取叶子 staticDatadefaultLayoutHeaderTitle / hideMobileMenu
routes/_app/account.tsx AccountLayout(读 accountLayoutTitle
routes/_app/org.tsx OrganizationLayout(读 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.tsx DatabaseLayout(读 databaseLayoutTitle
database/triggers.tsx 子 shell:PageLayout + 权限门 + nav 内联自 DatabaseTriggersLayout 复用原组件会在 database.tsx 外壳内再包一层 DatabaseLayout(二次包裹),故只内联其内层
auth.tsx AuthLayout(读 authLayoutTitle 支持叶子 skipAuthLayout: trueAuthProvidersLayoutAuthEmailsLayout 内部已自包 AuthLayout)
storage.tsx StorageLayout + StorageBucketsLayout 默认两层都包;bucket 详情页设 skipStorageBucketsLayout: true/storage/s3storageBucketsLayout{Title,HideSubtitle} 覆盖内层头部
functions.tsx EdgeFunctionsLayout 支持 skipFunctionsLayout: truefunctions/$functionSlug.tsx 子 shell 提供 EdgeFunctionDetailsLayout 给 5 个 slug 叶子
branches.tsx 仅 BranchLayout per-page 的 PageLayout 留在各叶子;BranchesPageWrapper / MergeRequestsPageWrapper 提升为 pages/... 文件顶层导出供 route 复用
logs.tsx LogsLayout logs/indexskipLogsLayout: true(UnifiedLogs 自己处理 ProjectLayout);原 pages/.../logs/index.tsx 把内联 <DefaultLayout> 移入 getLayout 避免重复
advisors.tsx AdvisorsLayout 支持 skipAdvisorsLayout: true;rules 子 shell 扫描整个 match 链
advisors/rules.tsx 子 shell,内联 AdvisorRulesLayout 内层 原组件自带 DefaultLayout + AdvisorsLayout,复用会双包两层,故只内联内层
settings.tsx SettingsLayout 支持 skipSettingsLayout: truesettings/api 纯重定向页);settings/api-keys.tsx 子 shell 提供 ApiKeysLayout;jwt/index 内联 JWTKeysLayout
integrations.tsx ProjectIntegrationsLayout 4 个叶子共享同一布局,shell 只包一次 <Outlet/>
sql.tsx EditorBaseLayout + SQLEditorLayout 四个叶子布局 props 相同,外壳硬编码;EditorBaseLayout 自带 ProjectLayoutWithAuth,SQLEditorLayout 另有 withAuth HOC(认证跑两次但不重复渲染)
editor.tsx EditorBaseLayout + 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 布局读取后决定渲染什么标题/菜单。这是对 Next NextPageWithLayout + 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())即切入流模式——打开 Web ReadableStream,先把已缓冲的 chunk 冲入,后续 res.write(chunk) 实时入队,res.end() 关闭。finalize() 先返回 Response,handler 随后仍可继续推 chunk。这正是 AI SDK 的 result.pipeUIMessageStreamToResponse(res, …)逐 token 流式输出到浏览器的原因。
  • 客户端 abort:Web Request.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 并从 ui chunk import cva,而 ui 又反向 re-export TreeView,导致 TreeView 顶层 cva(...) 在 SSR 预渲染时拿到 undefined;
  • lucide-react——防止图标被按图标拆成 import createLucideIcon 的 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/react alias

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 ?worker import),SSR 解析不受影响;应用源码的 import 说明符保持 .../webpack 以保 Next 构建。整个 setup-workers 链还在 optimizeDeps.exclude——Rolldown 依赖预构建加载不了 ?worker id(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 并由 load hook 提供文本。它们不能走 transform——Rolldown 原生依赖扫描器会跳过 JS 插件 hook,把 TS declaration 语法(如 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 模式迁移到 TanStack useBlocker
  3. 删除两个 catch-all 页中的 _splat/routeSlug 归一化块(仅为让两运行时挂载同一 body 而存在);
  4. __root.tsx 移除 RouteValidationWrappernext/router compat 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.yamlapps/studio/pages/**path_instructions guardrail;
  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/ 的实现,理解每一条规则背后对应的真实故障模式——迁移的成败往往不取决于路由文件搬得多快,而取决于这些"边界与兜底"是否提前想清楚。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390