Better Auth 集成 TanStack Start:从虚拟模块报错到tanstackStartCookies的正确姿势
【免费下载链接】better-authThe most comprehensive authentication framework项目地址: https://gitcode.com/GitHub_Trending/be/better-auth
Better Auth 在将认证能力接入 TanStack Start 时,曾因直接导入内部包@tanstack/start-server-core而触发 Vite 预打包(pre-bundling)失败,报错Could not resolve "#tanstack-router-entry"。本文以仓库中的 Postmortem 复盘文档 为核心,梳理该问题的根因、正确导入路径、用户侧临时规避方案,并结合 官方集成文档 与 插件源码,完整演示如何在 React / Solid.js 两种 TanStack Start 应用里配置认证、自动写入 Cookie 并保护路由与 Server Function。读完本文,你将能独立规避此类虚拟模块陷阱,并正确使用 Better Auth 的 TanStack Start 集成。
背景:一个反复出现的 Vite 预打包报错
问题现象
当 Better Auth 的 TanStack Start 集成代码直接执行如下导入时:
// BAD - 不要这样做 import { setCookie } from "@tanstack/start-server-core";Vite 在开发服务器启动或依赖预构建阶段会对@tanstack/start-server-core执行预打包(optimizeDeps),而该包内部依赖虚拟模块(virtual modules)#tanstack-router-entry,这类模块无法被常规的依赖扫描与打包流程解析,最终抛出错误:
Could not resolve "#tanstack-router-entry"复发历史:为什么问题会反复出现
复盘文档记录了该问题的多次复发,均由“同时支持 Solid 与 React 两种 TanStack Start 版本”的 PR 引入:
- PR #6045
- PR #6235
- PR #7340(对应 v1.4.12 → v1.4.13 版本区间)
其中 v1.4.14 的首次修复尝试采用了独立的@tanstack/react-start-server与@tanstack/solid-start-server包,但并未解决问题,这为后续找到正确方案提供了关键教训:解决路径不在“换一个包名”,而在于“换一个子路径”。
根因:内部包的 Vite 配置与虚拟模块
@tanstack/start-server-core是 TanStack 的内部包,它自带特殊的 Vite 配置,将诸如#tanstack-router-entry这样的虚拟模块声明为外部依赖(external)。虚拟模块在构建期由 Vite 插件按需生成,无法被当作普通 npm 依赖预先打包。因此一旦 Better Auth 集成模块(或任何第三方代码)直接 import 该包,依赖预构建便会在解析虚拟模块时失败。
从当前仓库源码看,Better Auth 的 TanStack Start Cookie 插件会动态导入框架专属的 server 子路径,而不再触碰内部包:
- React 版本:tanstack-start.ts 中
await import("@tanstack/react-start/server") - Solid.js 版本:tanstack-start-solid.ts 中
await import("@tanstack/solid-start/server")
这种“延迟动态导入”还带来一个额外好处:插件只在真正需要写入 Cookie(即认证响应产生set-cookie头)时才加载对应框架的 server 模块,避免了在模块加载阶段就触发依赖解析。
解决方案:使用框架专属的/server子路径
正确做法是从主包的/server子路径导入,该子路径会完整地重新导出(re-export)@tanstack/start-server-core中的所有 API:
// GOOD - 适用于 React import { setCookie } from "@tanstack/react-start/server"; // GOOD - 适用于 Solid.js import { setCookie } from "@tanstack/solid-start/server";注意:独立的@tanstack/react-start-server与@tanstack/solid-start-server包并不能修复此问题。必须使用主包(@tanstack/react-start/@tanstack/solid-start)的/server子路径。
这一点在当前仓库的依赖声明中也能印证:packages/better-auth/package.json 的 peerDependencies 与 devDependencies 中声明的是@tanstack/react-start(^1.168.4)与@tanstack/solid-start(^1.168.4),二者均标记为可选 peer 依赖,而不是*-start-server系列包。
用户侧临时规避方案
如果你在官方修复发布前就遇到了Could not resolve "#tanstack-router-entry",可以在项目的vite.config.ts中将该内部包从依赖预构建中排除:
optimizeDeps: { exclude: ['@tanstack/start-server-core'], },排除后 Vite 不再尝试预打包该包,虚拟模块的解析交由 TanStack Start 自身的插件链路处理,从而绕过报错。这是纯用户侧配置,无需修改任何依赖。
完整接入:React 版 TanStack Start
1. 挂载认证处理器
创建/src/routes/api/auth/$.ts,将 Better Auth 的auth.handler挂载到 TanStack 的 API 路由上:
import { auth } from '@/lib/auth' import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/api/auth/$')({ server: { handlers: { GET: async ({ request }:{ request: Request }) => { return await auth.handler(request) }, POST: async ({ request }:{ request: Request }) => { return await auth.handler(request) }, }, }, })2. 配置tanstackStartCookies插件
在src/lib/auth.ts中引入 Cookie 插件。文档明确要求:确保它位于 plugins 数组的最后一个。插件源码中通过warnIfCookiePluginNotLast(见 cookie-plugin-guard.ts)在运行时检测插件顺序,若顺序不当会发出警告——因为后续的after钩子需要先于它处理响应头,才能让 Cookie 写入逻辑拿到完整的set-cookie信息。
import { betterAuth } from "better-auth"; import { tanstackStartCookies } from "better-auth/tanstack-start"; export const auth = betterAuth({ //...your config plugins: [tanstackStartCookies()] // make sure this is the last plugin in the array })React 版插件内部逻辑(tanstack-start.ts)为:
- 通过
after钩子拦截每个认证请求的响应; - 从
ctx.context.responseHeaders中读取set-cookie头; - 用
parseSetCookieHeader解析出全部 Cookie 键值; - 动态导入
@tanstack/react-start/server的setCookie,配合toCookieOptions还原 Cookie 属性后逐个写入; - 若当前上下文标记为
router或写入发生在 Server Component 场景而抛错,则静默跳过。
3. 调用认证 API,Cookie 自动落盘
之后调用auth.api中需要写 Cookie 的方法(如signInEmail),Cookie 会被自动通过 TanStack Start 的 Cookie 系统写入:
import { auth } from "@/lib/auth" const signIn = async () => { await auth.api.signInEmail({ body: { email: "user@email.com", password: "password", } }) }完整接入:Solid.js 版 TanStack Start
Solid.js 应用流程完全相同,区别只在于插件入口与导入来源:
import { betterAuth } from "better-auth"; import { tanstackStartCookies } from "better-auth/tanstack-start/solid"; export const auth = betterAuth({ //...your config plugins: [tanstackStartCookies()] // make sure this is the last plugin in the array })对应地,Solid.js 版插件(tanstack-start-solid.ts)动态导入的是@tanstack/solid-start/server的setCookie,其余解析与写入逻辑与 React 版一致,插件 id 为tanstack-start-cookies-solid。
两个入口在 packages/better-auth/package.json 的exports中分别映射为:
better-auth/tanstack-start→./src/integrations/tanstack-start.tsbetter-auth/tanstack-start/solid→./src/integrations/tanstack-start-solid.ts
两者同时被登记在 tsdown.config.ts 的构建入口中,产出对应的 ESM 产物。
保护受认证资源
创建服务端会话辅助函数
使用createServerFn与getRequestHeaders(来自@tanstack/react-start/server,即上文所述的/server子路径)读取请求头并校验会话:
import { createServerFn } from "@tanstack/react-start"; import { getRequestHeaders } from "@tanstack/react-start/server"; import { auth } from "@/lib/auth"; export const getSession = createServerFn({ method: "GET" }).handler(async () => { const headers = getRequestHeaders(); const session = await auth.api.getSession({ headers }); return session; }); export const ensureSession = createServerFn({ method: "GET" }).handler(async () => { const headers = getRequestHeaders(); const session = await auth.api.getSession({ headers }); if (!session) { throw new Error("Unauthorized"); } return session; });保护单个路由
在路由定义中使用beforeLoad,确保每次导航(包括<Link>触发的客户端导航)都会校验会话:
import { createFileRoute, redirect } from '@tanstack/react-router' import { getSession } from '@/lib/auth.functions' export const Route = createFileRoute('/dashboard')({ beforeLoad: async () => { const session = await getSession(); if (!session) { throw redirect({ to: "/login" }); } return { user: session.user }; }, component: Dashboard, }) function Dashboard() { const { user } = Route.useRouteContext(); return <div>Welcome, {user.name}!</div> }用无路径 Layout 路由保护多个页面
先创建无路径布局路由_protected.tsx,未登录时重定向并携带当前地址作为回跳参数:
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router' import { getSession } from '@/lib/auth.functions' export const Route = createFileRoute('/_protected')({ beforeLoad: async ({ location }) => { const session = await getSession(); if (!session) { throw redirect({ to: "/login", search: { redirect: location.href }, }); } return { user: session.user }; }, component: () => <Outlet />, })再把需要保护的页面(dashboard.tsx、settings.tsx等)嵌套进_protected目录即可统一受控:
src/routes/ ├── _protected.tsx # 无路径布局,统一鉴权 ├── _protected/ │ ├── dashboard.tsx │ └── settings.tsx └── login.tsx保护 Server Function
借助ensureSession,在 Server Function 内直接抛错即可阻断未授权调用:
import { createServerFn } from "@tanstack/react-start"; import { ensureSession } from "./auth.functions"; export const createPost = createServerFn({ method: "POST" }) .inputValidator((data: { title: string }) => data) .handler(async ({ data }) => { const session = await ensureSession(); const post = await db.posts.create({ title: data.title, authorId: session.user.id, }); return post; });经验教训与自查清单
复盘文档给出四条核心教训,也即接入 TanStack Start 时应当遵守的规则:
- 绝不直接导入
@tanstack/start-server-core——它是带虚拟模块的内部包,直接导入会触发Could not resolve "#tanstack-router-entry"。 - 始终使用框架专属子路径——React 用
@tanstack/react-start/server,Solid.js 用@tanstack/solid-start/server。 - 不要使用独立的
-server包——@tanstack/react-start-server无法修复该问题。 - 两者的 API 是同一套——
setCookie、getCookie等能力在两个子路径下均可获得,代码迁移只改导入来源即可。
官方修复涉及的文件(均可在当前仓库查看):
- packages/better-auth/src/integrations/tanstack-start.ts —— React 版,导入改为
@tanstack/react-start/server - packages/better-auth/src/integrations/tanstack-start-solid.ts —— Solid.js 版,导入改为
@tanstack/solid-start/server - packages/better-auth/package.json —— peerDependencies 由
@tanstack/*-start-server更新为@tanstack/*-start(^1.0.0,可选)
完整的 TanStack Start 集成说明还可参考 官方集成文档(含npm create @tanstack/start脚手架一键集成 Better Auth 的用法)与 安装指南。若你仍在使用旧版本并在升级前遇到该报错,optimizeDeps.exclude是可靠的过渡手段。
【免费下载链接】better-authThe most comprehensive authentication framework项目地址: https://gitcode.com/GitHub_Trending/be/better-auth
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考