diff --git a/e2e/react-start/custom-manifest-base/.gitignore b/e2e/react-start/custom-manifest-base/.gitignore
new file mode 100644
index 00000000000..a79d5cf1299
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/.gitignore
@@ -0,0 +1,20 @@
+node_modules
+package-lock.json
+yarn.lock
+
+.DS_Store
+.cache
+.env
+.vercel
+.output
+
+/build/
+/api/
+/server/build
+/public/build
+# Sentry Config File
+.env.sentry-build-plugin
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/react-start/custom-manifest-base/.prettierignore b/e2e/react-start/custom-manifest-base/.prettierignore
new file mode 100644
index 00000000000..2be5eaa6ece
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/.prettierignore
@@ -0,0 +1,4 @@
+**/build
+**/public
+pnpm-lock.yaml
+routeTree.gen.ts
\ No newline at end of file
diff --git a/e2e/react-start/custom-manifest-base/express-server.ts b/e2e/react-start/custom-manifest-base/express-server.ts
new file mode 100644
index 00000000000..26ad0b2ce8a
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/express-server.ts
@@ -0,0 +1,51 @@
+import express from 'express'
+import { toNodeHandler } from 'srvx/node'
+import cors from 'cors'
+
+const DEVELOPMENT = process.env.NODE_ENV === 'development'
+const PORT = Number.parseInt(process.env.PORT || '3000')
+
+const app = express()
+
+const cdn = express()
+
+if (DEVELOPMENT) {
+ const viteDevServer = await import('vite').then((vite) =>
+ vite.createServer({
+ server: { middlewareMode: true },
+ }),
+ )
+ app.use(viteDevServer.middlewares)
+ app.use(async (req, res, next) => {
+ try {
+ const { default: serverEntry } =
+ await viteDevServer.ssrLoadModule('./src/server.ts')
+ const handler = toNodeHandler(serverEntry.fetch)
+ await handler(req, res)
+ } catch (error) {
+ if (typeof error === 'object' && error instanceof Error) {
+ viteDevServer.ssrFixStacktrace(error)
+ }
+ next(error)
+ }
+ })
+} else {
+ const { default: handler } = await import('./dist/server/server.js')
+ const nodeHandler = toNodeHandler(handler.fetch)
+ cdn.use(cors())
+ cdn.use('', express.static('dist/client'))
+ app.use(async (req, res, next) => {
+ try {
+ await nodeHandler(req, res)
+ } catch (error) {
+ next(error)
+ }
+ })
+}
+
+cdn.listen(3001, () => {
+ console.log(`CDN is running on http://localhost:3001`)
+})
+app.listen(PORT, () => {
+ console.log(`Server is running on http://localhost:${PORT}`)
+})
diff --git a/e2e/react-start/custom-manifest-base/package.json b/e2e/react-start/custom-manifest-base/package.json
new file mode 100644
index 00000000000..8cc1847c15b
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "tanstack-react-start-e2e-custom-manifest-base",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "cross-env NODE_ENV=development tsx express-server.ts",
+ "build": "vite build && tsc --noEmit",
+ "start": "tsx express-server.ts",
+ "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/react-router": "workspace:^",
+ "@tanstack/react-router-devtools": "workspace:^",
+ "@tanstack/react-start": "workspace:^",
+ "cors": "^2.8.5",
+ "express": "^4.21.2",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "redaxios": "^0.5.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.3",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.20",
+ "cross-env": "^10.0.0",
+ "postcss": "^8.5.1",
+ "srvx": "^0.8.6",
+ "tailwindcss": "^3.4.17",
+ "tsx": "^4.20.3",
+ "typescript": "^5.7.2",
+ "vite": "^7.1.7",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/react-start/custom-manifest-base/playwright.config.ts b/e2e/react-start/custom-manifest-base/playwright.config.ts
new file mode 100644
index 00000000000..5095425f473
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/playwright.config.ts
@@ -0,0 +1,42 @@
+import { defineConfig, devices } from '@playwright/test'
+import {
+ getDummyServerPort,
+ getTestServerPort,
+} from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(packageJson.name)
+const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}/custom/basepath`
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ globalSetup: './tests/setup/global.setup.ts',
+ globalTeardown: './tests/setup/global.teardown.ts',
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_NODE_ENV="test" VITE_EXTERNAL_PORT=${EXTERNAL_PORT} pnpm build && VITE_NODE_ENV="test" VITE_EXTERNAL_PORT=${EXTERNAL_PORT} VITE_SERVER_PORT=${PORT} PORT=${PORT} pnpm start`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/react-start/custom-manifest-base/postcss.config.mjs b/e2e/react-start/custom-manifest-base/postcss.config.mjs
new file mode 100644
index 00000000000..2e7af2b7f1a
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/postcss.config.mjs
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/e2e/react-start/custom-manifest-base/public/android-chrome-192x192.png b/e2e/react-start/custom-manifest-base/public/android-chrome-192x192.png
new file mode 100644
index 00000000000..09c8324f8c6
Binary files /dev/null and b/e2e/react-start/custom-manifest-base/public/android-chrome-192x192.png differ
diff --git a/e2e/react-start/custom-manifest-base/public/android-chrome-512x512.png b/e2e/react-start/custom-manifest-base/public/android-chrome-512x512.png
new file mode 100644
index 00000000000..11d626ea3d0
Binary files /dev/null and b/e2e/react-start/custom-manifest-base/public/android-chrome-512x512.png differ
diff --git a/e2e/react-start/custom-manifest-base/public/apple-touch-icon.png b/e2e/react-start/custom-manifest-base/public/apple-touch-icon.png
new file mode 100644
index 00000000000..5a9423cc02c
Binary files /dev/null and b/e2e/react-start/custom-manifest-base/public/apple-touch-icon.png differ
diff --git a/e2e/react-start/custom-manifest-base/public/favicon-16x16.png b/e2e/react-start/custom-manifest-base/public/favicon-16x16.png
new file mode 100644
index 00000000000..e3389b00443
Binary files /dev/null and b/e2e/react-start/custom-manifest-base/public/favicon-16x16.png differ
diff --git a/e2e/react-start/custom-manifest-base/public/favicon-32x32.png b/e2e/react-start/custom-manifest-base/public/favicon-32x32.png
new file mode 100644
index 00000000000..900c77d444c
Binary files /dev/null and b/e2e/react-start/custom-manifest-base/public/favicon-32x32.png differ
diff --git a/e2e/react-start/custom-manifest-base/public/favicon.ico b/e2e/react-start/custom-manifest-base/public/favicon.ico
new file mode 100644
index 00000000000..1a1751676f7
Binary files /dev/null and b/e2e/react-start/custom-manifest-base/public/favicon.ico differ
diff --git a/e2e/react-start/custom-manifest-base/public/favicon.png b/e2e/react-start/custom-manifest-base/public/favicon.png
new file mode 100644
index 00000000000..1e77bc06091
Binary files /dev/null and b/e2e/react-start/custom-manifest-base/public/favicon.png differ
diff --git a/e2e/react-start/custom-manifest-base/public/script.js b/e2e/react-start/custom-manifest-base/public/script.js
new file mode 100644
index 00000000000..897477e7d0a
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/public/script.js
@@ -0,0 +1,2 @@
+console.log('SCRIPT_1 loaded')
+window.SCRIPT_1 = true
diff --git a/e2e/react-start/custom-manifest-base/public/script2.js b/e2e/react-start/custom-manifest-base/public/script2.js
new file mode 100644
index 00000000000..819af30daf9
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/public/script2.js
@@ -0,0 +1,2 @@
+console.log('SCRIPT_2 loaded')
+window.SCRIPT_2 = true
diff --git a/e2e/react-start/custom-manifest-base/public/site.webmanifest b/e2e/react-start/custom-manifest-base/public/site.webmanifest
new file mode 100644
index 00000000000..fa99de77db6
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/public/site.webmanifest
@@ -0,0 +1,19 @@
+{
+ "name": "",
+ "short_name": "",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/e2e/react-start/custom-manifest-base/src/components/CustomMessage.tsx b/e2e/react-start/custom-manifest-base/src/components/CustomMessage.tsx
new file mode 100644
index 00000000000..d00e4eac60b
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/components/CustomMessage.tsx
@@ -0,0 +1,10 @@
+import * as React from 'react'
+
+export function CustomMessage({ message }: { message: string }) {
+ return (
+
+
This is a custom message:
+
{message}
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/components/DefaultCatchBoundary.tsx b/e2e/react-start/custom-manifest-base/src/components/DefaultCatchBoundary.tsx
new file mode 100644
index 00000000000..15f316681cc
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/components/DefaultCatchBoundary.tsx
@@ -0,0 +1,53 @@
+import {
+ ErrorComponent,
+ Link,
+ rootRouteId,
+ useMatch,
+ useRouter,
+} from '@tanstack/react-router'
+import type { ErrorComponentProps } from '@tanstack/react-router'
+
+export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
+ const router = useRouter()
+ const isRoot = useMatch({
+ strict: false,
+ select: (state) => state.id === rootRouteId,
+ })
+
+ console.error(error)
+
+ return (
+
+
+
+ {
+ router.invalidate()
+ }}
+ className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold`}
+ >
+ Try Again
+
+ {isRoot ? (
+
+ Home
+
+ ) : (
+ {
+ e.preventDefault()
+ window.history.back()
+ }}
+ >
+ Go Back
+
+ )}
+
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/components/NotFound.tsx b/e2e/react-start/custom-manifest-base/src/components/NotFound.tsx
new file mode 100644
index 00000000000..af4e0e74946
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/components/NotFound.tsx
@@ -0,0 +1,25 @@
+import { Link } from '@tanstack/react-router'
+
+export function NotFound({ children }: { children?: any }) {
+ return (
+
+
+ {children ||
The page you are looking for does not exist.
}
+
+
+ window.history.back()}
+ className="bg-emerald-500 text-white px-2 py-1 rounded uppercase font-black text-sm"
+ >
+ Go back
+
+
+ Start Over
+
+
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routeTree.gen.ts b/e2e/react-start/custom-manifest-base/src/routeTree.gen.ts
new file mode 100644
index 00000000000..0e57e8bf39e
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routeTree.gen.ts
@@ -0,0 +1,367 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as UsersRouteImport } from './routes/users'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as LogoutRouteImport } from './routes/logout'
+import { Route as DeferredRouteImport } from './routes/deferred'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as RedirectIndexRouteImport } from './routes/redirect/index'
+import { Route as PostsIndexRouteImport } from './routes/posts.index'
+import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
+import { Route as RedirectThrowItRouteImport } from './routes/redirect/throw-it'
+import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
+import { Route as ApiUsersRouteImport } from './routes/api.users'
+import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
+import { Route as ApiUsersIdRouteImport } from './routes/api/users.$id'
+
+const UsersRoute = UsersRouteImport.update({
+ id: '/users',
+ path: '/users',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const LogoutRoute = LogoutRouteImport.update({
+ id: '/logout',
+ path: '/logout',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const DeferredRoute = DeferredRouteImport.update({
+ id: '/deferred',
+ path: '/deferred',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const UsersIndexRoute = UsersIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => UsersRoute,
+} as any)
+const RedirectIndexRoute = RedirectIndexRouteImport.update({
+ id: '/redirect/',
+ path: '/redirect/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PostsIndexRoute = PostsIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => PostsRoute,
+} as any)
+const UsersUserIdRoute = UsersUserIdRouteImport.update({
+ id: '/$userId',
+ path: '/$userId',
+ getParentRoute: () => UsersRoute,
+} as any)
+const RedirectThrowItRoute = RedirectThrowItRouteImport.update({
+ id: '/redirect/throw-it',
+ path: '/redirect/throw-it',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PostsPostIdRoute = PostsPostIdRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => PostsRoute,
+} as any)
+const ApiUsersRoute = ApiUsersRouteImport.update({
+ id: '/api/users',
+ path: '/api/users',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
+ id: '/posts_/$postId/deep',
+ path: '/posts/$postId/deep',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ApiUsersIdRoute = ApiUsersIdRouteImport.update({
+ id: '/$id',
+ path: '/$id',
+ getParentRoute: () => ApiUsersRoute,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/deferred': typeof DeferredRoute
+ '/logout': typeof LogoutRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/users': typeof UsersRouteWithChildren
+ '/api/users': typeof ApiUsersRouteWithChildren
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/redirect/throw-it': typeof RedirectThrowItRoute
+ '/users/$userId': typeof UsersUserIdRoute
+ '/posts/': typeof PostsIndexRoute
+ '/redirect': typeof RedirectIndexRoute
+ '/users/': typeof UsersIndexRoute
+ '/api/users/$id': typeof ApiUsersIdRoute
+ '/posts/$postId/deep': typeof PostsPostIdDeepRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/deferred': typeof DeferredRoute
+ '/logout': typeof LogoutRoute
+ '/api/users': typeof ApiUsersRouteWithChildren
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/redirect/throw-it': typeof RedirectThrowItRoute
+ '/users/$userId': typeof UsersUserIdRoute
+ '/posts': typeof PostsIndexRoute
+ '/redirect': typeof RedirectIndexRoute
+ '/users': typeof UsersIndexRoute
+ '/api/users/$id': typeof ApiUsersIdRoute
+ '/posts/$postId/deep': typeof PostsPostIdDeepRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/deferred': typeof DeferredRoute
+ '/logout': typeof LogoutRoute
+ '/posts': typeof PostsRouteWithChildren
+ '/users': typeof UsersRouteWithChildren
+ '/api/users': typeof ApiUsersRouteWithChildren
+ '/posts/$postId': typeof PostsPostIdRoute
+ '/redirect/throw-it': typeof RedirectThrowItRoute
+ '/users/$userId': typeof UsersUserIdRoute
+ '/posts/': typeof PostsIndexRoute
+ '/redirect/': typeof RedirectIndexRoute
+ '/users/': typeof UsersIndexRoute
+ '/api/users/$id': typeof ApiUsersIdRoute
+ '/posts_/$postId/deep': typeof PostsPostIdDeepRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/deferred'
+ | '/logout'
+ | '/posts'
+ | '/users'
+ | '/api/users'
+ | '/posts/$postId'
+ | '/redirect/throw-it'
+ | '/users/$userId'
+ | '/posts/'
+ | '/redirect'
+ | '/users/'
+ | '/api/users/$id'
+ | '/posts/$postId/deep'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/deferred'
+ | '/logout'
+ | '/api/users'
+ | '/posts/$postId'
+ | '/redirect/throw-it'
+ | '/users/$userId'
+ | '/posts'
+ | '/redirect'
+ | '/users'
+ | '/api/users/$id'
+ | '/posts/$postId/deep'
+ id:
+ | '__root__'
+ | '/'
+ | '/deferred'
+ | '/logout'
+ | '/posts'
+ | '/users'
+ | '/api/users'
+ | '/posts/$postId'
+ | '/redirect/throw-it'
+ | '/users/$userId'
+ | '/posts/'
+ | '/redirect/'
+ | '/users/'
+ | '/api/users/$id'
+ | '/posts_/$postId/deep'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ DeferredRoute: typeof DeferredRoute
+ LogoutRoute: typeof LogoutRoute
+ PostsRoute: typeof PostsRouteWithChildren
+ UsersRoute: typeof UsersRouteWithChildren
+ ApiUsersRoute: typeof ApiUsersRouteWithChildren
+ RedirectThrowItRoute: typeof RedirectThrowItRoute
+ RedirectIndexRoute: typeof RedirectIndexRoute
+ PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/users': {
+ id: '/users'
+ path: '/users'
+ fullPath: '/users'
+ preLoaderRoute: typeof UsersRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/logout': {
+ id: '/logout'
+ path: '/logout'
+ fullPath: '/logout'
+ preLoaderRoute: typeof LogoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/deferred': {
+ id: '/deferred'
+ path: '/deferred'
+ fullPath: '/deferred'
+ preLoaderRoute: typeof DeferredRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/users/': {
+ id: '/users/'
+ path: '/'
+ fullPath: '/users/'
+ preLoaderRoute: typeof UsersIndexRouteImport
+ parentRoute: typeof UsersRoute
+ }
+ '/redirect/': {
+ id: '/redirect/'
+ path: '/redirect'
+ fullPath: '/redirect'
+ preLoaderRoute: typeof RedirectIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts/': {
+ id: '/posts/'
+ path: '/'
+ fullPath: '/posts/'
+ preLoaderRoute: typeof PostsIndexRouteImport
+ parentRoute: typeof PostsRoute
+ }
+ '/users/$userId': {
+ id: '/users/$userId'
+ path: '/$userId'
+ fullPath: '/users/$userId'
+ preLoaderRoute: typeof UsersUserIdRouteImport
+ parentRoute: typeof UsersRoute
+ }
+ '/redirect/throw-it': {
+ id: '/redirect/throw-it'
+ path: '/redirect/throw-it'
+ fullPath: '/redirect/throw-it'
+ preLoaderRoute: typeof RedirectThrowItRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts/$postId': {
+ id: '/posts/$postId'
+ path: '/$postId'
+ fullPath: '/posts/$postId'
+ preLoaderRoute: typeof PostsPostIdRouteImport
+ parentRoute: typeof PostsRoute
+ }
+ '/api/users': {
+ id: '/api/users'
+ path: '/api/users'
+ fullPath: '/api/users'
+ preLoaderRoute: typeof ApiUsersRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts_/$postId/deep': {
+ id: '/posts_/$postId/deep'
+ path: '/posts/$postId/deep'
+ fullPath: '/posts/$postId/deep'
+ preLoaderRoute: typeof PostsPostIdDeepRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/api/users/$id': {
+ id: '/api/users/$id'
+ path: '/$id'
+ fullPath: '/api/users/$id'
+ preLoaderRoute: typeof ApiUsersIdRouteImport
+ parentRoute: typeof ApiUsersRoute
+ }
+ }
+}
+
+interface PostsRouteChildren {
+ PostsPostIdRoute: typeof PostsPostIdRoute
+ PostsIndexRoute: typeof PostsIndexRoute
+}
+
+const PostsRouteChildren: PostsRouteChildren = {
+ PostsPostIdRoute: PostsPostIdRoute,
+ PostsIndexRoute: PostsIndexRoute,
+}
+
+const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)
+
+interface UsersRouteChildren {
+ UsersUserIdRoute: typeof UsersUserIdRoute
+ UsersIndexRoute: typeof UsersIndexRoute
+}
+
+const UsersRouteChildren: UsersRouteChildren = {
+ UsersUserIdRoute: UsersUserIdRoute,
+ UsersIndexRoute: UsersIndexRoute,
+}
+
+const UsersRouteWithChildren = UsersRoute._addFileChildren(UsersRouteChildren)
+
+interface ApiUsersRouteChildren {
+ ApiUsersIdRoute: typeof ApiUsersIdRoute
+}
+
+const ApiUsersRouteChildren: ApiUsersRouteChildren = {
+ ApiUsersIdRoute: ApiUsersIdRoute,
+}
+
+const ApiUsersRouteWithChildren = ApiUsersRoute._addFileChildren(
+ ApiUsersRouteChildren,
+)
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ DeferredRoute: DeferredRoute,
+ LogoutRoute: LogoutRoute,
+ PostsRoute: PostsRouteWithChildren,
+ UsersRoute: UsersRouteWithChildren,
+ ApiUsersRoute: ApiUsersRouteWithChildren,
+ RedirectThrowItRoute: RedirectThrowItRoute,
+ RedirectIndexRoute: RedirectIndexRoute,
+ PostsPostIdDeepRoute: PostsPostIdDeepRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/e2e/react-start/custom-manifest-base/src/router.tsx b/e2e/react-start/custom-manifest-base/src/router.tsx
new file mode 100644
index 00000000000..1a1d8822d20
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
+import { NotFound } from './components/NotFound'
+
+export function getRouter() {
+ const router = createRouter({
+ routeTree,
+ defaultPreload: 'intent',
+ defaultErrorComponent: DefaultCatchBoundary,
+ defaultNotFoundComponent: () => ,
+ scrollRestoration: true,
+ })
+
+ return router
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routes/__root.tsx b/e2e/react-start/custom-manifest-base/src/routes/__root.tsx
new file mode 100644
index 00000000000..0e4d4311afc
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/__root.tsx
@@ -0,0 +1,141 @@
+///
+import * as React from 'react'
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+
+import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
+import { NotFound } from '~/components/NotFound'
+import '~/styles/app.css'
+import { seo } from '~/utils/seo'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [
+ {
+ charSet: 'utf-8',
+ },
+ {
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1',
+ },
+ ...seo({
+ title:
+ 'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework',
+ description: `TanStack Start is a type-safe, client-first, full-stack React framework. `,
+ }),
+ ],
+ links: [
+ {
+ rel: 'apple-touch-icon',
+ sizes: '180x180',
+ href: '/apple-touch-icon.png',
+ },
+ {
+ rel: 'icon',
+ type: 'image/png',
+ sizes: '32x32',
+ href: '/favicon-32x32.png',
+ },
+ {
+ rel: 'icon',
+ type: 'image/png',
+ sizes: '16x16',
+ href: '/favicon-16x16.png',
+ },
+ { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' },
+ { rel: 'icon', href: '/favicon.ico' },
+ ],
+ }),
+ errorComponent: (props) => {
+ return (
+
+
+
+ )
+ },
+ notFoundComponent: () => ,
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+ )
+}
+
+const RouterDevtools =
+ process.env.NODE_ENV === 'production'
+ ? () => null // Render nothing in production
+ : React.lazy(() =>
+ // Lazy load in development
+ import('@tanstack/react-router-devtools').then((res) => ({
+ default: res.TanStackRouterDevtools,
+ })),
+ )
+
+function RootDocument({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+ Home
+ {' '}
+
+ Posts
+ {' '}
+
+ Users
+ {' '}
+
+ Deferred
+ {' '}
+
+ This Route Does Not Exist
+
+
+
+ {children}
+
+
+
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routes/api.users.ts b/e2e/react-start/custom-manifest-base/src/routes/api.users.ts
new file mode 100644
index 00000000000..a03076490b8
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/api.users.ts
@@ -0,0 +1,28 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { json } from '@tanstack/react-start'
+import axios from 'redaxios'
+
+import type { User } from '~/utils/users'
+
+let queryURL = 'https://jsonplaceholder.typicode.com'
+
+if (import.meta.env.VITE_NODE_ENV === 'test') {
+ queryURL = `http://localhost:${import.meta.env.VITE_EXTERNAL_PORT}`
+}
+
+export const Route = createFileRoute('/api/users')({
+ server: {
+ handlers: {
+ GET: async ({ request }) => {
+ console.info('Fetching users... @', request.url)
+ const res = await axios.get>(`${queryURL}/users`)
+
+ const list = res.data.slice(0, 10)
+
+ return json(
+ list.map((u) => ({ id: u.id, name: u.name, email: u.email })),
+ )
+ },
+ },
+ },
+})
diff --git a/e2e/react-start/custom-manifest-base/src/routes/api/users.$id.ts b/e2e/react-start/custom-manifest-base/src/routes/api/users.$id.ts
new file mode 100644
index 00000000000..5a2b9fe6216
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/api/users.$id.ts
@@ -0,0 +1,32 @@
+import { json } from '@tanstack/react-start'
+import axios from 'redaxios'
+import type { User } from '~/utils/users'
+import { createFileRoute } from '@tanstack/react-router'
+
+let queryURL = 'https://jsonplaceholder.typicode.com'
+
+if (import.meta.env.VITE_NODE_ENV === 'test') {
+ queryURL = `http://localhost:${import.meta.env.VITE_EXTERNAL_PORT}`
+}
+
+export const Route = createFileRoute('/api/users/$id')({
+ server: {
+ handlers: {
+ GET: async ({ request, params }) => {
+ console.info(`Fetching users by id=${params.id}... @`, request.url)
+ try {
+ const res = await axios.get(`${queryURL}/users/` + params.id)
+
+ return json({
+ id: res.data.id,
+ name: res.data.name,
+ email: res.data.email,
+ })
+ } catch (e) {
+ console.error(e)
+ return json({ error: 'User not found' }, { status: 404 })
+ }
+ },
+ },
+ },
+})
diff --git a/e2e/react-start/custom-manifest-base/src/routes/deferred.tsx b/e2e/react-start/custom-manifest-base/src/routes/deferred.tsx
new file mode 100644
index 00000000000..9c6e3064b88
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/deferred.tsx
@@ -0,0 +1,62 @@
+import { Await, createFileRoute } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+import { Suspense, useState } from 'react'
+
+const personServerFn = createServerFn({ method: 'GET' })
+ .inputValidator((data: { name: string }) => data)
+ .handler(({ data }) => {
+ return { name: data.name, randomNumber: Math.floor(Math.random() * 100) }
+ })
+
+const slowServerFn = createServerFn({ method: 'GET' })
+ .inputValidator((data: { name: string }) => data)
+ .handler(async ({ data }) => {
+ await new Promise((r) => setTimeout(r, 1000))
+ return { name: data.name, randomNumber: Math.floor(Math.random() * 100) }
+ })
+
+export const Route = createFileRoute('/deferred')({
+ loader: async () => {
+ return {
+ deferredStuff: new Promise((r) =>
+ setTimeout(() => r('Hello deferred!'), 2000),
+ ),
+ deferredPerson: slowServerFn({ data: { name: 'Tanner Linsley' } }),
+ person: await personServerFn({ data: { name: 'John Doe' } }),
+ }
+ },
+ component: Deferred,
+})
+
+function Deferred() {
+ const [count, setCount] = useState(0)
+ const { deferredStuff, deferredPerson, person } = Route.useLoaderData()
+
+ return (
+
+
+ {person.name} - {person.randomNumber}
+
+
Loading person... }>
+ (
+
+ {data.name} - {data.randomNumber}
+
+ )}
+ />
+
+ Loading stuff...}>
+ {data} }
+ />
+
+ Count: {count}
+
+ setCount(count + 1)}>Increment
+
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routes/index.tsx b/e2e/react-start/custom-manifest-base/src/routes/index.tsx
new file mode 100644
index 00000000000..37169a78b4a
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/index.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { CustomMessage } from '~/components/CustomMessage'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Welcome Home!!!
+
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routes/logout.tsx b/e2e/react-start/custom-manifest-base/src/routes/logout.tsx
new file mode 100644
index 00000000000..800952f24bf
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/logout.tsx
@@ -0,0 +1,32 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+
+const logoutFn = createServerFn({
+ method: 'POST',
+}).handler(async () => {
+ // do logout stuff here
+ throw redirect({
+ to: '/',
+ })
+})
+
+export const Route = createFileRoute('/logout')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Logout Page
+
+ This form tests that server function URLs correctly include the app's
+ basepath. The form action should be '/custom/basepath/_serverFn/...' not
+ just '/_serverFn/...'
+
+
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routes/posts.$postId.tsx b/e2e/react-start/custom-manifest-base/src/routes/posts.$postId.tsx
new file mode 100644
index 00000000000..5ea6fb33859
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/posts.$postId.tsx
@@ -0,0 +1,39 @@
+import { ErrorComponent, Link, createFileRoute } from '@tanstack/react-router'
+import type { ErrorComponentProps } from '@tanstack/react-router'
+
+import { fetchPost } from '~/utils/posts'
+import { NotFound } from '~/components/NotFound'
+
+export const Route = createFileRoute('/posts/$postId')({
+ loader: async ({ params: { postId } }) => fetchPost({ data: postId }),
+ errorComponent: PostErrorComponent,
+ component: PostComponent,
+ notFoundComponent: () => {
+ return Post not found
+ },
+})
+
+export function PostErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+function PostComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
{post.title}
+
{post.body}
+
+ Deep View
+
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routes/posts.index.tsx b/e2e/react-start/custom-manifest-base/src/routes/posts.index.tsx
new file mode 100644
index 00000000000..162f2cba412
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/posts.index.tsx
@@ -0,0 +1,8 @@
+import { createFileRoute } from '@tanstack/react-router'
+export const Route = createFileRoute('/posts/')({
+ component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+ return Select a post.
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routes/posts.tsx b/e2e/react-start/custom-manifest-base/src/routes/posts.tsx
new file mode 100644
index 00000000000..0f69c183419
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/posts.tsx
@@ -0,0 +1,46 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+
+import { fetchPosts } from '~/utils/posts'
+
+export const Route = createFileRoute('/posts')({
+ head: () => ({
+ meta: [
+ {
+ title: 'Posts page',
+ },
+ ],
+ }),
+ loader: async () => fetchPosts(),
+ component: PostsComponent,
+})
+
+function PostsComponent() {
+ const posts = Route.useLoaderData()
+
+ return (
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routes/posts_.$postId.deep.tsx b/e2e/react-start/custom-manifest-base/src/routes/posts_.$postId.deep.tsx
new file mode 100644
index 00000000000..e8f733560bf
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/posts_.$postId.deep.tsx
@@ -0,0 +1,27 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+import { PostErrorComponent } from './posts.$postId'
+import { fetchPost } from '~/utils/posts'
+
+export const Route = createFileRoute('/posts_/$postId/deep')({
+ loader: async ({ params: { postId } }) => fetchPost({ data: postId }),
+ errorComponent: PostErrorComponent,
+ component: PostDeepComponent,
+})
+
+function PostDeepComponent() {
+ const post = Route.useLoaderData()
+
+ return (
+
+
+ ← All Posts
+
+
{post.title}
+
{post.body}
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routes/redirect/index.tsx b/e2e/react-start/custom-manifest-base/src/routes/redirect/index.tsx
new file mode 100644
index 00000000000..5e95ca541d5
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/redirect/index.tsx
@@ -0,0 +1,15 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routes/redirect/throw-it.tsx b/e2e/react-start/custom-manifest-base/src/routes/redirect/throw-it.tsx
new file mode 100644
index 00000000000..efb7c3cd33f
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/redirect/throw-it.tsx
@@ -0,0 +1,10 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/redirect/throw-it')({
+ beforeLoad: () => {
+ throw redirect({
+ to: '/posts/$postId',
+ params: { postId: '1' },
+ })
+ },
+})
diff --git a/e2e/react-start/custom-manifest-base/src/routes/users.$userId.tsx b/e2e/react-start/custom-manifest-base/src/routes/users.$userId.tsx
new file mode 100644
index 00000000000..e293be37317
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/users.$userId.tsx
@@ -0,0 +1,37 @@
+import { ErrorComponent, createFileRoute } from '@tanstack/react-router'
+import axios from 'redaxios'
+import type { ErrorComponentProps } from '@tanstack/react-router'
+
+import type { User } from '~/utils/users'
+import { NotFound } from '~/components/NotFound'
+
+export const Route = createFileRoute('/users/$userId')({
+ loader: async ({ params: { userId } }) => {
+ return await axios
+ .get('/api/users/' + userId)
+ .then((r) => r.data)
+ .catch(() => {
+ throw new Error('Failed to fetch user')
+ })
+ },
+ errorComponent: UserErrorComponent,
+ component: UserComponent,
+ notFoundComponent: () => {
+ return User not found
+ },
+})
+
+function UserErrorComponent({ error }: ErrorComponentProps) {
+ return
+}
+
+function UserComponent() {
+ const user = Route.useLoaderData()
+
+ return (
+
+
{user.name}
+
{user.email}
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routes/users.index.tsx b/e2e/react-start/custom-manifest-base/src/routes/users.index.tsx
new file mode 100644
index 00000000000..ea814e2cbf8
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/users.index.tsx
@@ -0,0 +1,8 @@
+import { createFileRoute } from '@tanstack/react-router'
+export const Route = createFileRoute('/users/')({
+ component: UsersIndexComponent,
+})
+
+function UsersIndexComponent() {
+ return Select a user.
+}
diff --git a/e2e/react-start/custom-manifest-base/src/routes/users.tsx b/e2e/react-start/custom-manifest-base/src/routes/users.tsx
new file mode 100644
index 00000000000..c9a3420d5e7
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/routes/users.tsx
@@ -0,0 +1,47 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+import axios from 'redaxios'
+import type { User } from '~/utils/users'
+
+export const Route = createFileRoute('/users')({
+ loader: async () => {
+ return await axios
+ .get>('/api/users')
+ .then((r) => r.data)
+ .catch(() => {
+ throw new Error('Failed to fetch users')
+ })
+ },
+ component: UsersComponent,
+})
+
+function UsersComponent() {
+ const users = Route.useLoaderData()
+
+ return (
+
+
+ {[
+ ...users,
+ { id: 'i-do-not-exist', name: 'Non-existent User', email: '' },
+ ].map((user) => {
+ return (
+
+
+ {user.name}
+
+
+ )
+ })}
+
+
+
+
+ )
+}
diff --git a/e2e/react-start/custom-manifest-base/src/server.ts b/e2e/react-start/custom-manifest-base/src/server.ts
new file mode 100644
index 00000000000..c8f7c61d553
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/server.ts
@@ -0,0 +1,17 @@
+// src/server.ts
+import {
+ createStartHandler,
+ defaultStreamHandler,
+ defineHandlerCallback,
+} from '@tanstack/react-start/server'
+
+const customHandler = defineHandlerCallback((ctx) => {
+ // add custom logic here
+ return defaultStreamHandler(ctx)
+})
+
+const fetch = createStartHandler(customHandler, {assetsUrl: 'http://localhost:3001'})
+
+export default {
+ fetch
+}
\ No newline at end of file
diff --git a/e2e/react-start/custom-manifest-base/src/styles/app.css b/e2e/react-start/custom-manifest-base/src/styles/app.css
new file mode 100644
index 00000000000..c53c8706654
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/styles/app.css
@@ -0,0 +1,22 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html {
+ color-scheme: light dark;
+ }
+
+ * {
+ @apply border-gray-200 dark:border-gray-800;
+ }
+
+ html,
+ body {
+ @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200;
+ }
+
+ .using-mouse * {
+ outline: none !important;
+ }
+}
diff --git a/e2e/react-start/custom-manifest-base/src/utils/basepath.ts b/e2e/react-start/custom-manifest-base/src/utils/basepath.ts
new file mode 100644
index 00000000000..6e719f196cf
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/utils/basepath.ts
@@ -0,0 +1 @@
+export const basepath = '/custom/basepath'
diff --git a/e2e/react-start/custom-manifest-base/src/utils/posts.tsx b/e2e/react-start/custom-manifest-base/src/utils/posts.tsx
new file mode 100644
index 00000000000..b2d9f3edecf
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/utils/posts.tsx
@@ -0,0 +1,42 @@
+import { notFound } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+import axios from 'redaxios'
+
+export type PostType = {
+ id: string
+ title: string
+ body: string
+}
+
+let queryURL = 'https://jsonplaceholder.typicode.com'
+
+if (import.meta.env.VITE_NODE_ENV === 'test') {
+ queryURL = `http://localhost:${import.meta.env.VITE_EXTERNAL_PORT}`
+}
+
+export const fetchPost = createServerFn({ method: 'GET' })
+ .inputValidator((postId: string) => postId)
+ .handler(async ({ data: postId }) => {
+ console.info(`Fetching post with id ${postId}...`)
+ const post = await axios
+ .get(`${queryURL}/posts/${postId}`)
+ .then((r) => r.data)
+ .catch((err) => {
+ console.error(err)
+ if (err.status === 404) {
+ throw notFound()
+ }
+ throw err
+ })
+
+ return post
+ })
+
+export const fetchPosts = createServerFn({ method: 'GET' }).handler(
+ async () => {
+ console.info('Fetching posts...')
+ return axios
+ .get>(`${queryURL}/posts`)
+ .then((r) => r.data.slice(0, 10))
+ },
+)
diff --git a/e2e/react-start/custom-manifest-base/src/utils/seo.ts b/e2e/react-start/custom-manifest-base/src/utils/seo.ts
new file mode 100644
index 00000000000..d18ad84b74e
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/utils/seo.ts
@@ -0,0 +1,33 @@
+export const seo = ({
+ title,
+ description,
+ keywords,
+ image,
+}: {
+ title: string
+ description?: string
+ image?: string
+ keywords?: string
+}) => {
+ const tags = [
+ { title },
+ { name: 'description', content: description },
+ { name: 'keywords', content: keywords },
+ { name: 'twitter:title', content: title },
+ { name: 'twitter:description', content: description },
+ { name: 'twitter:creator', content: '@tannerlinsley' },
+ { name: 'twitter:site', content: '@tannerlinsley' },
+ { name: 'og:type', content: 'website' },
+ { name: 'og:title', content: title },
+ { name: 'og:description', content: description },
+ ...(image
+ ? [
+ { name: 'twitter:image', content: image },
+ { name: 'twitter:card', content: 'summary_large_image' },
+ { name: 'og:image', content: image },
+ ]
+ : []),
+ ]
+
+ return tags
+}
diff --git a/e2e/react-start/custom-manifest-base/src/utils/users.tsx b/e2e/react-start/custom-manifest-base/src/utils/users.tsx
new file mode 100644
index 00000000000..46be4b15804
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/src/utils/users.tsx
@@ -0,0 +1,9 @@
+export type User = {
+ id: number
+ name: string
+ email: string
+}
+
+const PORT = process.env.VITE_SERVER_PORT || 3000
+
+export const DEPLOY_URL = `http://localhost:${PORT}`
diff --git a/e2e/react-start/custom-manifest-base/tailwind.config.mjs b/e2e/react-start/custom-manifest-base/tailwind.config.mjs
new file mode 100644
index 00000000000..e49f4eb776e
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}'],
+}
diff --git a/e2e/react-start/custom-manifest-base/tests/navigation.spec.ts b/e2e/react-start/custom-manifest-base/tests/navigation.spec.ts
new file mode 100644
index 00000000000..c96cc384c88
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/tests/navigation.spec.ts
@@ -0,0 +1,70 @@
+import { expect, test } from '@playwright/test'
+
+test('Navigating to post', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await page.getByRole('link', { name: 'Deep View' }).click()
+ await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating to user', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Users' }).click()
+ await page.getByRole('link', { name: 'Leanne Graham' }).click()
+ await expect(page.getByRole('heading')).toContainText('Leanne Graham')
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+ await page.getByRole('link', { name: 'Start Over' }).click()
+ await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
+
+test('Should change title on client side navigation', async ({ page }) => {
+ await page.goto('/')
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+
+ await expect(page).toHaveTitle('Posts page')
+})
+
+test('Server function URLs correctly include app basepath', async ({
+ page,
+}) => {
+ await page.goto('/logout')
+
+ const form = page.locator('form')
+ const actionUrl = await form.getAttribute('action')
+
+ expect(actionUrl).toMatch(/^\/custom\/basepath\/_serverFn\//)
+})
+
+test('client-side redirect', async ({ page, baseURL }) => {
+ await page.goto('/redirect')
+ await page.getByTestId('link-to-throw-it').click()
+ await page.waitForLoadState('networkidle')
+
+ expect(await page.getByTestId('post-view').isVisible()).toBe(true)
+ expect(page.url()).toBe(`${baseURL}/posts/1`)
+})
+
+test('server-side redirect', async ({ page, baseURL }) => {
+ await page.goto('/redirect/throw-it')
+ await page.waitForLoadState('networkidle')
+
+ expect(await page.getByTestId('post-view').isVisible()).toBe(true)
+ expect(page.url()).toBe(`${baseURL}/posts/1`)
+
+ // do not follow redirects since we want to test the Location header
+ await page.request
+ .get('/redirect/throw-it', { maxRedirects: 0 })
+ .then((res) => {
+ const headers = new Headers(res.headers())
+ expect(headers.get('location')).toBe('/custom/basepath/posts/1')
+ })
+})
diff --git a/e2e/react-start/custom-manifest-base/tests/setup/global.setup.ts b/e2e/react-start/custom-manifest-base/tests/setup/global.setup.ts
new file mode 100644
index 00000000000..3593d10ab90
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/tests/setup/global.setup.ts
@@ -0,0 +1,6 @@
+import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function setup() {
+ await e2eStartDummyServer(packageJson.name)
+}
diff --git a/e2e/react-start/custom-manifest-base/tests/setup/global.teardown.ts b/e2e/react-start/custom-manifest-base/tests/setup/global.teardown.ts
new file mode 100644
index 00000000000..62fd79911cc
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/tests/setup/global.teardown.ts
@@ -0,0 +1,6 @@
+import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function teardown() {
+ await e2eStopDummyServer(packageJson.name)
+}
diff --git a/e2e/react-start/custom-manifest-base/tsconfig.json b/e2e/react-start/custom-manifest-base/tsconfig.json
new file mode 100644
index 00000000000..d35a4b17f48
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "include": ["**/*.ts", "**/*.tsx", "public/script*.js"],
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "target": "ES2022",
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./src/*"]
+ },
+ "noEmit": true,
+ "types": ["vite/client"]
+ }
+}
diff --git a/e2e/react-start/custom-manifest-base/vite.config.ts b/e2e/react-start/custom-manifest-base/vite.config.ts
new file mode 100644
index 00000000000..56f014b761d
--- /dev/null
+++ b/e2e/react-start/custom-manifest-base/vite.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import viteReact from '@vitejs/plugin-react'
+
+export default defineConfig({
+ server: {
+ port: 3000,
+ },
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ vite: { installDevServerMiddleware: true },
+ }),
+ viteReact(),
+ ],
+})
diff --git a/package.json b/package.json
index e0a13af4420..117075aaaeb 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"type": "git",
"url": "https://github.com/TanStack/router.git"
},
- "packageManager": "pnpm@10.13.1",
+ "packageManager": "pnpm@10.18.3",
"type": "module",
"scripts": {
"cleanNodeModules": "pnpm -r exec rm -rf node_modules",
@@ -39,8 +39,8 @@
"@eslint-react/eslint-plugin": "^1.26.2",
"@playwright/test": "^1.52.0",
"@tanstack/config": "0.22.0",
- "@tanstack/react-query": "5.66.0",
"@tanstack/query-core": "5.66.0",
+ "@tanstack/react-query": "5.66.0",
"@types/node": "22.10.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts
index e2d29b7822c..dd38e620fb3 100644
--- a/packages/server-functions-plugin/src/index.ts
+++ b/packages/server-functions-plugin/src/index.ts
@@ -204,7 +204,8 @@ export function TanStackServerFnPlugin(
([id, fn]: any) =>
`'${id}': {
functionName: '${fn.functionName}',
- importer: () => import(${JSON.stringify(fn.extractedFilename)})
+ importer: () => import(${JSON.stringify(fn.extractedFilename)}),
+ extractedFilename: ${JSON.stringify(fn.extractedFilename)}
}`,
)
.join(',')}}
@@ -213,11 +214,13 @@ export function TanStackServerFnPlugin(
if (!serverFnInfo) {
throw new Error('Server function info not found for ' + id)
}
+ console.info(serverFnInfo["extractedFilename"])
const fnModule = await serverFnInfo.importer()
if (!fnModule) {
console.info('serverFnInfo', serverFnInfo)
throw new Error('Server function module not resolved for ' + id)
+ return
}
const action = fnModule[serverFnInfo.functionName]
diff --git a/packages/start-plugin-core/src/schema.ts b/packages/start-plugin-core/src/schema.ts
index 39d471447da..0d0a892a371 100644
--- a/packages/start-plugin-core/src/schema.ts
+++ b/packages/start-plugin-core/src/schema.ts
@@ -129,6 +129,7 @@ const pageSchema = pageBaseSchema.extend({
const tanstackStartOptionsSchema = z
.object({
srcDirectory: z.string().optional().default('src'),
+ assetsUrl: z.string().optional(),
start: z
.object({
entry: z.string().optional(),
diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts
index d0fd84e8f43..25e5310c545 100644
--- a/packages/start-server-core/src/createStartHandler.ts
+++ b/packages/start-server-core/src/createStartHandler.ts
@@ -53,6 +53,7 @@ function getStartResponseHeaders(opts: { router: AnyRouter }) {
export function createStartHandler(
cb: HandlerCallback,
+ opts: {assetsUrl?: string} = {}
): RequestHandler {
const ROUTER_BASEPATH = process.env.TSS_ROUTER_BASEPATH || '/'
let startRoutesManifest: Manifest | null = null
@@ -212,7 +213,10 @@ export function createStartHandler(
// if the startRoutesManifest is not loaded yet, load it once
if (startRoutesManifest === null) {
- startRoutesManifest = await getStartManifest()
+ startRoutesManifest = await getStartManifest({
+ ...opts,
+ routerBasePath: ROUTER_BASEPATH
+ })
}
const router = await getRouter()
attachRouterServerSsrUtils({
diff --git a/packages/start-server-core/src/router-manifest.ts b/packages/start-server-core/src/router-manifest.ts
index 9f36b2fa361..67e3a5d1e9e 100644
--- a/packages/start-server-core/src/router-manifest.ts
+++ b/packages/start-server-core/src/router-manifest.ts
@@ -8,7 +8,7 @@ import { loadVirtualModule } from './loadVirtualModule'
* special assets that are needed for the client. It does not include relationships
* between routes or any other data that is not needed for the client.
*/
-export async function getStartManifest() {
+export async function getStartManifest({assetsUrl: maybeAssetsUrl, routerBasePath}: { assetsUrl?: string, routerBasePath?: string } = {}) {
const { tsrStartManifest } = await loadVirtualModule(
VIRTUAL_MODULES.startManifest,
)
@@ -18,8 +18,9 @@ export async function getStartManifest() {
startManifest.routes[rootRouteId] || {})
rootRoute.assets = rootRoute.assets || []
-
- let script = `import('${startManifest.clientEntry}')`
+ let script =
+ maybeAssetsUrl ?
+ `import('${maybeAssetsUrl + "/" + startManifest.clientEntry.replace(routerBasePath ?? '', '')}')` : `import('${startManifest.clientEntry}')`
if (process.env.TSS_DEV_SERVER === 'true') {
const { injectedHeadScripts } = await loadVirtualModule(
VIRTUAL_MODULES.injectedHeadScripts,
@@ -43,11 +44,32 @@ export async function getStartManifest() {
routes: Object.fromEntries(
Object.entries(startManifest.routes).map(([k, v]) => {
const { preloads, assets } = v
+ if (!maybeAssetsUrl) {
+ return [
+ k,
+ {
+ preloads,
+ assets,
+ },
+ ]
+ }
return [
k,
{
- preloads,
- assets,
+ preloads: preloads?.map((url) => maybeAssetsUrl + '/' + url.replace(routerBasePath ?? '', '')) || [],
+ assets:
+ assets?.map((asset) => {
+ if (asset.tag === 'link' && asset.attrs?.href) {
+ return {
+ ...asset,
+ attrs: {
+ ...asset.attrs,
+ href: maybeAssetsUrl + '/' + asset.attrs.href,
+ },
+ }
+ }
+ return asset
+ }) || [],
},
]
}),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e42bf3c4a1b..302726ef3f1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1367,6 +1367,85 @@ importers:
specifier: ^5.1.4
version: 5.1.4(typescript@5.8.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ e2e/react-start/custom-manifest-base:
+ dependencies:
+ '@tanstack/react-router':
+ specifier: workspace:*
+ version: link:../../../packages/react-router
+ '@tanstack/react-router-devtools':
+ specifier: workspace:^
+ version: link:../../../packages/react-router-devtools
+ '@tanstack/react-start':
+ specifier: workspace:*
+ version: link:../../../packages/react-start
+ cors:
+ specifier: ^2.8.5
+ version: 2.8.5
+ express:
+ specifier: ^4.21.2
+ version: 4.21.2
+ react:
+ specifier: ^19.0.0
+ version: 19.0.0
+ react-dom:
+ specifier: ^19.0.0
+ version: 19.0.0(react@19.0.0)
+ redaxios:
+ specifier: ^0.5.1
+ version: 0.5.1
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.52.0
+ version: 1.52.0
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/cors':
+ specifier: ^2.8.19
+ version: 2.8.19
+ '@types/express':
+ specifier: ^5.0.3
+ version: 5.0.3
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ '@types/react':
+ specifier: ^19.0.8
+ version: 19.0.8
+ '@types/react-dom':
+ specifier: ^19.0.3
+ version: 19.0.3(@types/react@19.0.8)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ autoprefixer:
+ specifier: ^10.4.20
+ version: 10.4.20(postcss@8.5.6)
+ cross-env:
+ specifier: ^10.0.0
+ version: 10.0.0
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.6
+ srvx:
+ specifier: ^0.8.6
+ version: 0.8.7
+ tailwindcss:
+ specifier: ^3.4.17
+ version: 3.4.17
+ tsx:
+ specifier: ^4.20.3
+ version: 4.20.3
+ typescript:
+ specifier: ^5.7.2
+ version: 5.9.2
+ vite:
+ specifier: ^7.1.7
+ version: 7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ vite-tsconfig-paths:
+ specifier: ^5.1.4
+ version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
e2e/react-start/query-integration:
dependencies:
'@tanstack/react-query':
@@ -2377,7 +2456,7 @@ importers:
version: 1.0.3(@rsbuild/core@1.2.4)
'@rsbuild/plugin-solid':
specifier: ^1.0.4
- version: 1.0.4(@babel/core@7.27.4)(@rsbuild/core@1.2.4)(solid-js@1.9.5)
+ version: 1.0.4(@babel/core@7.28.4)(@rsbuild/core@1.2.4)(solid-js@1.9.5)
'@tanstack/router-e2e-utils':
specifier: workspace:^
version: link:../../e2e-utils
@@ -11555,6 +11634,9 @@ packages:
'@types/cookies@0.9.0':
resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==}
+ '@types/cors@2.8.19':
+ resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
+
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
@@ -12776,6 +12858,10 @@ packages:
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+ cors@2.8.5:
+ resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
+ engines: {node: '>= 0.10'}
+
cosmiconfig@7.1.0:
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
engines: {node: '>=10'}
@@ -20044,17 +20130,6 @@ snapshots:
'@rspack/plugin-react-refresh': 1.0.1(react-refresh@0.16.0)
react-refresh: 0.16.0
- '@rsbuild/plugin-solid@1.0.4(@babel/core@7.27.4)(@rsbuild/core@1.2.4)(solid-js@1.9.5)':
- dependencies:
- '@rsbuild/core': 1.2.4
- '@rsbuild/plugin-babel': 1.0.3(@rsbuild/core@1.2.4)
- babel-preset-solid: 1.9.3(@babel/core@7.27.4)
- solid-refresh: 0.6.3(solid-js@1.9.5)
- transitivePeerDependencies:
- - '@babel/core'
- - solid-js
- - supports-color
-
'@rsbuild/plugin-solid@1.0.4(@babel/core@7.28.4)(@rsbuild/core@1.2.4)(solid-js@1.9.5)':
dependencies:
'@rsbuild/core': 1.2.4
@@ -21004,6 +21079,10 @@ snapshots:
'@types/keygrip': 1.0.6
'@types/node': 22.10.2
+ '@types/cors@2.8.19':
+ dependencies:
+ '@types/node': 22.10.2
+
'@types/deep-eql@4.0.2': {}
'@types/diff@7.0.2': {}
@@ -22477,6 +22556,11 @@ snapshots:
core-util-is@1.0.3: {}
+ cors@2.8.5:
+ dependencies:
+ object-assign: 4.1.1
+ vary: 1.1.2
+
cosmiconfig@7.1.0:
dependencies:
'@types/parse-json': 4.0.2