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