diff --git a/e2e/react-start/basic-nitro-spa/.gitignore b/e2e/react-start/basic-nitro-spa/.gitignore
new file mode 100644
index 00000000000..114d10aa0e4
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/.gitignore
@@ -0,0 +1,12 @@
+node_modules
+.DS_Store
+.cache
+.env
+dist
+.output
+.nitro
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/react-start/basic-nitro-spa/package.json b/e2e/react-start/basic-nitro-spa/package.json
new file mode 100644
index 00000000000..6edf2933e0e
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "tanstack-react-start-e2e-basic-nitro-spa",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev:v2": "vite dev -c vite.config.v2.ts --port 3000",
+ "dev:v3": "vite dev -c vite.config.v3.ts --port 3000",
+ "dev:e2e:v2": "vite dev -c vite.config.v2.ts",
+ "dev:e2e:v3": "vite dev -c vite.config.v3.ts",
+ "build:v2": "vite build -c vite.config.v2.ts && tsc --noEmit",
+ "build:v3": "vite build -c vite.config.v3.ts && tsc --noEmit",
+ "preview:v2": "vite preview -c vite.config.v2.ts",
+ "preview:v3": "vite preview -c vite.config.v3.ts",
+ "test:e2e:shared": "rm -rf port*.txt; playwright test --project=chromium",
+ "test:e2e:v2": "rm -rf .output dist .nitro && NITRO_VARIANT=v2 pnpm run test:e2e:shared",
+ "test:e2e:v3": "rm -rf .output dist .nitro && NITRO_VARIANT=v3 pnpm run test:e2e:shared",
+ "test:e2e": "pnpm run test:e2e:v2 && pnpm run test:e2e:v3"
+ },
+ "dependencies": {
+ "@tanstack/react-router": "workspace:^",
+ "@tanstack/react-router-devtools": "workspace:^",
+ "@tanstack/react-start": "workspace:^",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tailwindcss/postcss": "^4.1.15",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@tanstack/nitro-v2-vite-plugin": "workspace:^",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "nitro": "npm:nitro-nightly@latest",
+ "postcss": "^8.5.1",
+ "tailwindcss": "^4.1.15",
+ "typescript": "^5.7.2",
+ "vite": "^7.1.7",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/react-start/basic-nitro-spa/playwright.config.ts b/e2e/react-start/basic-nitro-spa/playwright.config.ts
new file mode 100644
index 00000000000..b26f5493b47
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/playwright.config.ts
@@ -0,0 +1,44 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+const nitroVariant = process.env.NITRO_VARIANT
+if (nitroVariant !== 'v2' && nitroVariant !== 'v3') {
+ throw new Error('Set NITRO_VARIANT to "v2" or "v3" for Nitro e2e tests.')
+}
+const buildScript = nitroVariant === 'v2' ? 'build:v2' : 'build:v3'
+const buildCommand = `pnpm run ${buildScript}`
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ // Note: We run node directly instead of vite preview because Nitro's
+ // configurePreviewServer spawns on a random port. The prerendering during
+ // build uses vite.preview() correctly.
+ command: `${buildCommand} && PORT=${PORT} node .output/server/index.mjs`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/react-start/basic-nitro-spa/postcss.config.mjs b/e2e/react-start/basic-nitro-spa/postcss.config.mjs
new file mode 100644
index 00000000000..a7f73a2d1d7
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/e2e/react-start/basic-nitro-spa/public/android-chrome-192x192.png b/e2e/react-start/basic-nitro-spa/public/android-chrome-192x192.png
new file mode 100644
index 00000000000..09c8324f8c6
Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/android-chrome-192x192.png differ
diff --git a/e2e/react-start/basic-nitro-spa/public/android-chrome-512x512.png b/e2e/react-start/basic-nitro-spa/public/android-chrome-512x512.png
new file mode 100644
index 00000000000..11d626ea3d0
Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/android-chrome-512x512.png differ
diff --git a/e2e/react-start/basic-nitro-spa/public/apple-touch-icon.png b/e2e/react-start/basic-nitro-spa/public/apple-touch-icon.png
new file mode 100644
index 00000000000..5a9423cc02c
Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/apple-touch-icon.png differ
diff --git a/e2e/react-start/basic-nitro-spa/public/favicon-16x16.png b/e2e/react-start/basic-nitro-spa/public/favicon-16x16.png
new file mode 100644
index 00000000000..e3389b00443
Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/favicon-16x16.png differ
diff --git a/e2e/react-start/basic-nitro-spa/public/favicon-32x32.png b/e2e/react-start/basic-nitro-spa/public/favicon-32x32.png
new file mode 100644
index 00000000000..900c77d444c
Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/favicon-32x32.png differ
diff --git a/e2e/react-start/basic-nitro-spa/public/favicon.ico b/e2e/react-start/basic-nitro-spa/public/favicon.ico
new file mode 100644
index 00000000000..1a1751676f7
Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/favicon.ico differ
diff --git a/e2e/react-start/basic-nitro-spa/public/favicon.png b/e2e/react-start/basic-nitro-spa/public/favicon.png
new file mode 100644
index 00000000000..1e77bc06091
Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/favicon.png differ
diff --git a/e2e/react-start/basic-nitro-spa/public/site.webmanifest b/e2e/react-start/basic-nitro-spa/public/site.webmanifest
new file mode 100644
index 00000000000..fa99de77db6
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/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/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx b/e2e/react-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx
new file mode 100644
index 00000000000..ef2daa1ea1d
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/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 (
+
+
+
+
+ {isRoot ? (
+
+ Home
+
+ ) : (
+ {
+ e.preventDefault()
+ window.history.back()
+ }}
+ >
+ Go Back
+
+ )}
+
+
+ )
+}
diff --git a/e2e/react-start/basic-nitro-spa/src/components/NotFound.tsx b/e2e/react-start/basic-nitro-spa/src/components/NotFound.tsx
new file mode 100644
index 00000000000..4e84e3f8e00
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/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.
}
+
+
+
+
+ Start Over
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts b/e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts
new file mode 100644
index 00000000000..4219501f5ef
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts
@@ -0,0 +1,86 @@
+/* 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 StaticRouteImport } from './routes/static'
+import { Route as IndexRouteImport } from './routes/index'
+
+const StaticRoute = StaticRouteImport.update({
+ id: '/static',
+ path: '/static',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/static': typeof StaticRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/static': typeof StaticRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/static': typeof StaticRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/static'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/static'
+ id: '__root__' | '/' | '/static'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ StaticRoute: typeof StaticRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/static': {
+ id: '/static'
+ path: '/static'
+ fullPath: '/static'
+ preLoaderRoute: typeof StaticRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ StaticRoute: StaticRoute,
+}
+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/basic-nitro-spa/src/router.tsx b/e2e/react-start/basic-nitro-spa/src/router.tsx
new file mode 100644
index 00000000000..1a1d8822d20
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/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/basic-nitro-spa/src/routes/__root.tsx b/e2e/react-start/basic-nitro-spa/src/routes/__root.tsx
new file mode 100644
index 00000000000..5b62b589077
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/src/routes/__root.tsx
@@ -0,0 +1,73 @@
+///
+import {
+ HeadContent,
+ Link,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
+import * as React from 'react'
+import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
+import { NotFound } from '~/components/NotFound'
+import appCss from '~/styles/app.css?url'
+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 + Nitro E2E Test',
+ description: 'Testing nitro integration with TanStack Start',
+ }),
+ ],
+ links: [
+ { rel: 'stylesheet', href: appCss },
+ { rel: 'icon', href: '/favicon.ico' },
+ ],
+ }),
+ errorComponent: DefaultCatchBoundary,
+ notFoundComponent: () => ,
+ shellComponent: RootDocument,
+})
+
+function RootDocument({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+ Home
+
+
+ Static
+
+
+
+ {children}
+
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic-nitro-spa/src/routes/index.tsx b/e2e/react-start/basic-nitro-spa/src/routes/index.tsx
new file mode 100644
index 00000000000..311e2cf3739
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/src/routes/index.tsx
@@ -0,0 +1,26 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+
+export const Route = createFileRoute('/')({
+ loader: () => getData(),
+ component: Home,
+})
+
+const getData = createServerFn().handler(() => {
+ return {
+ message: 'Hello from Nitro server!',
+ timestamp: new Date().toISOString(),
+ }
+})
+
+function Home() {
+ const data = Route.useLoaderData()
+
+ return (
+
+
Welcome Home!
+
{data.message}
+
Loaded at: {data.timestamp}
+
+ )
+}
diff --git a/e2e/react-start/basic-nitro-spa/src/routes/static.tsx b/e2e/react-start/basic-nitro-spa/src/routes/static.tsx
new file mode 100644
index 00000000000..f018bf39649
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/src/routes/static.tsx
@@ -0,0 +1,26 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+
+export const Route = createFileRoute('/static')({
+ loader: () => getStaticData(),
+ component: StaticPage,
+})
+
+const getStaticData = createServerFn().handler(() => {
+ return {
+ content: 'This page was prerendered at build time',
+ buildTime: new Date().toISOString(),
+ }
+})
+
+function StaticPage() {
+ const data = Route.useLoaderData()
+
+ return (
+
+
Static Page
+
{data.content}
+
Build time: {data.buildTime}
+
+ )
+}
diff --git a/e2e/react-start/basic-nitro-spa/src/styles/app.css b/e2e/react-start/basic-nitro-spa/src/styles/app.css
new file mode 100644
index 00000000000..c36c737cd46
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/src/styles/app.css
@@ -0,0 +1,30 @@
+@import 'tailwindcss';
+
+@layer base {
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentcolor);
+ }
+}
+
+@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/basic-nitro-spa/src/utils/seo.ts b/e2e/react-start/basic-nitro-spa/src/utils/seo.ts
new file mode 100644
index 00000000000..d18ad84b74e
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/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/basic-nitro-spa/tests/app.spec.ts b/e2e/react-start/basic-nitro-spa/tests/app.spec.ts
new file mode 100644
index 00000000000..b0f96e373af
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/tests/app.spec.ts
@@ -0,0 +1,30 @@
+import { existsSync } from 'node:fs'
+import { join } from 'node:path'
+import { expect, test } from '@playwright/test'
+
+test('SPA shell is prerendered during build with nitro', async ({ page }) => {
+ const outputDir = join(process.cwd(), '.output', 'public')
+ expect(existsSync(join(outputDir, 'index.html'))).toBe(true)
+
+ await page.goto('/')
+ await expect(page.getByTestId('home-heading')).toBeVisible()
+})
+
+test('server functions work with nitro', async ({ page }) => {
+ await page.goto('/')
+ await expect(page.getByTestId('home-heading')).toHaveText('Welcome Home!')
+ await expect(page.getByTestId('message')).toHaveText(
+ 'Hello from Nitro server!',
+ )
+})
+
+test('client-side navigation works in SPA mode', async ({ page }) => {
+ await page.goto('/')
+ await expect(page.getByTestId('home-heading')).toBeVisible()
+
+ await page.click('a[href="/static"]')
+ await expect(page.getByTestId('static-heading')).toBeVisible()
+
+ await page.click('a[href="/"]')
+ await expect(page.getByTestId('home-heading')).toBeVisible()
+})
diff --git a/e2e/react-start/basic-nitro-spa/tsconfig.json b/e2e/react-start/basic-nitro-spa/tsconfig.json
new file mode 100644
index 00000000000..3a9fb7cd716
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "include": ["**/*.ts", "**/*.tsx"],
+ "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
+ }
+}
diff --git a/e2e/react-start/basic-nitro-spa/vite.config.v2.ts b/e2e/react-start/basic-nitro-spa/vite.config.v2.ts
new file mode 100644
index 00000000000..4b6e6dec6ae
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/vite.config.v2.ts
@@ -0,0 +1,21 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin'
+
+export default defineConfig({
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ spa: {
+ enabled: true,
+ prerender: {
+ outputPath: 'index.html',
+ },
+ },
+ }),
+ nitroV2Plugin(),
+ ],
+})
diff --git a/e2e/react-start/basic-nitro-spa/vite.config.v3.ts b/e2e/react-start/basic-nitro-spa/vite.config.v3.ts
new file mode 100644
index 00000000000..37f603e3ba8
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/vite.config.v3.ts
@@ -0,0 +1,21 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import { nitro } from 'nitro/vite'
+
+export default defineConfig({
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ spa: {
+ enabled: true,
+ prerender: {
+ outputPath: 'index.html',
+ },
+ },
+ }),
+ nitro(),
+ ],
+})
diff --git a/e2e/react-start/basic-nitro/.gitignore b/e2e/react-start/basic-nitro/.gitignore
new file mode 100644
index 00000000000..cce09e5f653
--- /dev/null
+++ b/e2e/react-start/basic-nitro/.gitignore
@@ -0,0 +1,11 @@
+node_modules
+.DS_Store
+.cache
+.env
+dist
+.output
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/react-start/basic-nitro/.prettierignore b/e2e/react-start/basic-nitro/.prettierignore
new file mode 100644
index 00000000000..a16e01379d7
--- /dev/null
+++ b/e2e/react-start/basic-nitro/.prettierignore
@@ -0,0 +1 @@
+routeTree.gen.ts
diff --git a/e2e/react-start/basic-nitro/package.json b/e2e/react-start/basic-nitro/package.json
new file mode 100644
index 00000000000..109d3e2d33f
--- /dev/null
+++ b/e2e/react-start/basic-nitro/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "tanstack-react-start-e2e-basic-nitro",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev:v2": "vite dev -c vite.config.v2.ts --port 3000",
+ "dev:v3": "vite dev -c vite.config.v3.ts --port 3000",
+ "dev:e2e:v2": "vite dev -c vite.config.v2.ts",
+ "dev:e2e:v3": "vite dev -c vite.config.v3.ts",
+ "build:v2": "vite build -c vite.config.v2.ts && tsc --noEmit",
+ "build:v3": "vite build -c vite.config.v3.ts && tsc --noEmit",
+ "preview:v2": "vite preview -c vite.config.v2.ts",
+ "preview:v3": "vite preview -c vite.config.v3.ts",
+ "test:e2e:shared": "rm -rf port*.txt; playwright test --project=chromium",
+ "test:e2e:v2": "rm -rf .output dist .nitro && NITRO_VARIANT=v2 pnpm run test:e2e:shared",
+ "test:e2e:v3": "rm -rf .output dist .nitro && NITRO_VARIANT=v3 pnpm run test:e2e:shared",
+ "test:e2e": "pnpm run test:e2e:v2 && pnpm run test:e2e:v3"
+ },
+ "dependencies": {
+ "@tanstack/react-router": "workspace:^",
+ "@tanstack/react-router-devtools": "workspace:^",
+ "@tanstack/react-start": "workspace:^",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tailwindcss/postcss": "^4.1.15",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@tanstack/nitro-v2-vite-plugin": "workspace:^",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "nitro": "npm:nitro-nightly@latest",
+ "postcss": "^8.5.1",
+ "tailwindcss": "^4.1.15",
+ "typescript": "^5.7.2",
+ "vite": "^7.1.7",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/react-start/basic-nitro/playwright.config.ts b/e2e/react-start/basic-nitro/playwright.config.ts
new file mode 100644
index 00000000000..b26f5493b47
--- /dev/null
+++ b/e2e/react-start/basic-nitro/playwright.config.ts
@@ -0,0 +1,44 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+const nitroVariant = process.env.NITRO_VARIANT
+if (nitroVariant !== 'v2' && nitroVariant !== 'v3') {
+ throw new Error('Set NITRO_VARIANT to "v2" or "v3" for Nitro e2e tests.')
+}
+const buildScript = nitroVariant === 'v2' ? 'build:v2' : 'build:v3'
+const buildCommand = `pnpm run ${buildScript}`
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ // Note: We run node directly instead of vite preview because Nitro's
+ // configurePreviewServer spawns on a random port. The prerendering during
+ // build uses vite.preview() correctly.
+ command: `${buildCommand} && PORT=${PORT} node .output/server/index.mjs`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/react-start/basic-nitro/postcss.config.mjs b/e2e/react-start/basic-nitro/postcss.config.mjs
new file mode 100644
index 00000000000..a7f73a2d1d7
--- /dev/null
+++ b/e2e/react-start/basic-nitro/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/e2e/react-start/basic-nitro/public/android-chrome-192x192.png b/e2e/react-start/basic-nitro/public/android-chrome-192x192.png
new file mode 100644
index 00000000000..09c8324f8c6
Binary files /dev/null and b/e2e/react-start/basic-nitro/public/android-chrome-192x192.png differ
diff --git a/e2e/react-start/basic-nitro/public/android-chrome-512x512.png b/e2e/react-start/basic-nitro/public/android-chrome-512x512.png
new file mode 100644
index 00000000000..11d626ea3d0
Binary files /dev/null and b/e2e/react-start/basic-nitro/public/android-chrome-512x512.png differ
diff --git a/e2e/react-start/basic-nitro/public/apple-touch-icon.png b/e2e/react-start/basic-nitro/public/apple-touch-icon.png
new file mode 100644
index 00000000000..5a9423cc02c
Binary files /dev/null and b/e2e/react-start/basic-nitro/public/apple-touch-icon.png differ
diff --git a/e2e/react-start/basic-nitro/public/favicon-16x16.png b/e2e/react-start/basic-nitro/public/favicon-16x16.png
new file mode 100644
index 00000000000..e3389b00443
Binary files /dev/null and b/e2e/react-start/basic-nitro/public/favicon-16x16.png differ
diff --git a/e2e/react-start/basic-nitro/public/favicon-32x32.png b/e2e/react-start/basic-nitro/public/favicon-32x32.png
new file mode 100644
index 00000000000..900c77d444c
Binary files /dev/null and b/e2e/react-start/basic-nitro/public/favicon-32x32.png differ
diff --git a/e2e/react-start/basic-nitro/public/favicon.ico b/e2e/react-start/basic-nitro/public/favicon.ico
new file mode 100644
index 00000000000..1a1751676f7
Binary files /dev/null and b/e2e/react-start/basic-nitro/public/favicon.ico differ
diff --git a/e2e/react-start/basic-nitro/public/favicon.png b/e2e/react-start/basic-nitro/public/favicon.png
new file mode 100644
index 00000000000..1e77bc06091
Binary files /dev/null and b/e2e/react-start/basic-nitro/public/favicon.png differ
diff --git a/e2e/react-start/basic-nitro/public/site.webmanifest b/e2e/react-start/basic-nitro/public/site.webmanifest
new file mode 100644
index 00000000000..fa99de77db6
--- /dev/null
+++ b/e2e/react-start/basic-nitro/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/basic-nitro/src/components/DefaultCatchBoundary.tsx b/e2e/react-start/basic-nitro/src/components/DefaultCatchBoundary.tsx
new file mode 100644
index 00000000000..ef2daa1ea1d
--- /dev/null
+++ b/e2e/react-start/basic-nitro/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 (
+
+
+
+
+ {isRoot ? (
+
+ Home
+
+ ) : (
+ {
+ e.preventDefault()
+ window.history.back()
+ }}
+ >
+ Go Back
+
+ )}
+
+
+ )
+}
diff --git a/e2e/react-start/basic-nitro/src/components/NotFound.tsx b/e2e/react-start/basic-nitro/src/components/NotFound.tsx
new file mode 100644
index 00000000000..4e84e3f8e00
--- /dev/null
+++ b/e2e/react-start/basic-nitro/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.
}
+
+
+
+
+ Start Over
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic-nitro/src/routeTree.gen.ts b/e2e/react-start/basic-nitro/src/routeTree.gen.ts
new file mode 100644
index 00000000000..4219501f5ef
--- /dev/null
+++ b/e2e/react-start/basic-nitro/src/routeTree.gen.ts
@@ -0,0 +1,86 @@
+/* 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 StaticRouteImport } from './routes/static'
+import { Route as IndexRouteImport } from './routes/index'
+
+const StaticRoute = StaticRouteImport.update({
+ id: '/static',
+ path: '/static',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/static': typeof StaticRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/static': typeof StaticRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/static': typeof StaticRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/static'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/static'
+ id: '__root__' | '/' | '/static'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ StaticRoute: typeof StaticRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/static': {
+ id: '/static'
+ path: '/static'
+ fullPath: '/static'
+ preLoaderRoute: typeof StaticRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ StaticRoute: StaticRoute,
+}
+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/basic-nitro/src/router.tsx b/e2e/react-start/basic-nitro/src/router.tsx
new file mode 100644
index 00000000000..1a1d8822d20
--- /dev/null
+++ b/e2e/react-start/basic-nitro/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/basic-nitro/src/routes/__root.tsx b/e2e/react-start/basic-nitro/src/routes/__root.tsx
new file mode 100644
index 00000000000..17577a3905e
--- /dev/null
+++ b/e2e/react-start/basic-nitro/src/routes/__root.tsx
@@ -0,0 +1,92 @@
+///
+import {
+ HeadContent,
+ Link,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
+import * as React from 'react'
+import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
+import { NotFound } from '~/components/NotFound'
+import appCss from '~/styles/app.css?url'
+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: 'stylesheet', href: appCss },
+ {
+ 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: DefaultCatchBoundary,
+ notFoundComponent: () => ,
+ shellComponent: RootDocument,
+})
+
+function RootDocument({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+ Home
+
+
+ Static
+
+
+
+ {children}
+
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic-nitro/src/routes/index.tsx b/e2e/react-start/basic-nitro/src/routes/index.tsx
new file mode 100644
index 00000000000..2b0878af588
--- /dev/null
+++ b/e2e/react-start/basic-nitro/src/routes/index.tsx
@@ -0,0 +1,26 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+
+export const Route = createFileRoute('/')({
+ loader: () => getData(),
+ component: Home,
+})
+
+const getData = createServerFn().handler(() => {
+ return {
+ message: `Running in Node.js ${process.version}`,
+ runtime: 'Nitro',
+ }
+})
+
+function Home() {
+ const data = Route.useLoaderData()
+
+ return (
+
+
Welcome Home!!!
+
{data.message}
+
{data.runtime}
+
+ )
+}
diff --git a/e2e/react-start/basic-nitro/src/routes/static.tsx b/e2e/react-start/basic-nitro/src/routes/static.tsx
new file mode 100644
index 00000000000..3333760d11a
--- /dev/null
+++ b/e2e/react-start/basic-nitro/src/routes/static.tsx
@@ -0,0 +1,28 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+
+export const Route = createFileRoute('/static')({
+ loader: () => getData(),
+ component: StaticPage,
+})
+
+const getData = createServerFn().handler(() => {
+ return {
+ generatedAt: new Date().toISOString(),
+ runtime: 'Nitro',
+ }
+})
+
+function StaticPage() {
+ const data = Route.useLoaderData()
+
+ return (
+
+
Static Page
+
+ This page was prerendered with {data.runtime}
+
+
Generated at: {data.generatedAt}
+
+ )
+}
diff --git a/e2e/react-start/basic-nitro/src/styles/app.css b/e2e/react-start/basic-nitro/src/styles/app.css
new file mode 100644
index 00000000000..c36c737cd46
--- /dev/null
+++ b/e2e/react-start/basic-nitro/src/styles/app.css
@@ -0,0 +1,30 @@
+@import 'tailwindcss';
+
+@layer base {
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentcolor);
+ }
+}
+
+@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/basic-nitro/src/utils/seo.ts b/e2e/react-start/basic-nitro/src/utils/seo.ts
new file mode 100644
index 00000000000..d18ad84b74e
--- /dev/null
+++ b/e2e/react-start/basic-nitro/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/basic-nitro/tests/app.spec.ts b/e2e/react-start/basic-nitro/tests/app.spec.ts
new file mode 100644
index 00000000000..31b1a7f6251
--- /dev/null
+++ b/e2e/react-start/basic-nitro/tests/app.spec.ts
@@ -0,0 +1,38 @@
+import { existsSync, readFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+test('returns correct runtime info', async ({ page }) => {
+ await page.goto('/')
+ await expect(page.getByTestId('message')).toContainText('Running in Node.js')
+ await expect(page.getByTestId('runtime')).toHaveText('Nitro')
+})
+
+test('prerender with Nitro', async ({ page }) => {
+ const distDir = join(process.cwd(), '.output', 'public')
+ const staticHtmlPath = join(distDir, 'static', 'index.html')
+ expect(existsSync(staticHtmlPath)).toBe(true)
+ const staticHtml = readFileSync(staticHtmlPath, 'utf8')
+ const normalizedHtml = staticHtml.replace(//g, '')
+ expect(normalizedHtml).toContain('This page was prerendered with Nitro')
+ expect(normalizedHtml).toContain('Generated at:')
+
+ await page.goto('/static')
+ await expect(page.getByTestId('static-heading')).toHaveText('Static Page')
+ await expect(page.getByTestId('static-content')).toHaveText(
+ 'This page was prerendered with Nitro',
+ )
+ await expect(page.getByTestId('generated-at')).toContainText('Generated at:')
+})
+
+test('client-side navigation works', async ({ page }) => {
+ await page.goto('/')
+ await expect(page.getByTestId('message')).toContainText('Running in Node.js')
+
+ await page.getByRole('link', { name: 'Static' }).click()
+ await expect(page.getByTestId('static-heading')).toHaveText('Static Page')
+
+ await page.getByRole('link', { name: 'Home' }).click()
+ await expect(page.getByTestId('message')).toContainText('Running in Node.js')
+})
diff --git a/e2e/react-start/basic-nitro/tests/setup/global.setup.ts b/e2e/react-start/basic-nitro/tests/setup/global.setup.ts
new file mode 100644
index 00000000000..f54c01cad2c
--- /dev/null
+++ b/e2e/react-start/basic-nitro/tests/setup/global.setup.ts
@@ -0,0 +1,3 @@
+export default async function setup() {
+ // No additional setup needed for Nitro
+}
diff --git a/e2e/react-start/basic-nitro/tests/setup/global.teardown.ts b/e2e/react-start/basic-nitro/tests/setup/global.teardown.ts
new file mode 100644
index 00000000000..ac5a84f5ea6
--- /dev/null
+++ b/e2e/react-start/basic-nitro/tests/setup/global.teardown.ts
@@ -0,0 +1,3 @@
+export default async function teardown() {
+ // No additional teardown needed for Nitro
+}
diff --git a/e2e/react-start/basic-nitro/tsconfig.json b/e2e/react-start/basic-nitro/tsconfig.json
new file mode 100644
index 00000000000..3a9fb7cd716
--- /dev/null
+++ b/e2e/react-start/basic-nitro/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "include": ["**/*.ts", "**/*.tsx"],
+ "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
+ }
+}
diff --git a/e2e/react-start/basic-nitro/vite.config.v2.ts b/e2e/react-start/basic-nitro/vite.config.v2.ts
new file mode 100644
index 00000000000..a10eb758e6e
--- /dev/null
+++ b/e2e/react-start/basic-nitro/vite.config.v2.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin'
+
+export default defineConfig({
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ prerender: {
+ enabled: true,
+ filter: (page) => page.path === '/static',
+ },
+ }),
+ nitroV2Plugin(),
+ ],
+})
diff --git a/e2e/react-start/basic-nitro/vite.config.v3.ts b/e2e/react-start/basic-nitro/vite.config.v3.ts
new file mode 100644
index 00000000000..eb7246636c4
--- /dev/null
+++ b/e2e/react-start/basic-nitro/vite.config.v3.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import { nitro } from 'nitro/vite'
+
+export default defineConfig({
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ prerender: {
+ enabled: true,
+ filter: (page) => page.path === '/static',
+ },
+ }),
+ nitro(),
+ ],
+})
diff --git a/e2e/solid-start/basic-nitro-spa/.gitignore b/e2e/solid-start/basic-nitro-spa/.gitignore
new file mode 100644
index 00000000000..114d10aa0e4
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/.gitignore
@@ -0,0 +1,12 @@
+node_modules
+.DS_Store
+.cache
+.env
+dist
+.output
+.nitro
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/solid-start/basic-nitro-spa/package.json b/e2e/solid-start/basic-nitro-spa/package.json
new file mode 100644
index 00000000000..b6bde8093c3
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "tanstack-solid-start-e2e-basic-nitro-spa",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev:v2": "vite dev -c vite.config.v2.ts --port 3000",
+ "dev:v3": "vite dev -c vite.config.v3.ts --port 3000",
+ "dev:e2e:v2": "vite dev -c vite.config.v2.ts",
+ "dev:e2e:v3": "vite dev -c vite.config.v3.ts",
+ "build:v2": "vite build -c vite.config.v2.ts && tsc --noEmit",
+ "build:v3": "vite build -c vite.config.v3.ts && tsc --noEmit",
+ "preview:v2": "vite preview -c vite.config.v2.ts",
+ "preview:v3": "vite preview -c vite.config.v3.ts",
+ "test:e2e:shared": "rm -rf port*.txt; playwright test --project=chromium",
+ "test:e2e:v2": "rm -rf .output dist .nitro && NITRO_VARIANT=v2 pnpm run test:e2e:shared",
+ "test:e2e:v3": "rm -rf .output dist .nitro && NITRO_VARIANT=v3 pnpm run test:e2e:shared",
+ "test:e2e": "pnpm run test:e2e:v2 && pnpm run test:e2e:v3"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/solid-router-devtools": "workspace:^",
+ "@tanstack/solid-start": "workspace:^",
+ "solid-js": "^1.9.10"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tailwindcss/postcss": "^4.1.15",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@tanstack/nitro-v2-vite-plugin": "workspace:^",
+ "@types/node": "^22.10.2",
+ "nitro": "npm:nitro-nightly@latest",
+ "postcss": "^8.5.1",
+ "tailwindcss": "^4.1.15",
+ "typescript": "^5.7.2",
+ "vite": "^7.1.7",
+ "vite-plugin-solid": "^2.11.10",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/solid-start/basic-nitro-spa/playwright.config.ts b/e2e/solid-start/basic-nitro-spa/playwright.config.ts
new file mode 100644
index 00000000000..b26f5493b47
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/playwright.config.ts
@@ -0,0 +1,44 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+const nitroVariant = process.env.NITRO_VARIANT
+if (nitroVariant !== 'v2' && nitroVariant !== 'v3') {
+ throw new Error('Set NITRO_VARIANT to "v2" or "v3" for Nitro e2e tests.')
+}
+const buildScript = nitroVariant === 'v2' ? 'build:v2' : 'build:v3'
+const buildCommand = `pnpm run ${buildScript}`
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ // Note: We run node directly instead of vite preview because Nitro's
+ // configurePreviewServer spawns on a random port. The prerendering during
+ // build uses vite.preview() correctly.
+ command: `${buildCommand} && PORT=${PORT} node .output/server/index.mjs`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-start/basic-nitro-spa/postcss.config.mjs b/e2e/solid-start/basic-nitro-spa/postcss.config.mjs
new file mode 100644
index 00000000000..a7f73a2d1d7
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/e2e/solid-start/basic-nitro-spa/public/android-chrome-192x192.png b/e2e/solid-start/basic-nitro-spa/public/android-chrome-192x192.png
new file mode 100644
index 00000000000..09c8324f8c6
Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/android-chrome-192x192.png differ
diff --git a/e2e/solid-start/basic-nitro-spa/public/android-chrome-512x512.png b/e2e/solid-start/basic-nitro-spa/public/android-chrome-512x512.png
new file mode 100644
index 00000000000..11d626ea3d0
Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/android-chrome-512x512.png differ
diff --git a/e2e/solid-start/basic-nitro-spa/public/apple-touch-icon.png b/e2e/solid-start/basic-nitro-spa/public/apple-touch-icon.png
new file mode 100644
index 00000000000..5a9423cc02c
Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/apple-touch-icon.png differ
diff --git a/e2e/solid-start/basic-nitro-spa/public/favicon-16x16.png b/e2e/solid-start/basic-nitro-spa/public/favicon-16x16.png
new file mode 100644
index 00000000000..e3389b00443
Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/favicon-16x16.png differ
diff --git a/e2e/solid-start/basic-nitro-spa/public/favicon-32x32.png b/e2e/solid-start/basic-nitro-spa/public/favicon-32x32.png
new file mode 100644
index 00000000000..900c77d444c
Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/favicon-32x32.png differ
diff --git a/e2e/solid-start/basic-nitro-spa/public/favicon.ico b/e2e/solid-start/basic-nitro-spa/public/favicon.ico
new file mode 100644
index 00000000000..1a1751676f7
Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/favicon.ico differ
diff --git a/e2e/solid-start/basic-nitro-spa/public/favicon.png b/e2e/solid-start/basic-nitro-spa/public/favicon.png
new file mode 100644
index 00000000000..1e77bc06091
Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/favicon.png differ
diff --git a/e2e/solid-start/basic-nitro-spa/public/site.webmanifest b/e2e/solid-start/basic-nitro-spa/public/site.webmanifest
new file mode 100644
index 00000000000..fa99de77db6
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/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/solid-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx b/e2e/solid-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx
new file mode 100644
index 00000000000..2c0d464a066
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx
@@ -0,0 +1,53 @@
+import {
+ ErrorComponent,
+ Link,
+ rootRouteId,
+ useMatch,
+ useRouter,
+} from '@tanstack/solid-router'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
+ const router = useRouter()
+ const isRoot = useMatch({
+ strict: false,
+ select: (state) => state.id === rootRouteId,
+ })
+
+ console.error(error)
+
+ return (
+
+
+
+
+ {isRoot() ? (
+
+ Home
+
+ ) : (
+ {
+ e.preventDefault()
+ window.history.back()
+ }}
+ >
+ Go Back
+
+ )}
+
+
+ )
+}
diff --git a/e2e/solid-start/basic-nitro-spa/src/components/NotFound.tsx b/e2e/solid-start/basic-nitro-spa/src/components/NotFound.tsx
new file mode 100644
index 00000000000..c48444862b5
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/src/components/NotFound.tsx
@@ -0,0 +1,25 @@
+import { Link } from '@tanstack/solid-router'
+
+export function NotFound({ children }: { children?: any }) {
+ return (
+
+
+ {children ||
The page you are looking for does not exist.
}
+
+
+
+
+ Start Over
+
+
+
+ )
+}
diff --git a/e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts b/e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts
new file mode 100644
index 00000000000..2bd11546dd6
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts
@@ -0,0 +1,86 @@
+/* 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 StaticRouteImport } from './routes/static'
+import { Route as IndexRouteImport } from './routes/index'
+
+const StaticRoute = StaticRouteImport.update({
+ id: '/static',
+ path: '/static',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/static': typeof StaticRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/static': typeof StaticRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/static': typeof StaticRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/static'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/static'
+ id: '__root__' | '/' | '/static'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ StaticRoute: typeof StaticRoute
+}
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/static': {
+ id: '/static'
+ path: '/static'
+ fullPath: '/static'
+ preLoaderRoute: typeof StaticRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ StaticRoute: StaticRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/solid-start'
+declare module '@tanstack/solid-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/e2e/solid-start/basic-nitro-spa/src/router.tsx b/e2e/solid-start/basic-nitro-spa/src/router.tsx
new file mode 100644
index 00000000000..5da353c1ce2
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/solid-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/solid-start/basic-nitro-spa/src/routes/__root.tsx b/e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx
new file mode 100644
index 00000000000..7020b288737
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx
@@ -0,0 +1,75 @@
+///
+import {
+ HeadContent,
+ Link,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/solid-router'
+import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
+import { HydrationScript } from 'solid-js/web'
+import type * as Solid from 'solid-js'
+import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
+import { NotFound } from '~/components/NotFound'
+import appCss from '~/styles/app.css?url'
+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 + Nitro SPA E2E Test',
+ description: 'Testing nitro SPA integration with TanStack Start',
+ }),
+ ],
+ links: [
+ { rel: 'stylesheet', href: appCss },
+ { rel: 'icon', href: '/favicon.ico' },
+ ],
+ }),
+ errorComponent: DefaultCatchBoundary,
+ notFoundComponent: () => ,
+ shellComponent: RootDocument,
+})
+
+function RootDocument({ children }: { children: Solid.JSX.Element }) {
+ return (
+
+
+
+
+
+
+
+
+ Home
+
+
+ Static
+
+
+
+ {children}
+
+
+
+
+ )
+}
diff --git a/e2e/solid-start/basic-nitro-spa/src/routes/index.tsx b/e2e/solid-start/basic-nitro-spa/src/routes/index.tsx
new file mode 100644
index 00000000000..fdcf05ce075
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/src/routes/index.tsx
@@ -0,0 +1,26 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+
+export const Route = createFileRoute('/')({
+ loader: () => getData(),
+ component: Home,
+})
+
+const getData = createServerFn().handler(() => {
+ return {
+ message: 'Hello from Nitro server!',
+ timestamp: new Date().toISOString(),
+ }
+})
+
+function Home() {
+ const data = Route.useLoaderData()
+
+ return (
+
+
Welcome Home!
+
{data().message}
+
Loaded at: {data().timestamp}
+
+ )
+}
diff --git a/e2e/solid-start/basic-nitro-spa/src/routes/static.tsx b/e2e/solid-start/basic-nitro-spa/src/routes/static.tsx
new file mode 100644
index 00000000000..55c781f286c
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/src/routes/static.tsx
@@ -0,0 +1,26 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+
+export const Route = createFileRoute('/static')({
+ loader: () => getStaticData(),
+ component: StaticPage,
+})
+
+const getStaticData = createServerFn().handler(() => {
+ return {
+ content: 'This page was prerendered at build time',
+ buildTime: new Date().toISOString(),
+ }
+})
+
+function StaticPage() {
+ const data = Route.useLoaderData()
+
+ return (
+
+
Static Page
+
{data().content}
+
Build time: {data().buildTime}
+
+ )
+}
diff --git a/e2e/solid-start/basic-nitro-spa/src/styles/app.css b/e2e/solid-start/basic-nitro-spa/src/styles/app.css
new file mode 100644
index 00000000000..c36c737cd46
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/src/styles/app.css
@@ -0,0 +1,30 @@
+@import 'tailwindcss';
+
+@layer base {
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentcolor);
+ }
+}
+
+@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/solid-start/basic-nitro-spa/src/utils/seo.ts b/e2e/solid-start/basic-nitro-spa/src/utils/seo.ts
new file mode 100644
index 00000000000..d18ad84b74e
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/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/solid-start/basic-nitro-spa/tests/app.spec.ts b/e2e/solid-start/basic-nitro-spa/tests/app.spec.ts
new file mode 100644
index 00000000000..b0f96e373af
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/tests/app.spec.ts
@@ -0,0 +1,30 @@
+import { existsSync } from 'node:fs'
+import { join } from 'node:path'
+import { expect, test } from '@playwright/test'
+
+test('SPA shell is prerendered during build with nitro', async ({ page }) => {
+ const outputDir = join(process.cwd(), '.output', 'public')
+ expect(existsSync(join(outputDir, 'index.html'))).toBe(true)
+
+ await page.goto('/')
+ await expect(page.getByTestId('home-heading')).toBeVisible()
+})
+
+test('server functions work with nitro', async ({ page }) => {
+ await page.goto('/')
+ await expect(page.getByTestId('home-heading')).toHaveText('Welcome Home!')
+ await expect(page.getByTestId('message')).toHaveText(
+ 'Hello from Nitro server!',
+ )
+})
+
+test('client-side navigation works in SPA mode', async ({ page }) => {
+ await page.goto('/')
+ await expect(page.getByTestId('home-heading')).toBeVisible()
+
+ await page.click('a[href="/static"]')
+ await expect(page.getByTestId('static-heading')).toBeVisible()
+
+ await page.click('a[href="/"]')
+ await expect(page.getByTestId('home-heading')).toBeVisible()
+})
diff --git a/e2e/solid-start/basic-nitro-spa/tsconfig.json b/e2e/solid-start/basic-nitro-spa/tsconfig.json
new file mode 100644
index 00000000000..ed8b73fa2dd
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "include": ["**/*.ts", "**/*.tsx"],
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "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/solid-start/basic-nitro-spa/vite.config.v2.ts b/e2e/solid-start/basic-nitro-spa/vite.config.v2.ts
new file mode 100644
index 00000000000..6677a660004
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/vite.config.v2.ts
@@ -0,0 +1,23 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
+import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin'
+import viteSolid from 'vite-plugin-solid'
+
+export default defineConfig({
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ spa: {
+ enabled: true,
+ prerender: {
+ outputPath: 'index.html',
+ },
+ },
+ }),
+ viteSolid({ ssr: true }),
+ nitroV2Plugin(),
+ ],
+})
diff --git a/e2e/solid-start/basic-nitro-spa/vite.config.v3.ts b/e2e/solid-start/basic-nitro-spa/vite.config.v3.ts
new file mode 100644
index 00000000000..2a9b082bf2e
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/vite.config.v3.ts
@@ -0,0 +1,23 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
+import { nitro } from 'nitro/vite'
+import viteSolid from 'vite-plugin-solid'
+
+export default defineConfig({
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ spa: {
+ enabled: true,
+ prerender: {
+ outputPath: 'index.html',
+ },
+ },
+ }),
+ viteSolid({ ssr: true }),
+ nitro(),
+ ],
+})
diff --git a/e2e/solid-start/basic-nitro/.gitignore b/e2e/solid-start/basic-nitro/.gitignore
new file mode 100644
index 00000000000..114d10aa0e4
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/.gitignore
@@ -0,0 +1,12 @@
+node_modules
+.DS_Store
+.cache
+.env
+dist
+.output
+.nitro
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/solid-start/basic-nitro/.prettierignore b/e2e/solid-start/basic-nitro/.prettierignore
new file mode 100644
index 00000000000..a16e01379d7
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/.prettierignore
@@ -0,0 +1 @@
+routeTree.gen.ts
diff --git a/e2e/solid-start/basic-nitro/package.json b/e2e/solid-start/basic-nitro/package.json
new file mode 100644
index 00000000000..92363500e09
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "tanstack-solid-start-e2e-basic-nitro",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev:v2": "vite dev -c vite.config.v2.ts --port 3000",
+ "dev:v3": "vite dev -c vite.config.v3.ts --port 3000",
+ "dev:e2e:v2": "vite dev -c vite.config.v2.ts",
+ "dev:e2e:v3": "vite dev -c vite.config.v3.ts",
+ "build:v2": "vite build -c vite.config.v2.ts && tsc --noEmit",
+ "build:v3": "vite build -c vite.config.v3.ts && tsc --noEmit",
+ "preview:v2": "vite preview -c vite.config.v2.ts",
+ "preview:v3": "vite preview -c vite.config.v3.ts",
+ "test:e2e:shared": "rm -rf port*.txt; playwright test --project=chromium",
+ "test:e2e:v2": "rm -rf .output dist .nitro && NITRO_VARIANT=v2 pnpm run test:e2e:shared",
+ "test:e2e:v3": "rm -rf .output dist .nitro && NITRO_VARIANT=v3 pnpm run test:e2e:shared",
+ "test:e2e": "pnpm run test:e2e:v2 && pnpm run test:e2e:v3"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/solid-router-devtools": "workspace:^",
+ "@tanstack/solid-start": "workspace:^",
+ "solid-js": "^1.9.10"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tailwindcss/postcss": "^4.1.15",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@tanstack/nitro-v2-vite-plugin": "workspace:^",
+ "@types/node": "^22.10.2",
+ "nitro": "npm:nitro-nightly@latest",
+ "postcss": "^8.5.1",
+ "tailwindcss": "^4.1.15",
+ "typescript": "^5.7.2",
+ "vite": "^7.1.7",
+ "vite-plugin-solid": "^2.11.10",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/solid-start/basic-nitro/playwright.config.ts b/e2e/solid-start/basic-nitro/playwright.config.ts
new file mode 100644
index 00000000000..b26f5493b47
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/playwright.config.ts
@@ -0,0 +1,44 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+const nitroVariant = process.env.NITRO_VARIANT
+if (nitroVariant !== 'v2' && nitroVariant !== 'v3') {
+ throw new Error('Set NITRO_VARIANT to "v2" or "v3" for Nitro e2e tests.')
+}
+const buildScript = nitroVariant === 'v2' ? 'build:v2' : 'build:v3'
+const buildCommand = `pnpm run ${buildScript}`
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ // Note: We run node directly instead of vite preview because Nitro's
+ // configurePreviewServer spawns on a random port. The prerendering during
+ // build uses vite.preview() correctly.
+ command: `${buildCommand} && PORT=${PORT} node .output/server/index.mjs`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/solid-start/basic-nitro/postcss.config.mjs b/e2e/solid-start/basic-nitro/postcss.config.mjs
new file mode 100644
index 00000000000..a7f73a2d1d7
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/e2e/solid-start/basic-nitro/public/android-chrome-192x192.png b/e2e/solid-start/basic-nitro/public/android-chrome-192x192.png
new file mode 100644
index 00000000000..09c8324f8c6
Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/android-chrome-192x192.png differ
diff --git a/e2e/solid-start/basic-nitro/public/android-chrome-512x512.png b/e2e/solid-start/basic-nitro/public/android-chrome-512x512.png
new file mode 100644
index 00000000000..11d626ea3d0
Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/android-chrome-512x512.png differ
diff --git a/e2e/solid-start/basic-nitro/public/apple-touch-icon.png b/e2e/solid-start/basic-nitro/public/apple-touch-icon.png
new file mode 100644
index 00000000000..5a9423cc02c
Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/apple-touch-icon.png differ
diff --git a/e2e/solid-start/basic-nitro/public/favicon-16x16.png b/e2e/solid-start/basic-nitro/public/favicon-16x16.png
new file mode 100644
index 00000000000..e3389b00443
Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/favicon-16x16.png differ
diff --git a/e2e/solid-start/basic-nitro/public/favicon-32x32.png b/e2e/solid-start/basic-nitro/public/favicon-32x32.png
new file mode 100644
index 00000000000..900c77d444c
Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/favicon-32x32.png differ
diff --git a/e2e/solid-start/basic-nitro/public/favicon.ico b/e2e/solid-start/basic-nitro/public/favicon.ico
new file mode 100644
index 00000000000..1a1751676f7
Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/favicon.ico differ
diff --git a/e2e/solid-start/basic-nitro/public/favicon.png b/e2e/solid-start/basic-nitro/public/favicon.png
new file mode 100644
index 00000000000..1e77bc06091
Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/favicon.png differ
diff --git a/e2e/solid-start/basic-nitro/public/site.webmanifest b/e2e/solid-start/basic-nitro/public/site.webmanifest
new file mode 100644
index 00000000000..fa99de77db6
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/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/solid-start/basic-nitro/src/components/DefaultCatchBoundary.tsx b/e2e/solid-start/basic-nitro/src/components/DefaultCatchBoundary.tsx
new file mode 100644
index 00000000000..2c0d464a066
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/src/components/DefaultCatchBoundary.tsx
@@ -0,0 +1,53 @@
+import {
+ ErrorComponent,
+ Link,
+ rootRouteId,
+ useMatch,
+ useRouter,
+} from '@tanstack/solid-router'
+import type { ErrorComponentProps } from '@tanstack/solid-router'
+
+export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
+ const router = useRouter()
+ const isRoot = useMatch({
+ strict: false,
+ select: (state) => state.id === rootRouteId,
+ })
+
+ console.error(error)
+
+ return (
+
+
+
+
+ {isRoot() ? (
+
+ Home
+
+ ) : (
+ {
+ e.preventDefault()
+ window.history.back()
+ }}
+ >
+ Go Back
+
+ )}
+
+
+ )
+}
diff --git a/e2e/solid-start/basic-nitro/src/components/NotFound.tsx b/e2e/solid-start/basic-nitro/src/components/NotFound.tsx
new file mode 100644
index 00000000000..c48444862b5
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/src/components/NotFound.tsx
@@ -0,0 +1,25 @@
+import { Link } from '@tanstack/solid-router'
+
+export function NotFound({ children }: { children?: any }) {
+ return (
+
+
+ {children ||
The page you are looking for does not exist.
}
+
+
+
+
+ Start Over
+
+
+
+ )
+}
diff --git a/e2e/solid-start/basic-nitro/src/routeTree.gen.ts b/e2e/solid-start/basic-nitro/src/routeTree.gen.ts
new file mode 100644
index 00000000000..2bd11546dd6
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/src/routeTree.gen.ts
@@ -0,0 +1,86 @@
+/* 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 StaticRouteImport } from './routes/static'
+import { Route as IndexRouteImport } from './routes/index'
+
+const StaticRoute = StaticRouteImport.update({
+ id: '/static',
+ path: '/static',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/static': typeof StaticRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/static': typeof StaticRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/static': typeof StaticRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/static'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/static'
+ id: '__root__' | '/' | '/static'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ StaticRoute: typeof StaticRoute
+}
+
+declare module '@tanstack/solid-router' {
+ interface FileRoutesByPath {
+ '/static': {
+ id: '/static'
+ path: '/static'
+ fullPath: '/static'
+ preLoaderRoute: typeof StaticRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ StaticRoute: StaticRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/solid-start'
+declare module '@tanstack/solid-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/e2e/solid-start/basic-nitro/src/router.tsx b/e2e/solid-start/basic-nitro/src/router.tsx
new file mode 100644
index 00000000000..5da353c1ce2
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/src/router.tsx
@@ -0,0 +1,16 @@
+import { createRouter } from '@tanstack/solid-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/solid-start/basic-nitro/src/routes/__root.tsx b/e2e/solid-start/basic-nitro/src/routes/__root.tsx
new file mode 100644
index 00000000000..9d2f56b6756
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/src/routes/__root.tsx
@@ -0,0 +1,94 @@
+///
+import {
+ HeadContent,
+ Link,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/solid-router'
+import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
+import { HydrationScript } from 'solid-js/web'
+import type * as Solid from 'solid-js'
+import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
+import { NotFound } from '~/components/NotFound'
+import appCss from '~/styles/app.css?url'
+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 Solid Framework',
+ description: `TanStack Start is a type-safe, client-first, full-stack Solid framework. `,
+ }),
+ ],
+ links: [
+ { rel: 'stylesheet', href: appCss },
+ {
+ 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: DefaultCatchBoundary,
+ notFoundComponent: () => ,
+ shellComponent: RootDocument,
+})
+
+function RootDocument({ children }: { children: Solid.JSX.Element }) {
+ return (
+
+
+
+
+
+
+
+
+ Home
+
+
+ Static
+
+
+
+ {children}
+
+
+
+
+ )
+}
diff --git a/e2e/solid-start/basic-nitro/src/routes/index.tsx b/e2e/solid-start/basic-nitro/src/routes/index.tsx
new file mode 100644
index 00000000000..681a335b355
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/src/routes/index.tsx
@@ -0,0 +1,26 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+
+export const Route = createFileRoute('/')({
+ loader: () => getData(),
+ component: Home,
+})
+
+const getData = createServerFn().handler(() => {
+ return {
+ message: `Running in ${typeof navigator !== 'undefined' ? navigator.userAgent : 'Unknown'}`,
+ runtime: 'Nitro',
+ }
+})
+
+function Home() {
+ const data = Route.useLoaderData()
+
+ return (
+
+
Welcome Home!!!
+
{data().message}
+
{data().runtime}
+
+ )
+}
diff --git a/e2e/solid-start/basic-nitro/src/routes/static.tsx b/e2e/solid-start/basic-nitro/src/routes/static.tsx
new file mode 100644
index 00000000000..5d7a478efd6
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/src/routes/static.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/static')({
+ component: StaticPage,
+})
+
+function StaticPage() {
+ return (
+
+
Static Page
+
This page was prerendered with Nitro
+
+ )
+}
diff --git a/e2e/solid-start/basic-nitro/src/styles/app.css b/e2e/solid-start/basic-nitro/src/styles/app.css
new file mode 100644
index 00000000000..c36c737cd46
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/src/styles/app.css
@@ -0,0 +1,30 @@
+@import 'tailwindcss';
+
+@layer base {
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentcolor);
+ }
+}
+
+@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/solid-start/basic-nitro/src/utils/seo.ts b/e2e/solid-start/basic-nitro/src/utils/seo.ts
new file mode 100644
index 00000000000..d18ad84b74e
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/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/solid-start/basic-nitro/tests/app.spec.ts b/e2e/solid-start/basic-nitro/tests/app.spec.ts
new file mode 100644
index 00000000000..c48116ea805
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/tests/app.spec.ts
@@ -0,0 +1,36 @@
+import { existsSync, readFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+test('returns correct runtime info', async ({ page }) => {
+ await page.goto('/')
+ await expect(page.getByTestId('message')).toContainText('Running in Node.js')
+ await expect(page.getByTestId('runtime')).toHaveText('Nitro')
+})
+
+test('prerender with Nitro', async ({ page }) => {
+ const distDir = join(process.cwd(), '.output', 'public')
+ const staticHtmlPath = join(distDir, 'static', 'index.html')
+ expect(existsSync(staticHtmlPath)).toBe(true)
+ const staticHtml = readFileSync(staticHtmlPath, 'utf8')
+ const normalizedHtml = staticHtml.replace(//g, '')
+ expect(normalizedHtml).toContain('This page was prerendered with Nitro')
+
+ await page.goto('/static')
+ await expect(page.getByTestId('static-heading')).toHaveText('Static Page')
+ await expect(page.getByTestId('static-content')).toHaveText(
+ 'This page was prerendered with Nitro',
+ )
+})
+
+test('client-side navigation works', async ({ page }) => {
+ await page.goto('/')
+ await expect(page.getByTestId('message')).toContainText('Running in Node.js')
+
+ await page.getByRole('link', { name: 'Static' }).click()
+ await expect(page.getByTestId('static-heading')).toHaveText('Static Page')
+
+ await page.getByRole('link', { name: 'Home' }).click()
+ await expect(page.getByTestId('message')).toContainText('Running in Node.js')
+})
diff --git a/e2e/solid-start/basic-nitro/tsconfig.json b/e2e/solid-start/basic-nitro/tsconfig.json
new file mode 100644
index 00000000000..ed8b73fa2dd
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "include": ["**/*.ts", "**/*.tsx"],
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "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/solid-start/basic-nitro/vite.config.v2.ts b/e2e/solid-start/basic-nitro/vite.config.v2.ts
new file mode 100644
index 00000000000..15ad2fabfc3
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/vite.config.v2.ts
@@ -0,0 +1,21 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
+import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin'
+import viteSolid from 'vite-plugin-solid'
+
+export default defineConfig({
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ prerender: {
+ enabled: true,
+ filter: (page) => page.path === '/static',
+ },
+ }),
+ viteSolid({ ssr: true }),
+ nitroV2Plugin(),
+ ],
+})
diff --git a/e2e/solid-start/basic-nitro/vite.config.v3.ts b/e2e/solid-start/basic-nitro/vite.config.v3.ts
new file mode 100644
index 00000000000..5f6a5f48e73
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/vite.config.v3.ts
@@ -0,0 +1,21 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
+import { nitro } from 'nitro/vite'
+import viteSolid from 'vite-plugin-solid'
+
+export default defineConfig({
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ prerender: {
+ enabled: true,
+ filter: (page) => page.path === '/static',
+ },
+ }),
+ viteSolid({ ssr: true }),
+ nitro(),
+ ],
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3ab9c9061b6..ff91aeb6675 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1293,6 +1293,122 @@ importers:
specifier: ^4.49.1
version: 4.49.1
+ e2e/react-start/basic-nitro:
+ 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
+ react:
+ specifier: ^19.2.0
+ version: 19.2.0
+ react-dom:
+ specifier: ^19.2.0
+ version: 19.2.0(react@19.2.0)
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tailwindcss/postcss':
+ specifier: ^4.1.15
+ version: 4.1.18
+ '@tanstack/nitro-v2-vite-plugin':
+ specifier: workspace:*
+ version: link:../../../packages/nitro-v2-vite-plugin
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ '@types/react':
+ specifier: ^19.2.2
+ version: 19.2.2
+ '@types/react-dom':
+ specifier: ^19.2.2
+ version: 19.2.2(@types/react@19.2.2)
+ nitro:
+ specifier: npm:nitro-nightly@latest
+ version: nitro-nightly@3.0.1-20251230-165713-6e801e22(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@4.0.3)(ioredis@5.8.0)(lru-cache@11.2.2)(mysql2@3.15.3)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.6
+ tailwindcss:
+ specifier: ^4.1.15
+ version: 4.1.18
+ 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.1)(lightningcss@1.30.2)(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.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
+ e2e/react-start/basic-nitro-spa:
+ 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
+ react:
+ specifier: ^19.2.0
+ version: 19.2.0
+ react-dom:
+ specifier: ^19.2.0
+ version: 19.2.0(react@19.2.0)
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tailwindcss/postcss':
+ specifier: ^4.1.15
+ version: 4.1.18
+ '@tanstack/nitro-v2-vite-plugin':
+ specifier: workspace:*
+ version: link:../../../packages/nitro-v2-vite-plugin
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ '@types/react':
+ specifier: ^19.2.2
+ version: 19.2.2
+ '@types/react-dom':
+ specifier: ^19.2.2
+ version: 19.2.2(@types/react@19.2.2)
+ nitro:
+ specifier: npm:nitro-nightly@latest
+ version: nitro-nightly@3.0.1-20251230-165713-6e801e22(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@4.0.3)(ioredis@5.8.0)(lru-cache@11.2.2)(mysql2@3.15.3)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.6
+ tailwindcss:
+ specifier: ^4.1.15
+ version: 4.1.18
+ 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.1)(lightningcss@1.30.2)(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.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
e2e/react-start/basic-react-query:
dependencies:
'@tanstack/react-query':
@@ -3251,6 +3367,110 @@ importers:
specifier: ^4.49.1
version: 4.49.1
+ e2e/solid-start/basic-nitro:
+ dependencies:
+ '@tanstack/solid-router':
+ specifier: workspace:^
+ version: link:../../../packages/solid-router
+ '@tanstack/solid-router-devtools':
+ specifier: workspace:^
+ version: link:../../../packages/solid-router-devtools
+ '@tanstack/solid-start':
+ specifier: workspace:*
+ version: link:../../../packages/solid-start
+ solid-js:
+ specifier: 1.9.10
+ version: 1.9.10
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tailwindcss/postcss':
+ specifier: ^4.1.15
+ version: 4.1.18
+ '@tanstack/nitro-v2-vite-plugin':
+ specifier: workspace:*
+ version: link:../../../packages/nitro-v2-vite-plugin
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ nitro:
+ specifier: npm:nitro-nightly@latest
+ version: nitro-nightly@3.0.1-20251230-165713-6e801e22(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@4.0.3)(ioredis@5.8.0)(lru-cache@11.2.2)(mysql2@3.15.3)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.6
+ tailwindcss:
+ specifier: ^4.1.15
+ version: 4.1.18
+ 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.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ vite-plugin-solid:
+ specifier: ^2.11.10
+ version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(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.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
+ e2e/solid-start/basic-nitro-spa:
+ dependencies:
+ '@tanstack/solid-router':
+ specifier: workspace:^
+ version: link:../../../packages/solid-router
+ '@tanstack/solid-router-devtools':
+ specifier: workspace:^
+ version: link:../../../packages/solid-router-devtools
+ '@tanstack/solid-start':
+ specifier: workspace:*
+ version: link:../../../packages/solid-start
+ solid-js:
+ specifier: 1.9.10
+ version: 1.9.10
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tailwindcss/postcss':
+ specifier: ^4.1.15
+ version: 4.1.18
+ '@tanstack/nitro-v2-vite-plugin':
+ specifier: workspace:*
+ version: link:../../../packages/nitro-v2-vite-plugin
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ nitro:
+ specifier: npm:nitro-nightly@latest
+ version: nitro-nightly@3.0.1-20251230-165713-6e801e22(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@4.0.3)(ioredis@5.8.0)(lru-cache@11.2.2)(mysql2@3.15.3)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.6
+ tailwindcss:
+ specifier: ^4.1.15
+ version: 4.1.18
+ 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.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ vite-plugin-solid:
+ specifier: ^2.11.10
+ version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(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.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
e2e/solid-start/basic-solid-query:
dependencies:
'@tanstack/solid-query':
@@ -12846,15 +13066,9 @@ packages:
'@electric-sql/pglite@0.3.2':
resolution: {integrity: sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==}
- '@emnapi/core@1.5.0':
- resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
-
'@emnapi/core@1.7.1':
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
- '@emnapi/runtime@1.5.0':
- resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
-
'@emnapi/runtime@1.7.1':
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
@@ -24640,25 +24854,14 @@ snapshots:
'@electric-sql/pglite@0.3.2': {}
- '@emnapi/core@1.5.0':
- dependencies:
- '@emnapi/wasi-threads': 1.1.0
- tslib: 2.8.1
-
'@emnapi/core@1.7.1':
dependencies:
'@emnapi/wasi-threads': 1.1.0
tslib: 2.8.1
- optional: true
-
- '@emnapi/runtime@1.5.0':
- dependencies:
- tslib: 2.8.1
'@emnapi/runtime@1.7.1':
dependencies:
tslib: 2.8.1
- optional: true
'@emnapi/wasi-threads@1.1.0':
dependencies:
@@ -25719,12 +25922,12 @@ snapshots:
'@img/sharp-wasm32@0.33.5':
dependencies:
- '@emnapi/runtime': 1.5.0
+ '@emnapi/runtime': 1.7.1
optional: true
'@img/sharp-wasm32@0.34.4':
dependencies:
- '@emnapi/runtime': 1.5.0
+ '@emnapi/runtime': 1.7.1
optional: true
'@img/sharp-win32-arm64@0.34.4':
@@ -26200,15 +26403,15 @@ snapshots:
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
- '@emnapi/core': 1.5.0
- '@emnapi/runtime': 1.5.0
+ '@emnapi/core': 1.7.1
+ '@emnapi/runtime': 1.7.1
'@tybys/wasm-util': 0.10.1
optional: true
'@napi-rs/wasm-runtime@0.2.4':
dependencies:
- '@emnapi/core': 1.5.0
- '@emnapi/runtime': 1.5.0
+ '@emnapi/core': 1.7.1
+ '@emnapi/runtime': 1.7.1
'@tybys/wasm-util': 0.9.0
'@napi-rs/wasm-runtime@1.1.0':