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..79fc2616b05
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "tanstack-react-start-e2e-basic-nitro-spa",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev --port 3000",
+ "dev:e2e": "vite dev",
+ "build": "vite build && tsc --noEmit",
+ "preview": "vite preview",
+ "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+ },
+ "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:^",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "nitro": "^3.0.1-alpha.1",
+ "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..16ca7c3e0d9
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/playwright.config.ts
@@ -0,0 +1,38 @@
+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}`
+
+/**
+ * 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: `pnpm build && 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.ts b/e2e/react-start/basic-nitro-spa/vite.config.ts
new file mode 100644
index 00000000000..37f603e3ba8
--- /dev/null
+++ b/e2e/react-start/basic-nitro-spa/vite.config.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..6d1f338b6f6
--- /dev/null
+++ b/e2e/react-start/basic-nitro/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "tanstack-react-start-e2e-basic-nitro",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev --port 3000",
+ "dev:e2e": "vite dev",
+ "build": "vite build && tsc --noEmit",
+ "preview": "vite preview",
+ "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+ },
+ "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:^",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "nitro": "^3.0.1-alpha.1",
+ "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..16ca7c3e0d9
--- /dev/null
+++ b/e2e/react-start/basic-nitro/playwright.config.ts
@@ -0,0 +1,38 @@
+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}`
+
+/**
+ * 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: `pnpm build && 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..6954aadb406
--- /dev/null
+++ b/e2e/react-start/basic-nitro/tests/app.spec.ts
@@ -0,0 +1,32 @@
+import { existsSync } 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')
+ expect(existsSync(join(distDir, 'static', 'index.html'))).toBe(true)
+
+ 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/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.ts b/e2e/react-start/basic-nitro/vite.config.ts
new file mode 100644
index 00000000000..eb7246636c4
--- /dev/null
+++ b/e2e/react-start/basic-nitro/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 { 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..21734f69620
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "tanstack-solid-start-e2e-basic-nitro-spa",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev --port 3000",
+ "dev:e2e": "vite dev",
+ "build": "vite build && tsc --noEmit",
+ "preview": "vite preview",
+ "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+ },
+ "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:^",
+ "@types/node": "^22.10.2",
+ "nitro": "^3.0.1-alpha.1",
+ "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..16ca7c3e0d9
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/playwright.config.ts
@@ -0,0 +1,38 @@
+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}`
+
+/**
+ * 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: `pnpm build && 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.ts b/e2e/solid-start/basic-nitro-spa/vite.config.ts
new file mode 100644
index 00000000000..2a9b082bf2e
--- /dev/null
+++ b/e2e/solid-start/basic-nitro-spa/vite.config.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..ceb5e636f7a
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "tanstack-solid-start-e2e-basic-nitro",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev --port 3000",
+ "dev:e2e": "vite dev",
+ "build": "vite build && tsc --noEmit",
+ "preview": "vite preview",
+ "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+ },
+ "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:^",
+ "@types/node": "^22.10.2",
+ "nitro": "^3.0.1-alpha.1",
+ "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..16ca7c3e0d9
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/playwright.config.ts
@@ -0,0 +1,38 @@
+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}`
+
+/**
+ * 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: `pnpm build && 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..6954aadb406
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/tests/app.spec.ts
@@ -0,0 +1,32 @@
+import { existsSync } 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')
+ expect(existsSync(join(distDir, 'static', 'index.html'))).toBe(true)
+
+ 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.ts b/e2e/solid-start/basic-nitro/vite.config.ts
new file mode 100644
index 00000000000..5f6a5f48e73
--- /dev/null
+++ b/e2e/solid-start/basic-nitro/vite.config.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/packages/start-plugin-core/src/plugin.ts b/packages/start-plugin-core/src/plugin.ts
index 310d7309cf4..f8e12b557d0 100644
--- a/packages/start-plugin-core/src/plugin.ts
+++ b/packages/start-plugin-core/src/plugin.ts
@@ -50,6 +50,7 @@ export interface ResolvedStartConfig {
routerFilePath: string
srcDirectory: string
viteAppBase: string
+ viteConfigFile: string | undefined
}
export type GetConfigFn = () => {
@@ -76,6 +77,7 @@ export function TanStackStartVitePluginCore(
routerFilePath: '',
srcDirectory: '',
viteAppBase: '',
+ viteConfigFile: undefined,
}
const directive = corePluginOpts.serverFn?.directive ?? 'use server'
@@ -351,14 +353,60 @@ export function TanStackStartVitePluginCore(
},
}
},
+ configResolved(config) {
+ resolvedStartConfig.viteConfigFile = config.configFile || undefined
+ },
buildApp: {
order: 'post',
async handler(builder) {
const { startConfig } = getConfig()
- await postServerBuild({ builder, startConfig })
+ const hasNitro = builder.config.plugins.some(
+ (p): p is { name: string } =>
+ typeof p === 'object' &&
+ 'name' in p &&
+ typeof p.name === 'string' &&
+ p.name.startsWith('nitro:'),
+ )
+ await postServerBuild({
+ builder,
+ startConfig,
+ skipPrerender: hasNitro,
+ })
},
},
},
+ // Nitro module plugin - runs prerendering after Nitro build using vite preview
+ {
+ name: 'tanstack-start-core:nitro-prerender',
+ nitro: {
+ name: 'tanstack-start-prerender',
+ setup(nitro: any) {
+ nitro.hooks.hook('compiled', async () => {
+ const { startConfig, resolvedStartConfig } = getConfig()
+ if (!startConfig.prerender?.enabled && !startConfig.spa?.enabled) {
+ return
+ }
+
+ // Write nitro.json before calling vite.preview() since Nitro's
+ // configurePreviewServer hook requires it to exist. The 'compiled'
+ // hook runs before Nitro writes the build info, so we need to
+ // write a minimal version ourselves.
+ const { writeNitroBuildInfo, postServerBuildForNitro } =
+ await import('./post-server-build')
+ await writeNitroBuildInfo({
+ outputDir: nitro.options.output.dir,
+ preset: nitro.options.preset,
+ })
+
+ await postServerBuildForNitro({
+ startConfig,
+ outputDir: nitro.options.output.publicDir,
+ configFile: resolvedStartConfig.viteConfigFile,
+ })
+ })
+ },
+ },
+ } as PluginOption,
tanStackStartRouter(startPluginOpts, getConfig, corePluginOpts),
// N.B. TanStackStartCompilerPlugin must be before the TanStackServerFnPlugin
startCompilerPlugin({ framework: corePluginOpts.framework, environments }),
diff --git a/packages/start-plugin-core/src/post-server-build.ts b/packages/start-plugin-core/src/post-server-build.ts
index 01c9f746677..27b6f357d4b 100644
--- a/packages/start-plugin-core/src/post-server-build.ts
+++ b/packages/start-plugin-core/src/post-server-build.ts
@@ -1,20 +1,43 @@
+import { promises as fsp } from 'node:fs'
+import path from 'pathe'
import { HEADERS } from '@tanstack/start-server-core'
import { buildSitemap } from './build-sitemap'
import { VITE_ENVIRONMENT_NAMES } from './constants'
import { prerender } from './prerender'
+import { createLogger } from './utils'
import type { TanStackStartOutputConfig } from './schema'
import type { ViteBuilder } from 'vite'
-export async function postServerBuild({
- builder,
- startConfig,
+/**
+ * Write a minimal nitro.json file for vite.preview() to work with Nitro's
+ * configurePreviewServer hook. This is needed because the 'compiled' hook
+ * runs before Nitro writes its build info.
+ */
+export async function writeNitroBuildInfo({
+ outputDir,
+ preset,
}: {
- builder: ViteBuilder
- startConfig: TanStackStartOutputConfig
+ outputDir: string
+ preset: string
}) {
- // If the user has not set a prerender option, we need to set it to true
- // if the pages array is not empty and has sub options requiring for prerendering
- // If the user has explicitly set prerender.enabled, this should be respected
+ const logger = createLogger('prerender')
+ logger.info('Writing nitro.json for vite.preview()...')
+
+ const buildInfo = {
+ date: new Date().toJSON(),
+ preset,
+ framework: { name: 'tanstack-start' },
+ versions: {},
+ commands: {
+ preview: `node ${path.join(outputDir, 'server/index.mjs')}`,
+ },
+ }
+
+ const buildInfoPath = path.join(outputDir, 'nitro.json')
+ await fsp.writeFile(buildInfoPath, JSON.stringify(buildInfo, null, 2))
+}
+
+function setupPrerenderConfig(startConfig: TanStackStartOutputConfig) {
if (startConfig.prerender?.enabled !== false) {
startConfig.prerender = {
...startConfig.prerender,
@@ -26,7 +49,6 @@ export async function postServerBuild({
}
}
- // Setup the options for prerendering the SPA shell (i.e `src/routes/__root.tsx`)
if (startConfig.spa?.enabled) {
startConfig.prerender = {
...startConfig.prerender,
@@ -49,16 +71,26 @@ export async function postServerBuild({
},
})
}
+}
+
+export async function postServerBuild({
+ builder,
+ startConfig,
+ skipPrerender = false,
+}: {
+ builder: ViteBuilder
+ startConfig: TanStackStartOutputConfig
+ skipPrerender?: boolean
+}) {
+ setupPrerenderConfig(startConfig)
- // Run the prerendering process
- if (startConfig.prerender.enabled) {
+ if (startConfig.prerender?.enabled && !skipPrerender) {
await prerender({
startConfig,
builder,
})
}
- // Run the sitemap build process
if (startConfig.pages.length) {
buildSitemap({
startConfig,
@@ -68,3 +100,30 @@ export async function postServerBuild({
})
}
}
+
+export async function postServerBuildForNitro({
+ startConfig,
+ outputDir,
+ configFile,
+}: {
+ startConfig: TanStackStartOutputConfig
+ outputDir: string
+ configFile?: string
+}) {
+ setupPrerenderConfig(startConfig)
+
+ if (startConfig.prerender?.enabled) {
+ await prerender({
+ startConfig,
+ outputDir,
+ configFile,
+ })
+ }
+
+ if (startConfig.pages.length) {
+ buildSitemap({
+ startConfig,
+ publicDir: outputDir,
+ })
+ }
+}
diff --git a/packages/start-plugin-core/src/prerender.ts b/packages/start-plugin-core/src/prerender.ts
index ea6368109e5..ee4610a689a 100644
--- a/packages/start-plugin-core/src/prerender.ts
+++ b/packages/start-plugin-core/src/prerender.ts
@@ -1,19 +1,28 @@
import { promises as fsp } from 'node:fs'
+import { spawn } from 'node:child_process'
import os from 'node:os'
import path from 'pathe'
import { joinURL, withBase, withoutBase } from 'ufo'
import { VITE_ENVIRONMENT_NAMES } from './constants'
import { createLogger } from './utils'
import { Queue } from './queue'
-import type { PreviewServer, ResolvedConfig, ViteBuilder } from 'vite'
+import type { ChildProcess } from 'node:child_process'
+import type { PreviewServer, ViteBuilder } from 'vite'
import type { Page, TanStackStartOutputConfig } from './schema'
export async function prerender({
startConfig,
builder,
+ outputDir: outputDirOverride,
+ configFile: configFileOverride,
+ nitroServerPath,
}: {
startConfig: TanStackStartOutputConfig
- builder: ViteBuilder
+ builder?: ViteBuilder
+ outputDir?: string
+ configFile?: string
+ /** Path to Nitro's compiled server entry (e.g., .output/server/index.mjs) */
+ nitroServerPath?: string
}) {
const logger = createLogger('prerender')
logger.info('Prerendering pages...')
@@ -40,38 +49,57 @@ export async function prerender({
startConfig.pages = pages
}
- const serverEnv = builder.environments[VITE_ENVIRONMENT_NAMES.server]
+ let outputDir: string
- if (!serverEnv) {
- throw new Error(
- `Vite's "${VITE_ENVIRONMENT_NAMES.server}" environment not found`,
- )
- }
-
- const clientEnv = builder.environments[VITE_ENVIRONMENT_NAMES.client]
- if (!clientEnv) {
- throw new Error(
- `Vite's "${VITE_ENVIRONMENT_NAMES.client}" environment not found`,
- )
+ if (outputDirOverride) {
+ outputDir = outputDirOverride
+ } else if (builder) {
+ const clientEnv = builder.environments[VITE_ENVIRONMENT_NAMES.client]
+ if (!clientEnv) {
+ throw new Error(
+ `Vite's "${VITE_ENVIRONMENT_NAMES.client}" environment not found`,
+ )
+ }
+ outputDir = clientEnv.config.build.outDir
+ } else {
+ throw new Error('Either builder or outputDir must be provided')
}
- const outputDir = clientEnv.config.build.outDir
-
process.env.TSS_PRERENDERING = 'true'
- // Start Vite preview server instead of importing module
- const previewServer = await startPreviewServer(serverEnv.config)
- const baseUrl = getResolvedUrl(previewServer)
+ let cleanup: () => Promise
+ let baseUrl: URL
+
+ if (nitroServerPath) {
+ // Start Nitro server as a subprocess
+ const { url, close } = await startNitroServer(nitroServerPath)
+ baseUrl = url
+ cleanup = close
+ } else {
+ // Start Vite preview server
+ const configFile =
+ configFileOverride ??
+ builder?.environments[VITE_ENVIRONMENT_NAMES.server]?.config.configFile
+ const previewServer = await startPreviewServer(configFile)
+ baseUrl = getResolvedUrl(previewServer)
+ cleanup = async () => {
+ await previewServer.close()
+ }
+
+ // Wait for the server to be ready (handles Nitro's child process startup time)
+ await waitForServerReady(baseUrl, logger)
+ }
const isRedirectResponse = (res: Response) => {
return res.status >= 300 && res.status < 400 && res.headers.get('location')
}
+
async function localFetch(
- path: string,
+ fetchPath: string,
options?: RequestInit,
maxRedirects: number = 5,
): Promise {
- const url = new URL(path, baseUrl)
+ const url = new URL(fetchPath, baseUrl)
const request = new Request(url, options)
const response = await fetch(request)
@@ -98,8 +126,9 @@ export async function prerender({
})
} catch (error) {
logger.error(error)
+ throw error
} finally {
- await previewServer.close()
+ await cleanup()
}
function extractLinks(html: string): Array {
@@ -166,9 +195,7 @@ export async function prerender({
const res = await localFetch(
withBase(page.path, routerBasePath),
{
- headers: {
- ...(prerenderOptions.headers ?? {}),
- },
+ headers: prerenderOptions.headers,
},
prerenderOptions.maxRedirects,
)
@@ -262,19 +289,117 @@ export async function prerender({
}
}
+async function startNitroServer(
+ serverPath: string,
+): Promise<{ url: URL; close: () => Promise }> {
+ return new Promise((resolve, reject) => {
+ // Find a random port
+ const port = 3000 + Math.floor(Math.random() * 10000)
+ const env = { ...process.env, PORT: String(port) }
+
+ const child: ChildProcess = spawn('node', [serverPath], {
+ env,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ })
+
+ let resolved = false
+ const timeout = setTimeout(() => {
+ if (!resolved) {
+ child.kill()
+ reject(new Error('Nitro server startup timed out'))
+ }
+ }, 30000)
+
+ const checkServer = async () => {
+ try {
+ const res = await fetch(`http://localhost:${port}/`)
+ if (res.ok || res.status < 500) {
+ resolved = true
+ clearTimeout(timeout)
+ resolve({
+ url: new URL(`http://localhost:${port}`),
+ close: async () => {
+ child.kill('SIGTERM')
+ // Wait a bit for graceful shutdown
+ await new Promise((r) => setTimeout(r, 500))
+ },
+ })
+ }
+ } catch {
+ // Server not ready yet, retry
+ if (!resolved) {
+ setTimeout(checkServer, 100)
+ }
+ }
+ }
+
+ child.on('error', (err) => {
+ if (!resolved) {
+ clearTimeout(timeout)
+ reject(err)
+ }
+ })
+
+ child.stderr?.on('data', (data) => {
+ console.error('[nitro]', data.toString())
+ })
+
+ // Start checking after a short delay
+ setTimeout(checkServer, 200)
+ })
+}
+
async function startPreviewServer(
- viteConfig: ResolvedConfig,
+ configFile?: string | false,
): Promise {
const vite = await import('vite')
try {
- return await vite.preview({
- configFile: viteConfig.configFile,
+ const server = await vite.preview({
+ configFile,
preview: {
port: 0,
open: false,
},
})
+
+ // Check if Nitro's vite plugin is active (it spawns a child process)
+ const hasNitroPlugin = server.config.plugins.some((p) => {
+ return (
+ typeof p === 'object' &&
+ 'name' in p &&
+ typeof p.name === 'string' &&
+ p.name.startsWith('nitro:')
+ )
+ })
+
+ if (hasNitroPlugin) {
+ // Wrap the close method to handle Nitro's child process cleanup
+ // Nitro's configurePreviewServer spawns a child process and registers
+ // SIGINT/SIGHUP handlers to kill it. Since previewServer.close() doesn't
+ // trigger these signals, we need to emit SIGHUP ourselves.
+ const originalClose = server.close.bind(server)
+ server.close = async () => {
+ // Temporarily override process.exit to prevent Nitro's handler from
+ // exiting our process when we emit SIGHUP
+ const originalExit = process.exit
+ process.exit = (() => {}) as typeof process.exit
+
+ // Emit SIGHUP to trigger Nitro's child process cleanup
+ process.emit('SIGHUP', 'SIGHUP')
+
+ // Restore process.exit
+ process.exit = originalExit
+
+ // Give the child process a moment to terminate
+ await new Promise((resolve) => setTimeout(resolve, 100))
+
+ // Now close the preview server
+ return originalClose()
+ }
+ }
+
+ return server
} catch (error) {
throw new Error(
'Failed to start the Vite preview server for prerendering',
@@ -294,3 +419,30 @@ function getResolvedUrl(previewServer: PreviewServer): URL {
return new URL(baseUrl)
}
+
+async function waitForServerReady(
+ baseUrl: URL,
+ logger: ReturnType,
+ timeout = 30000,
+): Promise {
+ const startTime = Date.now()
+ const checkInterval = 100
+
+ while (Date.now() - startTime < timeout) {
+ try {
+ const response = await fetch(new URL('/', baseUrl))
+ // Server is ready if we get any response (even 404 is fine, we just need it to not error)
+ if (response.status < 500) {
+ logger.info('Server is ready')
+ return
+ }
+ } catch {
+ // Server not ready yet, retry
+ }
+ await new Promise((resolve) => setTimeout(resolve, checkInterval))
+ }
+
+ throw new Error(
+ `Server at ${baseUrl} did not become ready within ${timeout}ms`,
+ )
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4d1daf4939c..38056c16c40 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1256,6 +1256,116 @@ 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.15
+ '@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: ^3.0.1-alpha.1
+ version: 3.0.1-alpha.1(@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.17
+ 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.15
+ '@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: ^3.0.1-alpha.1
+ version: 3.0.1-alpha.1(@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.17
+ 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':
@@ -2983,6 +3093,104 @@ 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.15
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ nitro:
+ specifier: ^3.0.1-alpha.1
+ version: 3.0.1-alpha.1(@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.17
+ 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.15
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ nitro:
+ specifier: ^3.0.1-alpha.1
+ version: 3.0.1-alpha.1(@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.17
+ 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':
@@ -10114,7 +10322,7 @@ importers:
version: link:../start-storage-context
h3-v2:
specifier: npm:h3@2.0.0-beta.5
- version: h3@2.0.0-beta.5(crossws@0.4.1(srvx@0.8.15))
+ version: h3@2.0.0-beta.5(crossws@0.4.1(srvx@0.9.6))
seroval:
specifier: ^1.4.0
version: 1.4.0
@@ -12462,6 +12670,9 @@ packages:
'@napi-rs/wasm-runtime@0.2.4':
resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==}
+ '@napi-rs/wasm-runtime@1.0.7':
+ resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==}
+
'@neon-rs/load@0.0.4':
resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==}
@@ -12669,18 +12880,196 @@ packages:
resolution: {integrity: sha512-lLHUQUyYy86q+qbALr0TMVh+VQAYwNGbsxBx4LhfjvkNYG0hgAwWtq7ePebGs2nEhZmmIFl24ikuCpH2r5d3+A==}
engines: {node: '>=20.0'}
- '@oozcitak/util@9.0.4':
- resolution: {integrity: sha512-kmx1hRJlsvxiTCpK97off59LqSEOtkWOPe4rdfFL8TjZtihYSTVNObIfc86jtLngfnuIuuTRt+TUCgRS220RSQ==}
- engines: {node: '>=20.0'}
+ '@oozcitak/util@9.0.4':
+ resolution: {integrity: sha512-kmx1hRJlsvxiTCpK97off59LqSEOtkWOPe4rdfFL8TjZtihYSTVNObIfc86jtLngfnuIuuTRt+TUCgRS220RSQ==}
+ engines: {node: '>=20.0'}
+
+ '@open-draft/deferred-promise@2.2.0':
+ resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
+
+ '@open-draft/logger@0.3.0':
+ resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==}
+
+ '@open-draft/until@2.1.0':
+ resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
+
+ '@oxc-minify/binding-android-arm64@0.96.0':
+ resolution: {integrity: sha512-lzeIEMu/v6Y+La5JSesq4hvyKtKBq84cgQpKYTYM/yGuNk2tfd5Ha31hnC+mTh48lp/5vZH+WBfjVUjjINCfug==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@oxc-minify/binding-darwin-arm64@0.96.0':
+ resolution: {integrity: sha512-i0LkJAUXb4BeBFrJQbMKQPoxf8+cFEffDyLSb7NEzzKuPcH8qrVsnEItoOzeAdYam8Sr6qCHVwmBNEQzl7PWpw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxc-minify/binding-darwin-x64@0.96.0':
+ resolution: {integrity: sha512-C5vI0WPR+KPIFAD5LMOJk2J8iiT+Nv65vDXmemzXEXouzfEOLYNqnW+u6NSsccpuZHHWAiLyPFkYvKFduveAUQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxc-minify/binding-freebsd-x64@0.96.0':
+ resolution: {integrity: sha512-3//5DNx+xUjVBMLLk2sl6hfe4fwfENJtjVQUBXjxzwPuv8xgZUqASG4cRG3WqG5Qe8dV6SbCI4EgKQFjO4KCZA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@oxc-minify/binding-linux-arm-gnueabihf@0.96.0':
+ resolution: {integrity: sha512-WXChFKV7VdDk1NePDK1J31cpSvxACAVztJ7f7lJVYBTkH+iz5D0lCqPcE7a9eb7nC3xvz4yk7DM6dA9wlUQkQg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxc-minify/binding-linux-arm-musleabihf@0.96.0':
+ resolution: {integrity: sha512-7B18glYMX4Z/YoqgE3VRLs/2YhVLxlxNKSgrtsRpuR8xv58xca+hEhiFwZN1Rn+NSMZ29Z33LWD7iYWnqYFvRA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxc-minify/binding-linux-arm64-gnu@0.96.0':
+ resolution: {integrity: sha512-Yl+KcTldsEJNcaYxxonwAXZ2q3gxIzn3kXYQWgKWdaGIpNhOCWqF+KE5WLsldoh5Ro5SHtomvb8GM6cXrIBMog==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxc-minify/binding-linux-arm64-musl@0.96.0':
+ resolution: {integrity: sha512-rNqoFWOWaxwMmUY5fspd/h5HfvgUlA3sv9CUdA2MpnHFiyoJNovR7WU8tGh+Yn0qOAs0SNH0a05gIthHig14IA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxc-minify/binding-linux-riscv64-gnu@0.96.0':
+ resolution: {integrity: sha512-3paajIuzGnukHwSI3YBjYVqbd72pZd8NJxaayaNFR0AByIm8rmIT5RqFXbq8j2uhtpmNdZRXiu0em1zOmIScWA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxc-minify/binding-linux-s390x-gnu@0.96.0':
+ resolution: {integrity: sha512-9ESrpkB2XG0lQ89JlsxlZa86iQCOs+jkDZLl6O+u5wb7ynUy21bpJJ1joauCOSYIOUlSy3+LbtJLiqi7oSQt5Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@oxc-minify/binding-linux-x64-gnu@0.96.0':
+ resolution: {integrity: sha512-UMM1jkns+p+WwwmdjC5giI3SfR2BCTga18x3C0cAu6vDVf4W37uTZeTtSIGmwatTBbgiq++Te24/DE0oCdm1iQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxc-minify/binding-linux-x64-musl@0.96.0':
+ resolution: {integrity: sha512-8b1naiC7MdP7xeMi7cQ5tb9W1rZAP9Qz/jBRqp1Y5EOZ1yhSGnf1QWuZ/0pCc+XiB9vEHXEY3Aki/H+86m2eOg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxc-minify/binding-wasm32-wasi@0.96.0':
+ resolution: {integrity: sha512-bjGDjkGzo3GWU9Vg2qiFUrfoo5QxojPNV/2RHTlbIB5FWkkV4ExVjsfyqihFiAuj0NXIZqd2SAiEq9htVd3RFw==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@oxc-minify/binding-win32-arm64-msvc@0.96.0':
+ resolution: {integrity: sha512-4L4DlHUT47qMWQuTyUghpncR3NZHWtxvd0G1KgSjVgXf+cXzFdWQCWZZtCU0yrmOoVCNUf4S04IFCJyAe+Ie7A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxc-minify/binding-win32-x64-msvc@0.96.0':
+ resolution: {integrity: sha512-T2ijfqZLpV2bgGGocXV4SXTuMoouqN0asYTIm+7jVOLvT5XgDogf3ZvCmiEnSWmxl21+r5wHcs8voU2iUROXAg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@oxc-transform/binding-android-arm64@0.96.0':
+ resolution: {integrity: sha512-wOm+ZsqFvyZ7B9RefUMsj0zcXw77Z2pXA51nbSQyPXqr+g0/pDGxriZWP8Sdpz/e4AEaKPA9DvrwyOZxu7GRDQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@oxc-transform/binding-darwin-arm64@0.96.0':
+ resolution: {integrity: sha512-td1sbcvzsyuoNRiNdIRodPXRtFFwxzPpC/6/yIUtRRhKn30XQcizxupIvQQVpJWWchxkphbBDh6UN+u+2CJ8Zw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxc-transform/binding-darwin-x64@0.96.0':
+ resolution: {integrity: sha512-xgqxnqhPYH2NYkgbqtnCJfhbXvxIf/pnhF/ig5UBK8PYpCEWIP/cfLpQRQ9DcQnRfuxi7RMIF6LdmB1AiS6Fkg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxc-transform/binding-freebsd-x64@0.96.0':
+ resolution: {integrity: sha512-1i67OXdl/rvSkcTXqDlh6qGRXYseEmf0rl/R+/i88scZ/o3A+FzlX56sThuaPzSSv9eVgesnoYUjIBJELFc1oA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@oxc-transform/binding-linux-arm-gnueabihf@0.96.0':
+ resolution: {integrity: sha512-9MJBs0SWODsqyzO3eAnacXgJ/sZu1xqinjEwBzkcZ3tQI8nKhMADOzu2NzbVWDWujeoC8DESXaO08tujvUru+Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxc-transform/binding-linux-arm-musleabihf@0.96.0':
+ resolution: {integrity: sha512-BQom57I2ScccixljNYh2Wy+5oVZtF1LXiiUPxSLtDHbsanpEvV/+kzCagQpTjk1BVzSQzOxfEUWjvL7mY53pRQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxc-transform/binding-linux-arm64-gnu@0.96.0':
+ resolution: {integrity: sha512-kaqvUzNu8LL4aBSXqcqGVLFG13GmJEplRI2+yqzkgAItxoP/LfFMdEIErlTWLGyBwd0OLiNMHrOvkcCQRWadVg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxc-transform/binding-linux-arm64-musl@0.96.0':
+ resolution: {integrity: sha512-EiG/L3wEkPgTm4p906ufptyblBgtiQWTubGg/JEw82f8uLRroayr5zhbUqx40EgH037a3SfJthIyLZi7XPRFJw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxc-transform/binding-linux-riscv64-gnu@0.96.0':
+ resolution: {integrity: sha512-r01CY6OxKGtVeYnvH4mGmtkQMlLkXdPWWNXwo5o7fE2s/fgZPMpqh8bAuXEhuMXipZRJrjxTk1+ZQ4KCHpMn3Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxc-transform/binding-linux-s390x-gnu@0.96.0':
+ resolution: {integrity: sha512-4djg2vYLGbVeS8YiA2K4RPPpZE4fxTGCX5g/bOMbCYyirDbmBAIop4eOAj8vOA9i1CcWbDtmp+PVJ1dSw7f3IQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@oxc-transform/binding-linux-x64-gnu@0.96.0':
+ resolution: {integrity: sha512-f6pcWVz57Y8jXa2OS7cz3aRNuks34Q3j61+3nQ4xTE8H1KbalcEvHNmM92OEddaJ8QLs9YcE0kUC6eDTbY34+A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
- '@open-draft/deferred-promise@2.2.0':
- resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
+ '@oxc-transform/binding-linux-x64-musl@0.96.0':
+ resolution: {integrity: sha512-NSiRtFvR7Pbhv3mWyPMkTK38czIjcnK0+K5STo3CuzZRVbX1TM17zGdHzKBUHZu7v6IQ6/XsQ3ELa1BlEHPGWQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
- '@open-draft/logger@0.3.0':
- resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==}
+ '@oxc-transform/binding-wasm32-wasi@0.96.0':
+ resolution: {integrity: sha512-A91ARLiuZHGN4hBds9s7bW3czUuLuHLsV+cz44iF9j8e1zX9m2hNGXf/acQRbg/zcFUXmjz5nmk8EkZyob876w==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
- '@open-draft/until@2.1.0':
- resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
+ '@oxc-transform/binding-win32-arm64-msvc@0.96.0':
+ resolution: {integrity: sha512-IedJf40djKgDObomhYjdRAlmSYUEdfqX3A3M9KfUltl9AghTBBLkTzUMA7O09oo71vYf5TEhbFM7+Vn5vqw7AQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxc-transform/binding-win32-x64-msvc@0.96.0':
+ resolution: {integrity: sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
'@panva/hkdf@1.2.1':
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
@@ -17747,6 +18136,15 @@ packages:
crossws:
optional: true
+ h3@2.0.1-rc.5:
+ resolution: {integrity: sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg==}
+ engines: {node: '>=20.11.1'}
+ peerDependencies:
+ crossws: ^0.4.1
+ peerDependenciesMeta:
+ crossws:
+ optional: true
+
handle-thing@2.0.1:
resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==}
@@ -19077,6 +19475,9 @@ packages:
nf3@0.1.1:
resolution: {integrity: sha512-iJfiw84oKzsOOM+zqr0fycjgLx2wLw1RrGLHd9qxUPwMtut6GjBalDS9TRPZJXMAZjUv9ghLgtDrhzqcFJ8eTQ==}
+ nf3@0.1.12:
+ resolution: {integrity: sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ==}
+
nitro@3.0.1-alpha.0:
resolution: {integrity: sha512-lR3RplfXBOZXNlFQf9AJkqFVFhg5/CNbpBijM0dSYhGymb+FthJSdL6crmXVg518h2NVOd40rehhGZaf9ijW9w==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -19093,6 +19494,25 @@ packages:
xml2js:
optional: true
+ nitro@3.0.1-alpha.1:
+ resolution: {integrity: sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ rolldown: '*'
+ rollup: ^4
+ vite: ^7.1.7
+ xml2js: ^0.6.2
+ peerDependenciesMeta:
+ rolldown:
+ optional: true
+ rollup:
+ optional: true
+ vite:
+ optional: true
+ xml2js:
+ optional: true
+
nitropack@2.12.6:
resolution: {integrity: sha512-DEq31s0SP4/Z5DIoVBRo9DbWFPWwIoYD4cQMEz7eE+iJMiAP+1k9A3B9kcc6Ihc0jDJmfUcHYyh6h2XlynCx6g==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -19232,6 +19652,9 @@ packages:
ofetch@1.4.1:
resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==}
+ ofetch@2.0.0-alpha.3:
+ resolution: {integrity: sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA==}
+
ohash@2.0.11:
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
@@ -19279,6 +19702,14 @@ packages:
outvariant@1.4.3:
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+ oxc-minify@0.96.0:
+ resolution: {integrity: sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ oxc-transform@0.96.0:
+ resolution: {integrity: sha512-dQPNIF+gHpSkmC0+Vg9IktNyhcn28Y8R3eTLyzn52UNymkasLicl3sFAtz7oEVuFmCpgGjaUTKkwk+jW2cHpDQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
p-event@6.0.1:
resolution: {integrity: sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==}
engines: {node: '>=16.17'}
@@ -20051,6 +20482,9 @@ packages:
rou3@0.5.1:
resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==}
+ rou3@0.7.10:
+ resolution: {integrity: sha512-aoFj6f7MJZ5muJ+Of79nrhs9N3oLGqi2VEMe94Zbkjb6Wupha46EuoYgpWSOZlXww3bbd8ojgXTAA2mzimX5Ww==}
+
rou3@0.7.8:
resolution: {integrity: sha512-21X/el5fdOaEsqwl3an/d9kpZ8hshVIyrwFCpsoleJ4ccAGRbN+PVoxyXzWXkHDxfMkVnLe4yzx+imz2qoem2Q==}
@@ -20361,6 +20795,11 @@ packages:
engines: {node: '>=20.16.0'}
hasBin: true
+ srvx@0.9.6:
+ resolution: {integrity: sha512-5L4rT6qQqqb+xcoDoklUgCNdmzqJ6vbcDRwPVGRXewF55IJH0pqh0lQlrJ266ZWTKJ4mfeioqHQJeAYesS+RrQ==}
+ engines: {node: '>=20.16.0'}
+ hasBin: true
+
stable-hash-x@0.2.0:
resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==}
engines: {node: '>=12.0.0'}
@@ -21094,6 +21533,80 @@ packages:
uploadthing:
optional: true
+ unstorage@2.0.0-alpha.4:
+ resolution: {integrity: sha512-ywXZMZRfrvmO1giJeMTCw6VUn0ALYxVl8pFqJPStiyQUvgJImejtAHrKvXPj4QGJAoS/iLGcVGF6ljN/lkh1bw==}
+ peerDependencies:
+ '@azure/app-configuration': ^1.8.0
+ '@azure/cosmos': ^4.2.0
+ '@azure/data-tables': ^13.3.0
+ '@azure/identity': ^4.6.0
+ '@azure/keyvault-secrets': ^4.9.0
+ '@azure/storage-blob': ^12.26.0
+ '@capacitor/preferences': ^6.0.3 || ^7.0.0
+ '@deno/kv': '>=0.9.0'
+ '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0
+ '@planetscale/database': ^1.19.0
+ '@upstash/redis': ^1.34.3
+ '@vercel/blob': '>=0.27.1'
+ '@vercel/functions': ^2.2.12 || ^3.0.0
+ '@vercel/kv': ^1.0.1
+ aws4fetch: ^1.0.20
+ chokidar: ^4.0.3
+ db0: '>=0.2.1'
+ idb-keyval: ^6.2.1
+ ioredis: ^5.4.2
+ lru-cache: ^11.2.2
+ mongodb: ^6.20.0
+ ofetch: '*'
+ uploadthing: ^7.4.4
+ peerDependenciesMeta:
+ '@azure/app-configuration':
+ optional: true
+ '@azure/cosmos':
+ optional: true
+ '@azure/data-tables':
+ optional: true
+ '@azure/identity':
+ optional: true
+ '@azure/keyvault-secrets':
+ optional: true
+ '@azure/storage-blob':
+ optional: true
+ '@capacitor/preferences':
+ optional: true
+ '@deno/kv':
+ optional: true
+ '@netlify/blobs':
+ optional: true
+ '@planetscale/database':
+ optional: true
+ '@upstash/redis':
+ optional: true
+ '@vercel/blob':
+ optional: true
+ '@vercel/functions':
+ optional: true
+ '@vercel/kv':
+ optional: true
+ aws4fetch:
+ optional: true
+ chokidar:
+ optional: true
+ db0:
+ optional: true
+ idb-keyval:
+ optional: true
+ ioredis:
+ optional: true
+ lru-cache:
+ optional: true
+ mongodb:
+ optional: true
+ ofetch:
+ optional: true
+ uploadthing:
+ optional: true
+
untun@0.1.3:
resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==}
hasBin: true
@@ -24111,6 +24624,13 @@ snapshots:
'@emnapi/runtime': 1.5.0
'@tybys/wasm-util': 0.9.0
+ '@napi-rs/wasm-runtime@1.0.7':
+ dependencies:
+ '@emnapi/core': 1.5.0
+ '@emnapi/runtime': 1.5.0
+ '@tybys/wasm-util': 0.10.1
+ optional: true
+
'@neon-rs/load@0.0.4': {}
'@netlify/api@14.0.7':
@@ -24518,6 +25038,100 @@ snapshots:
'@open-draft/until@2.1.0': {}
+ '@oxc-minify/binding-android-arm64@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-darwin-arm64@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-darwin-x64@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-freebsd-x64@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-linux-arm-gnueabihf@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-linux-arm-musleabihf@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-linux-arm64-gnu@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-linux-arm64-musl@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-linux-riscv64-gnu@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-linux-s390x-gnu@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-linux-x64-gnu@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-linux-x64-musl@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-wasm32-wasi@0.96.0':
+ dependencies:
+ '@napi-rs/wasm-runtime': 1.0.7
+ optional: true
+
+ '@oxc-minify/binding-win32-arm64-msvc@0.96.0':
+ optional: true
+
+ '@oxc-minify/binding-win32-x64-msvc@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-android-arm64@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-darwin-arm64@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-darwin-x64@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-freebsd-x64@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-linux-arm-gnueabihf@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-linux-arm-musleabihf@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-linux-arm64-gnu@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-linux-arm64-musl@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-linux-riscv64-gnu@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-linux-s390x-gnu@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-linux-x64-gnu@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-linux-x64-musl@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-wasm32-wasi@0.96.0':
+ dependencies:
+ '@napi-rs/wasm-runtime': 1.0.7
+ optional: true
+
+ '@oxc-transform/binding-win32-arm64-msvc@0.96.0':
+ optional: true
+
+ '@oxc-transform/binding-win32-x64-msvc@0.96.0':
+ optional: true
+
'@panva/hkdf@1.2.1': {}
'@parcel/watcher-android-arm64@2.5.1':
@@ -28759,6 +29373,10 @@ snapshots:
optionalDependencies:
srvx: 0.8.15
+ crossws@0.4.1(srvx@0.9.6):
+ optionalDependencies:
+ srvx: 0.9.6
+
css-loader@7.1.2(@rspack/core@1.2.2(@swc/helpers@0.5.15))(webpack@5.97.1):
dependencies:
icss-utils: 5.1.0(postcss@8.5.6)
@@ -30128,14 +30746,14 @@ snapshots:
ufo: 1.6.1
uncrypto: 0.1.3
- h3@2.0.0-beta.5(crossws@0.4.1(srvx@0.8.15)):
+ h3@2.0.0-beta.5(crossws@0.4.1(srvx@0.9.6)):
dependencies:
cookie-es: 2.0.0
fetchdts: 0.1.7
rou3: 0.7.8
srvx: 0.8.15
optionalDependencies:
- crossws: 0.4.1(srvx@0.8.15)
+ crossws: 0.4.1(srvx@0.9.6)
h3@2.0.1-rc.2(crossws@0.4.1(srvx@0.8.15)):
dependencies:
@@ -30146,6 +30764,13 @@ snapshots:
optionalDependencies:
crossws: 0.4.1(srvx@0.8.15)
+ h3@2.0.1-rc.5(crossws@0.4.1(srvx@0.9.6)):
+ dependencies:
+ rou3: 0.7.10
+ srvx: 0.9.6
+ optionalDependencies:
+ crossws: 0.4.1(srvx@0.9.6)
+
handle-thing@2.0.1: {}
has-flag@4.0.0: {}
@@ -31449,6 +32074,8 @@ snapshots:
nf3@0.1.1: {}
+ nf3@0.1.12: {}
+
nitro@3.0.1-alpha.0(@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)(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)):
dependencies:
consola: 3.4.2
@@ -31499,6 +32126,54 @@ snapshots:
- sqlite3
- uploadthing
+ nitro@3.0.1-alpha.1(@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)):
+ dependencies:
+ consola: 3.4.2
+ crossws: 0.4.1(srvx@0.9.6)
+ db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3)
+ h3: 2.0.1-rc.5(crossws@0.4.1(srvx@0.9.6))
+ jiti: 2.6.1
+ nf3: 0.1.12
+ ofetch: 2.0.0-alpha.3
+ ohash: 2.0.11
+ oxc-minify: 0.96.0
+ oxc-transform: 0.96.0
+ srvx: 0.9.6
+ undici: 7.16.0
+ unenv: 2.0.0-rc.24
+ unstorage: 2.0.0-alpha.4(@netlify/blobs@10.1.0)(chokidar@4.0.3)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3)
+ optionalDependencies:
+ 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)
+ transitivePeerDependencies:
+ - '@azure/app-configuration'
+ - '@azure/cosmos'
+ - '@azure/data-tables'
+ - '@azure/identity'
+ - '@azure/keyvault-secrets'
+ - '@azure/storage-blob'
+ - '@capacitor/preferences'
+ - '@deno/kv'
+ - '@electric-sql/pglite'
+ - '@libsql/client'
+ - '@netlify/blobs'
+ - '@planetscale/database'
+ - '@upstash/redis'
+ - '@vercel/blob'
+ - '@vercel/functions'
+ - '@vercel/kv'
+ - aws4fetch
+ - better-sqlite3
+ - chokidar
+ - drizzle-orm
+ - idb-keyval
+ - ioredis
+ - lru-cache
+ - mongodb
+ - mysql2
+ - sqlite3
+ - uploadthing
+
nitropack@2.12.6(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(encoding@0.1.13)(mysql2@3.15.3):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.0
@@ -31753,6 +32428,8 @@ snapshots:
node-fetch-native: 1.6.7
ufo: 1.6.1
+ ofetch@2.0.0-alpha.3: {}
+
ohash@2.0.11: {}
omit.js@2.0.2: {}
@@ -31814,6 +32491,42 @@ snapshots:
outvariant@1.4.3: {}
+ oxc-minify@0.96.0:
+ optionalDependencies:
+ '@oxc-minify/binding-android-arm64': 0.96.0
+ '@oxc-minify/binding-darwin-arm64': 0.96.0
+ '@oxc-minify/binding-darwin-x64': 0.96.0
+ '@oxc-minify/binding-freebsd-x64': 0.96.0
+ '@oxc-minify/binding-linux-arm-gnueabihf': 0.96.0
+ '@oxc-minify/binding-linux-arm-musleabihf': 0.96.0
+ '@oxc-minify/binding-linux-arm64-gnu': 0.96.0
+ '@oxc-minify/binding-linux-arm64-musl': 0.96.0
+ '@oxc-minify/binding-linux-riscv64-gnu': 0.96.0
+ '@oxc-minify/binding-linux-s390x-gnu': 0.96.0
+ '@oxc-minify/binding-linux-x64-gnu': 0.96.0
+ '@oxc-minify/binding-linux-x64-musl': 0.96.0
+ '@oxc-minify/binding-wasm32-wasi': 0.96.0
+ '@oxc-minify/binding-win32-arm64-msvc': 0.96.0
+ '@oxc-minify/binding-win32-x64-msvc': 0.96.0
+
+ oxc-transform@0.96.0:
+ optionalDependencies:
+ '@oxc-transform/binding-android-arm64': 0.96.0
+ '@oxc-transform/binding-darwin-arm64': 0.96.0
+ '@oxc-transform/binding-darwin-x64': 0.96.0
+ '@oxc-transform/binding-freebsd-x64': 0.96.0
+ '@oxc-transform/binding-linux-arm-gnueabihf': 0.96.0
+ '@oxc-transform/binding-linux-arm-musleabihf': 0.96.0
+ '@oxc-transform/binding-linux-arm64-gnu': 0.96.0
+ '@oxc-transform/binding-linux-arm64-musl': 0.96.0
+ '@oxc-transform/binding-linux-riscv64-gnu': 0.96.0
+ '@oxc-transform/binding-linux-s390x-gnu': 0.96.0
+ '@oxc-transform/binding-linux-x64-gnu': 0.96.0
+ '@oxc-transform/binding-linux-x64-musl': 0.96.0
+ '@oxc-transform/binding-wasm32-wasi': 0.96.0
+ '@oxc-transform/binding-win32-arm64-msvc': 0.96.0
+ '@oxc-transform/binding-win32-x64-msvc': 0.96.0
+
p-event@6.0.1:
dependencies:
p-timeout: 6.1.4
@@ -32663,6 +33376,8 @@ snapshots:
rou3@0.5.1: {}
+ rou3@0.7.10: {}
+
rou3@0.7.8: {}
router@2.2.0:
@@ -33066,6 +33781,8 @@ snapshots:
dependencies:
cookie-es: 2.0.0
+ srvx@0.9.6: {}
+
stable-hash-x@0.2.0: {}
stack-trace@0.0.10: {}
@@ -33705,6 +34422,15 @@ snapshots:
lru-cache: 11.2.2
ofetch: 1.4.1
+ unstorage@2.0.0-alpha.4(@netlify/blobs@10.1.0)(chokidar@4.0.3)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3):
+ optionalDependencies:
+ '@netlify/blobs': 10.1.0
+ chokidar: 4.0.3
+ db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3)
+ ioredis: 5.8.0
+ lru-cache: 11.2.2
+ ofetch: 2.0.0-alpha.3
+
untun@0.1.3:
dependencies:
citty: 0.1.6