From 35d43ff34207f5183c6e6c38b8816a9e1a8f0f44 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 20 May 2026 12:33:29 +0200 Subject: [PATCH 1/2] feat(vite): forward tanstack start routes through nitro middleware Consult `globalThis.TSS_ROUTES_MANIFEST` (published by `@tanstack/start-plugin-core`) during dev so URLs matching a Start route count as explicit and flow through nitro's middleware (where Start's devApp handles them) instead of the asset/page heuristic. --- src/build/vite/dev.ts | 53 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/src/build/vite/dev.ts b/src/build/vite/dev.ts index b2549c7e0d..9e868b70d2 100644 --- a/src/build/vite/dev.ts +++ b/src/build/vite/dev.ts @@ -252,14 +252,13 @@ export async function configureViteDevServer(ctx: NitroPluginContext, server: Vi // (`routes/[...].ts` -> `/**`, `routes/[...slug].ts` -> `/**:slug`) is as authoritative as the // SSR `/**` and must not swallow Vite asset serves either, so both forms count as catch-all; // prefixed splat routes (`/api/photos/**`) are deterministic user routes and stay explicit. - const match = nitro.routing.routes.match( - req.method || "", - new URL(withBase(req.url!, nitro.options.baseURL), "http://localhost").pathname - ); + const pathname = new URL(withBase(req.url!, nitro.options.baseURL), "http://localhost") + .pathname; + const match = nitro.routing.routes.match(req.method || "", pathname); const matchedHandlers = match ? (Array.isArray(match) ? match : [match]) : []; - const isExplicitRoute = matchedHandlers.some( - (h) => h?.route && h.route !== "/**" && !h.route.startsWith("/**:") - ); + const isExplicitRoute = + matchedHandlers.some((h) => h?.route && h.route !== "/**" && !h.route.startsWith("/**:")) || + matchesStartRoute(pathname); // An explicit user route is a deterministic match and always wins, regardless of how the // browser tags the request (#4108, #4241, #4252, #4270) — no heuristic may override it. @@ -307,3 +306,43 @@ export async function configureViteDevServer(ctx: NitroPluginContext, server: Vi server.middlewares.use(nitroDevMiddleware); }; } + +// ---- TanStack Start route manifest probe ---- +// Start's router plugin publishes a filesystem-routes manifest on globalThis for sibling Vite +// plugins to consume (see `@tanstack/start-plugin-core` `routes-manifest-plugin`). When present, +// any URL matching one of Start's routes is treated as explicit so it flows through Nitro's +// middleware (where Start's devApp gets first shot) instead of the asset/page heuristic. + +type StartRoutesManifest = Record; + +let cachedStartManifest: StartRoutesManifest | undefined; +let cachedStartMatchers: RegExp[] = []; + +function matchesStartRoute(pathname: string): boolean { + const manifest = (globalThis as { TSS_ROUTES_MANIFEST?: StartRoutesManifest }) + .TSS_ROUTES_MANIFEST; + if (!manifest) return false; + if (manifest !== cachedStartManifest) { + cachedStartManifest = manifest; + cachedStartMatchers = []; + for (const [routePath, entry] of Object.entries(manifest)) { + if (!entry || routePath === "__root__") continue; + cachedStartMatchers.push(startRouteToRegex(routePath)); + } + } + return cachedStartMatchers.some((re) => re.test(pathname)); +} + +function startRouteToRegex(routePath: string): RegExp { + let path = routePath.replace(/\/$/, ""); + let splat = false; + if (path.endsWith("/$")) { + splat = true; + path = path.slice(0, -2); + } + const parts = path.split("/").map((seg) => { + if (seg.startsWith("$")) return "[^/]+"; + return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + }); + return new RegExp(`^${parts.join("/")}${splat ? "(?:/.*)?" : ""}/?$`); +} From b8be85d495da670351bea24c64f5f0a92ccf4881 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 20 May 2026 12:33:32 +0200 Subject: [PATCH 2/2] chore(examples): bump tanstack/start deps in vite-ssr-tss-react --- docs/4.examples/vite-ssr-tss-react.md | 6 +++--- examples/vite-ssr-tss-react/package.json | 6 +++--- pnpm-lock.yaml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/4.examples/vite-ssr-tss-react.md b/docs/4.examples/vite-ssr-tss-react.md index 259587a116..1e5e76cb7f 100644 --- a/docs/4.examples/vite-ssr-tss-react.md +++ b/docs/4.examples/vite-ssr-tss-react.md @@ -20,9 +20,9 @@ icon: i-simple-icons-tanstack "start": "node .output/server/index.mjs" }, "dependencies": { - "@tanstack/react-router": "^1.168.8", - "@tanstack/react-router-devtools": "^1.166.11", - "@tanstack/react-start": "^1.167.13", + "@tanstack/react-router": "^1.170.4", + "@tanstack/react-router-devtools": "^1.167.0", + "@tanstack/react-start": "^1.168.6", "nitro": "latest", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/examples/vite-ssr-tss-react/package.json b/examples/vite-ssr-tss-react/package.json index 81078f130f..835042ba05 100644 --- a/examples/vite-ssr-tss-react/package.json +++ b/examples/vite-ssr-tss-react/package.json @@ -6,9 +6,9 @@ "start": "node .output/server/index.mjs" }, "dependencies": { - "@tanstack/react-router": "^1.168.8", - "@tanstack/react-router-devtools": "^1.166.11", - "@tanstack/react-start": "^1.167.13", + "@tanstack/react-router": "^1.170.4", + "@tanstack/react-router-devtools": "^1.167.0", + "@tanstack/react-start": "^1.168.6", "nitro": "latest", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e009670f82..f1a2eddaf1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -644,13 +644,13 @@ importers: examples/vite-ssr-tss-react: dependencies: '@tanstack/react-router': - specifier: ^1.168.8 + specifier: ^1.170.4 version: 1.170.4(react-dom@19.2.6)(react@19.2.6) '@tanstack/react-router-devtools': - specifier: ^1.166.11 + specifier: ^1.167.0 version: 1.167.0(@tanstack/react-router@1.170.4)(@tanstack/router-core@1.171.2)(csstype@3.2.3)(react-dom@19.2.6)(react@19.2.6) '@tanstack/react-start': - specifier: ^1.167.13 + specifier: ^1.168.6 version: 1.168.6(@vitejs/plugin-rsc@0.5.26)(crossws@0.4.5)(react-dom@19.2.6)(react@19.2.6)(vite-plugin-solid@2.11.12)(vite@8.0.13) nitro: specifier: workspace:*