diff --git a/e2e/react-start/basic-nitro-spa/.gitignore b/e2e/react-start/basic-nitro-spa/.gitignore new file mode 100644 index 00000000000..114d10aa0e4 --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/.gitignore @@ -0,0 +1,12 @@ +node_modules +.DS_Store +.cache +.env +dist +.output +.nitro + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/react-start/basic-nitro-spa/package.json b/e2e/react-start/basic-nitro-spa/package.json new file mode 100644 index 00000000000..6edf2933e0e --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/package.json @@ -0,0 +1,42 @@ +{ + "name": "tanstack-react-start-e2e-basic-nitro-spa", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev:v2": "vite dev -c vite.config.v2.ts --port 3000", + "dev:v3": "vite dev -c vite.config.v3.ts --port 3000", + "dev:e2e:v2": "vite dev -c vite.config.v2.ts", + "dev:e2e:v3": "vite dev -c vite.config.v3.ts", + "build:v2": "vite build -c vite.config.v2.ts && tsc --noEmit", + "build:v3": "vite build -c vite.config.v3.ts && tsc --noEmit", + "preview:v2": "vite preview -c vite.config.v2.ts", + "preview:v3": "vite preview -c vite.config.v3.ts", + "test:e2e:shared": "rm -rf port*.txt; playwright test --project=chromium", + "test:e2e:v2": "rm -rf .output dist .nitro && NITRO_VARIANT=v2 pnpm run test:e2e:shared", + "test:e2e:v3": "rm -rf .output dist .nitro && NITRO_VARIANT=v3 pnpm run test:e2e:shared", + "test:e2e": "pnpm run test:e2e:v2 && pnpm run test:e2e:v3" + }, + "dependencies": { + "@tanstack/react-router": "workspace:^", + "@tanstack/react-router-devtools": "workspace:^", + "@tanstack/react-start": "workspace:^", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tailwindcss/postcss": "^4.1.15", + "@tanstack/router-e2e-utils": "workspace:^", + "@tanstack/nitro-v2-vite-plugin": "workspace:^", + "@types/node": "^22.10.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "nitro": "npm:nitro-nightly@latest", + "postcss": "^8.5.1", + "tailwindcss": "^4.1.15", + "typescript": "^5.7.2", + "vite": "^7.1.7", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/react-start/basic-nitro-spa/playwright.config.ts b/e2e/react-start/basic-nitro-spa/playwright.config.ts new file mode 100644 index 00000000000..b26f5493b47 --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/playwright.config.ts @@ -0,0 +1,44 @@ +import { defineConfig, devices } from '@playwright/test' +import { getTestServerPort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = await getTestServerPort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +const nitroVariant = process.env.NITRO_VARIANT +if (nitroVariant !== 'v2' && nitroVariant !== 'v3') { + throw new Error('Set NITRO_VARIANT to "v2" or "v3" for Nitro e2e tests.') +} +const buildScript = nitroVariant === 'v2' ? 'build:v2' : 'build:v3' +const buildCommand = `pnpm run ${buildScript}` + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + // Note: We run node directly instead of vite preview because Nitro's + // configurePreviewServer spawns on a random port. The prerendering during + // build uses vite.preview() correctly. + command: `${buildCommand} && PORT=${PORT} node .output/server/index.mjs`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/react-start/basic-nitro-spa/postcss.config.mjs b/e2e/react-start/basic-nitro-spa/postcss.config.mjs new file mode 100644 index 00000000000..a7f73a2d1d7 --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/e2e/react-start/basic-nitro-spa/public/android-chrome-192x192.png b/e2e/react-start/basic-nitro-spa/public/android-chrome-192x192.png new file mode 100644 index 00000000000..09c8324f8c6 Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/android-chrome-192x192.png differ diff --git a/e2e/react-start/basic-nitro-spa/public/android-chrome-512x512.png b/e2e/react-start/basic-nitro-spa/public/android-chrome-512x512.png new file mode 100644 index 00000000000..11d626ea3d0 Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/android-chrome-512x512.png differ diff --git a/e2e/react-start/basic-nitro-spa/public/apple-touch-icon.png b/e2e/react-start/basic-nitro-spa/public/apple-touch-icon.png new file mode 100644 index 00000000000..5a9423cc02c Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/apple-touch-icon.png differ diff --git a/e2e/react-start/basic-nitro-spa/public/favicon-16x16.png b/e2e/react-start/basic-nitro-spa/public/favicon-16x16.png new file mode 100644 index 00000000000..e3389b00443 Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/favicon-16x16.png differ diff --git a/e2e/react-start/basic-nitro-spa/public/favicon-32x32.png b/e2e/react-start/basic-nitro-spa/public/favicon-32x32.png new file mode 100644 index 00000000000..900c77d444c Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/favicon-32x32.png differ diff --git a/e2e/react-start/basic-nitro-spa/public/favicon.ico b/e2e/react-start/basic-nitro-spa/public/favicon.ico new file mode 100644 index 00000000000..1a1751676f7 Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/favicon.ico differ diff --git a/e2e/react-start/basic-nitro-spa/public/favicon.png b/e2e/react-start/basic-nitro-spa/public/favicon.png new file mode 100644 index 00000000000..1e77bc06091 Binary files /dev/null and b/e2e/react-start/basic-nitro-spa/public/favicon.png differ diff --git a/e2e/react-start/basic-nitro-spa/public/site.webmanifest b/e2e/react-start/basic-nitro-spa/public/site.webmanifest new file mode 100644 index 00000000000..fa99de77db6 --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/e2e/react-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx b/e2e/react-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx new file mode 100644 index 00000000000..ef2daa1ea1d --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/react-router' +import type { ErrorComponentProps } from '@tanstack/react-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/e2e/react-start/basic-nitro-spa/src/components/NotFound.tsx b/e2e/react-start/basic-nitro-spa/src/components/NotFound.tsx new file mode 100644 index 00000000000..4e84e3f8e00 --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/src/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from '@tanstack/react-router' + +export function NotFound({ children }: { children?: any }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts b/e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts new file mode 100644 index 00000000000..4219501f5ef --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as StaticRouteImport } from './routes/static' +import { Route as IndexRouteImport } from './routes/index' + +const StaticRoute = StaticRouteImport.update({ + id: '/static', + path: '/static', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/static': typeof StaticRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/static': typeof StaticRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/static': typeof StaticRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/static' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/static' + id: '__root__' | '/' | '/static' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StaticRoute: typeof StaticRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/static': { + id: '/static' + path: '/static' + fullPath: '/static' + preLoaderRoute: typeof StaticRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StaticRoute: StaticRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/react-start/basic-nitro-spa/src/router.tsx b/e2e/react-start/basic-nitro-spa/src/router.tsx new file mode 100644 index 00000000000..1a1d8822d20 --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function getRouter() { + const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + }) + + return router +} diff --git a/e2e/react-start/basic-nitro-spa/src/routes/__root.tsx b/e2e/react-start/basic-nitro-spa/src/routes/__root.tsx new file mode 100644 index 00000000000..5b62b589077 --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/src/routes/__root.tsx @@ -0,0 +1,73 @@ +/// +import { + HeadContent, + Link, + Scripts, + createRootRoute, +} from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' +import * as React from 'react' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' +import { seo } from '~/utils/seo' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ...seo({ + title: 'TanStack Start + Nitro E2E Test', + description: 'Testing nitro integration with TanStack Start', + }), + ], + links: [ + { rel: 'stylesheet', href: appCss }, + { rel: 'icon', href: '/favicon.ico' }, + ], + }), + errorComponent: DefaultCatchBoundary, + notFoundComponent: () => , + shellComponent: RootDocument, +}) + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + +
+ + Home + + + Static + +
+
+ {children} + + + + + ) +} diff --git a/e2e/react-start/basic-nitro-spa/src/routes/index.tsx b/e2e/react-start/basic-nitro-spa/src/routes/index.tsx new file mode 100644 index 00000000000..311e2cf3739 --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/src/routes/index.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' + +export const Route = createFileRoute('/')({ + loader: () => getData(), + component: Home, +}) + +const getData = createServerFn().handler(() => { + return { + message: 'Hello from Nitro server!', + timestamp: new Date().toISOString(), + } +}) + +function Home() { + const data = Route.useLoaderData() + + return ( +
+

Welcome Home!

+

{data.message}

+

Loaded at: {data.timestamp}

+
+ ) +} diff --git a/e2e/react-start/basic-nitro-spa/src/routes/static.tsx b/e2e/react-start/basic-nitro-spa/src/routes/static.tsx new file mode 100644 index 00000000000..f018bf39649 --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/src/routes/static.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' + +export const Route = createFileRoute('/static')({ + loader: () => getStaticData(), + component: StaticPage, +}) + +const getStaticData = createServerFn().handler(() => { + return { + content: 'This page was prerendered at build time', + buildTime: new Date().toISOString(), + } +}) + +function StaticPage() { + const data = Route.useLoaderData() + + return ( +
+

Static Page

+

{data.content}

+

Build time: {data.buildTime}

+
+ ) +} diff --git a/e2e/react-start/basic-nitro-spa/src/styles/app.css b/e2e/react-start/basic-nitro-spa/src/styles/app.css new file mode 100644 index 00000000000..c36c737cd46 --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/src/styles/app.css @@ -0,0 +1,30 @@ +@import 'tailwindcss'; + +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/react-start/basic-nitro-spa/src/utils/seo.ts b/e2e/react-start/basic-nitro-spa/src/utils/seo.ts new file mode 100644 index 00000000000..d18ad84b74e --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/src/utils/seo.ts @@ -0,0 +1,33 @@ +export const seo = ({ + title, + description, + keywords, + image, +}: { + title: string + description?: string + image?: string + keywords?: string +}) => { + const tags = [ + { title }, + { name: 'description', content: description }, + { name: 'keywords', content: keywords }, + { name: 'twitter:title', content: title }, + { name: 'twitter:description', content: description }, + { name: 'twitter:creator', content: '@tannerlinsley' }, + { name: 'twitter:site', content: '@tannerlinsley' }, + { name: 'og:type', content: 'website' }, + { name: 'og:title', content: title }, + { name: 'og:description', content: description }, + ...(image + ? [ + { name: 'twitter:image', content: image }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'og:image', content: image }, + ] + : []), + ] + + return tags +} diff --git a/e2e/react-start/basic-nitro-spa/tests/app.spec.ts b/e2e/react-start/basic-nitro-spa/tests/app.spec.ts new file mode 100644 index 00000000000..b0f96e373af --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/tests/app.spec.ts @@ -0,0 +1,30 @@ +import { existsSync } from 'node:fs' +import { join } from 'node:path' +import { expect, test } from '@playwright/test' + +test('SPA shell is prerendered during build with nitro', async ({ page }) => { + const outputDir = join(process.cwd(), '.output', 'public') + expect(existsSync(join(outputDir, 'index.html'))).toBe(true) + + await page.goto('/') + await expect(page.getByTestId('home-heading')).toBeVisible() +}) + +test('server functions work with nitro', async ({ page }) => { + await page.goto('/') + await expect(page.getByTestId('home-heading')).toHaveText('Welcome Home!') + await expect(page.getByTestId('message')).toHaveText( + 'Hello from Nitro server!', + ) +}) + +test('client-side navigation works in SPA mode', async ({ page }) => { + await page.goto('/') + await expect(page.getByTestId('home-heading')).toBeVisible() + + await page.click('a[href="/static"]') + await expect(page.getByTestId('static-heading')).toBeVisible() + + await page.click('a[href="/"]') + await expect(page.getByTestId('home-heading')).toBeVisible() +}) diff --git a/e2e/react-start/basic-nitro-spa/tsconfig.json b/e2e/react-start/basic-nitro-spa/tsconfig.json new file mode 100644 index 00000000000..3a9fb7cd716 --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true + } +} diff --git a/e2e/react-start/basic-nitro-spa/vite.config.v2.ts b/e2e/react-start/basic-nitro-spa/vite.config.v2.ts new file mode 100644 index 00000000000..4b6e6dec6ae --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/vite.config.v2.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin' + +export default defineConfig({ + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart({ + spa: { + enabled: true, + prerender: { + outputPath: 'index.html', + }, + }, + }), + nitroV2Plugin(), + ], +}) diff --git a/e2e/react-start/basic-nitro-spa/vite.config.v3.ts b/e2e/react-start/basic-nitro-spa/vite.config.v3.ts new file mode 100644 index 00000000000..37f603e3ba8 --- /dev/null +++ b/e2e/react-start/basic-nitro-spa/vite.config.v3.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import { nitro } from 'nitro/vite' + +export default defineConfig({ + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart({ + spa: { + enabled: true, + prerender: { + outputPath: 'index.html', + }, + }, + }), + nitro(), + ], +}) diff --git a/e2e/react-start/basic-nitro/.gitignore b/e2e/react-start/basic-nitro/.gitignore new file mode 100644 index 00000000000..cce09e5f653 --- /dev/null +++ b/e2e/react-start/basic-nitro/.gitignore @@ -0,0 +1,11 @@ +node_modules +.DS_Store +.cache +.env +dist +.output + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/react-start/basic-nitro/.prettierignore b/e2e/react-start/basic-nitro/.prettierignore new file mode 100644 index 00000000000..a16e01379d7 --- /dev/null +++ b/e2e/react-start/basic-nitro/.prettierignore @@ -0,0 +1 @@ +routeTree.gen.ts diff --git a/e2e/react-start/basic-nitro/package.json b/e2e/react-start/basic-nitro/package.json new file mode 100644 index 00000000000..109d3e2d33f --- /dev/null +++ b/e2e/react-start/basic-nitro/package.json @@ -0,0 +1,42 @@ +{ + "name": "tanstack-react-start-e2e-basic-nitro", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev:v2": "vite dev -c vite.config.v2.ts --port 3000", + "dev:v3": "vite dev -c vite.config.v3.ts --port 3000", + "dev:e2e:v2": "vite dev -c vite.config.v2.ts", + "dev:e2e:v3": "vite dev -c vite.config.v3.ts", + "build:v2": "vite build -c vite.config.v2.ts && tsc --noEmit", + "build:v3": "vite build -c vite.config.v3.ts && tsc --noEmit", + "preview:v2": "vite preview -c vite.config.v2.ts", + "preview:v3": "vite preview -c vite.config.v3.ts", + "test:e2e:shared": "rm -rf port*.txt; playwright test --project=chromium", + "test:e2e:v2": "rm -rf .output dist .nitro && NITRO_VARIANT=v2 pnpm run test:e2e:shared", + "test:e2e:v3": "rm -rf .output dist .nitro && NITRO_VARIANT=v3 pnpm run test:e2e:shared", + "test:e2e": "pnpm run test:e2e:v2 && pnpm run test:e2e:v3" + }, + "dependencies": { + "@tanstack/react-router": "workspace:^", + "@tanstack/react-router-devtools": "workspace:^", + "@tanstack/react-start": "workspace:^", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tailwindcss/postcss": "^4.1.15", + "@tanstack/router-e2e-utils": "workspace:^", + "@tanstack/nitro-v2-vite-plugin": "workspace:^", + "@types/node": "^22.10.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "nitro": "npm:nitro-nightly@latest", + "postcss": "^8.5.1", + "tailwindcss": "^4.1.15", + "typescript": "^5.7.2", + "vite": "^7.1.7", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/react-start/basic-nitro/playwright.config.ts b/e2e/react-start/basic-nitro/playwright.config.ts new file mode 100644 index 00000000000..b26f5493b47 --- /dev/null +++ b/e2e/react-start/basic-nitro/playwright.config.ts @@ -0,0 +1,44 @@ +import { defineConfig, devices } from '@playwright/test' +import { getTestServerPort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = await getTestServerPort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +const nitroVariant = process.env.NITRO_VARIANT +if (nitroVariant !== 'v2' && nitroVariant !== 'v3') { + throw new Error('Set NITRO_VARIANT to "v2" or "v3" for Nitro e2e tests.') +} +const buildScript = nitroVariant === 'v2' ? 'build:v2' : 'build:v3' +const buildCommand = `pnpm run ${buildScript}` + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + // Note: We run node directly instead of vite preview because Nitro's + // configurePreviewServer spawns on a random port. The prerendering during + // build uses vite.preview() correctly. + command: `${buildCommand} && PORT=${PORT} node .output/server/index.mjs`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/react-start/basic-nitro/postcss.config.mjs b/e2e/react-start/basic-nitro/postcss.config.mjs new file mode 100644 index 00000000000..a7f73a2d1d7 --- /dev/null +++ b/e2e/react-start/basic-nitro/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/e2e/react-start/basic-nitro/public/android-chrome-192x192.png b/e2e/react-start/basic-nitro/public/android-chrome-192x192.png new file mode 100644 index 00000000000..09c8324f8c6 Binary files /dev/null and b/e2e/react-start/basic-nitro/public/android-chrome-192x192.png differ diff --git a/e2e/react-start/basic-nitro/public/android-chrome-512x512.png b/e2e/react-start/basic-nitro/public/android-chrome-512x512.png new file mode 100644 index 00000000000..11d626ea3d0 Binary files /dev/null and b/e2e/react-start/basic-nitro/public/android-chrome-512x512.png differ diff --git a/e2e/react-start/basic-nitro/public/apple-touch-icon.png b/e2e/react-start/basic-nitro/public/apple-touch-icon.png new file mode 100644 index 00000000000..5a9423cc02c Binary files /dev/null and b/e2e/react-start/basic-nitro/public/apple-touch-icon.png differ diff --git a/e2e/react-start/basic-nitro/public/favicon-16x16.png b/e2e/react-start/basic-nitro/public/favicon-16x16.png new file mode 100644 index 00000000000..e3389b00443 Binary files /dev/null and b/e2e/react-start/basic-nitro/public/favicon-16x16.png differ diff --git a/e2e/react-start/basic-nitro/public/favicon-32x32.png b/e2e/react-start/basic-nitro/public/favicon-32x32.png new file mode 100644 index 00000000000..900c77d444c Binary files /dev/null and b/e2e/react-start/basic-nitro/public/favicon-32x32.png differ diff --git a/e2e/react-start/basic-nitro/public/favicon.ico b/e2e/react-start/basic-nitro/public/favicon.ico new file mode 100644 index 00000000000..1a1751676f7 Binary files /dev/null and b/e2e/react-start/basic-nitro/public/favicon.ico differ diff --git a/e2e/react-start/basic-nitro/public/favicon.png b/e2e/react-start/basic-nitro/public/favicon.png new file mode 100644 index 00000000000..1e77bc06091 Binary files /dev/null and b/e2e/react-start/basic-nitro/public/favicon.png differ diff --git a/e2e/react-start/basic-nitro/public/site.webmanifest b/e2e/react-start/basic-nitro/public/site.webmanifest new file mode 100644 index 00000000000..fa99de77db6 --- /dev/null +++ b/e2e/react-start/basic-nitro/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/e2e/react-start/basic-nitro/src/components/DefaultCatchBoundary.tsx b/e2e/react-start/basic-nitro/src/components/DefaultCatchBoundary.tsx new file mode 100644 index 00000000000..ef2daa1ea1d --- /dev/null +++ b/e2e/react-start/basic-nitro/src/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/react-router' +import type { ErrorComponentProps } from '@tanstack/react-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/e2e/react-start/basic-nitro/src/components/NotFound.tsx b/e2e/react-start/basic-nitro/src/components/NotFound.tsx new file mode 100644 index 00000000000..4e84e3f8e00 --- /dev/null +++ b/e2e/react-start/basic-nitro/src/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from '@tanstack/react-router' + +export function NotFound({ children }: { children?: any }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/e2e/react-start/basic-nitro/src/routeTree.gen.ts b/e2e/react-start/basic-nitro/src/routeTree.gen.ts new file mode 100644 index 00000000000..4219501f5ef --- /dev/null +++ b/e2e/react-start/basic-nitro/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as StaticRouteImport } from './routes/static' +import { Route as IndexRouteImport } from './routes/index' + +const StaticRoute = StaticRouteImport.update({ + id: '/static', + path: '/static', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/static': typeof StaticRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/static': typeof StaticRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/static': typeof StaticRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/static' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/static' + id: '__root__' | '/' | '/static' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StaticRoute: typeof StaticRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/static': { + id: '/static' + path: '/static' + fullPath: '/static' + preLoaderRoute: typeof StaticRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StaticRoute: StaticRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/react-start/basic-nitro/src/router.tsx b/e2e/react-start/basic-nitro/src/router.tsx new file mode 100644 index 00000000000..1a1d8822d20 --- /dev/null +++ b/e2e/react-start/basic-nitro/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function getRouter() { + const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + }) + + return router +} diff --git a/e2e/react-start/basic-nitro/src/routes/__root.tsx b/e2e/react-start/basic-nitro/src/routes/__root.tsx new file mode 100644 index 00000000000..17577a3905e --- /dev/null +++ b/e2e/react-start/basic-nitro/src/routes/__root.tsx @@ -0,0 +1,92 @@ +/// +import { + HeadContent, + Link, + Scripts, + createRootRoute, +} from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' +import * as React from 'react' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' +import { seo } from '~/utils/seo' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ...seo({ + title: + 'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework', + description: `TanStack Start is a type-safe, client-first, full-stack React framework. `, + }), + ], + links: [ + { rel: 'stylesheet', href: appCss }, + { + rel: 'apple-touch-icon', + sizes: '180x180', + href: '/apple-touch-icon.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '32x32', + href: '/favicon-32x32.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '16x16', + href: '/favicon-16x16.png', + }, + { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' }, + { rel: 'icon', href: '/favicon.ico' }, + ], + }), + errorComponent: DefaultCatchBoundary, + notFoundComponent: () => , + shellComponent: RootDocument, +}) + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + +
+ + Home + + + Static + +
+
+ {children} + + + + + ) +} diff --git a/e2e/react-start/basic-nitro/src/routes/index.tsx b/e2e/react-start/basic-nitro/src/routes/index.tsx new file mode 100644 index 00000000000..2b0878af588 --- /dev/null +++ b/e2e/react-start/basic-nitro/src/routes/index.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' + +export const Route = createFileRoute('/')({ + loader: () => getData(), + component: Home, +}) + +const getData = createServerFn().handler(() => { + return { + message: `Running in Node.js ${process.version}`, + runtime: 'Nitro', + } +}) + +function Home() { + const data = Route.useLoaderData() + + return ( +
+

Welcome Home!!!

+

{data.message}

+

{data.runtime}

+
+ ) +} diff --git a/e2e/react-start/basic-nitro/src/routes/static.tsx b/e2e/react-start/basic-nitro/src/routes/static.tsx new file mode 100644 index 00000000000..3333760d11a --- /dev/null +++ b/e2e/react-start/basic-nitro/src/routes/static.tsx @@ -0,0 +1,28 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' + +export const Route = createFileRoute('/static')({ + loader: () => getData(), + component: StaticPage, +}) + +const getData = createServerFn().handler(() => { + return { + generatedAt: new Date().toISOString(), + runtime: 'Nitro', + } +}) + +function StaticPage() { + const data = Route.useLoaderData() + + return ( +
+

Static Page

+

+ This page was prerendered with {data.runtime} +

+

Generated at: {data.generatedAt}

+
+ ) +} diff --git a/e2e/react-start/basic-nitro/src/styles/app.css b/e2e/react-start/basic-nitro/src/styles/app.css new file mode 100644 index 00000000000..c36c737cd46 --- /dev/null +++ b/e2e/react-start/basic-nitro/src/styles/app.css @@ -0,0 +1,30 @@ +@import 'tailwindcss'; + +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/react-start/basic-nitro/src/utils/seo.ts b/e2e/react-start/basic-nitro/src/utils/seo.ts new file mode 100644 index 00000000000..d18ad84b74e --- /dev/null +++ b/e2e/react-start/basic-nitro/src/utils/seo.ts @@ -0,0 +1,33 @@ +export const seo = ({ + title, + description, + keywords, + image, +}: { + title: string + description?: string + image?: string + keywords?: string +}) => { + const tags = [ + { title }, + { name: 'description', content: description }, + { name: 'keywords', content: keywords }, + { name: 'twitter:title', content: title }, + { name: 'twitter:description', content: description }, + { name: 'twitter:creator', content: '@tannerlinsley' }, + { name: 'twitter:site', content: '@tannerlinsley' }, + { name: 'og:type', content: 'website' }, + { name: 'og:title', content: title }, + { name: 'og:description', content: description }, + ...(image + ? [ + { name: 'twitter:image', content: image }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'og:image', content: image }, + ] + : []), + ] + + return tags +} diff --git a/e2e/react-start/basic-nitro/tests/app.spec.ts b/e2e/react-start/basic-nitro/tests/app.spec.ts new file mode 100644 index 00000000000..31b1a7f6251 --- /dev/null +++ b/e2e/react-start/basic-nitro/tests/app.spec.ts @@ -0,0 +1,38 @@ +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import { expect } from '@playwright/test' +import { test } from '@tanstack/router-e2e-utils' + +test('returns correct runtime info', async ({ page }) => { + await page.goto('/') + await expect(page.getByTestId('message')).toContainText('Running in Node.js') + await expect(page.getByTestId('runtime')).toHaveText('Nitro') +}) + +test('prerender with Nitro', async ({ page }) => { + const distDir = join(process.cwd(), '.output', 'public') + const staticHtmlPath = join(distDir, 'static', 'index.html') + expect(existsSync(staticHtmlPath)).toBe(true) + const staticHtml = readFileSync(staticHtmlPath, 'utf8') + const normalizedHtml = staticHtml.replace(//g, '') + expect(normalizedHtml).toContain('This page was prerendered with Nitro') + expect(normalizedHtml).toContain('Generated at:') + + await page.goto('/static') + await expect(page.getByTestId('static-heading')).toHaveText('Static Page') + await expect(page.getByTestId('static-content')).toHaveText( + 'This page was prerendered with Nitro', + ) + await expect(page.getByTestId('generated-at')).toContainText('Generated at:') +}) + +test('client-side navigation works', async ({ page }) => { + await page.goto('/') + await expect(page.getByTestId('message')).toContainText('Running in Node.js') + + await page.getByRole('link', { name: 'Static' }).click() + await expect(page.getByTestId('static-heading')).toHaveText('Static Page') + + await page.getByRole('link', { name: 'Home' }).click() + await expect(page.getByTestId('message')).toContainText('Running in Node.js') +}) diff --git a/e2e/react-start/basic-nitro/tests/setup/global.setup.ts b/e2e/react-start/basic-nitro/tests/setup/global.setup.ts new file mode 100644 index 00000000000..f54c01cad2c --- /dev/null +++ b/e2e/react-start/basic-nitro/tests/setup/global.setup.ts @@ -0,0 +1,3 @@ +export default async function setup() { + // No additional setup needed for Nitro +} diff --git a/e2e/react-start/basic-nitro/tests/setup/global.teardown.ts b/e2e/react-start/basic-nitro/tests/setup/global.teardown.ts new file mode 100644 index 00000000000..ac5a84f5ea6 --- /dev/null +++ b/e2e/react-start/basic-nitro/tests/setup/global.teardown.ts @@ -0,0 +1,3 @@ +export default async function teardown() { + // No additional teardown needed for Nitro +} diff --git a/e2e/react-start/basic-nitro/tsconfig.json b/e2e/react-start/basic-nitro/tsconfig.json new file mode 100644 index 00000000000..3a9fb7cd716 --- /dev/null +++ b/e2e/react-start/basic-nitro/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true + } +} diff --git a/e2e/react-start/basic-nitro/vite.config.v2.ts b/e2e/react-start/basic-nitro/vite.config.v2.ts new file mode 100644 index 00000000000..a10eb758e6e --- /dev/null +++ b/e2e/react-start/basic-nitro/vite.config.v2.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin' + +export default defineConfig({ + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart({ + prerender: { + enabled: true, + filter: (page) => page.path === '/static', + }, + }), + nitroV2Plugin(), + ], +}) diff --git a/e2e/react-start/basic-nitro/vite.config.v3.ts b/e2e/react-start/basic-nitro/vite.config.v3.ts new file mode 100644 index 00000000000..eb7246636c4 --- /dev/null +++ b/e2e/react-start/basic-nitro/vite.config.v3.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import { nitro } from 'nitro/vite' + +export default defineConfig({ + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart({ + prerender: { + enabled: true, + filter: (page) => page.path === '/static', + }, + }), + nitro(), + ], +}) diff --git a/e2e/solid-start/basic-nitro-spa/.gitignore b/e2e/solid-start/basic-nitro-spa/.gitignore new file mode 100644 index 00000000000..114d10aa0e4 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/.gitignore @@ -0,0 +1,12 @@ +node_modules +.DS_Store +.cache +.env +dist +.output +.nitro + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-start/basic-nitro-spa/package.json b/e2e/solid-start/basic-nitro-spa/package.json new file mode 100644 index 00000000000..b6bde8093c3 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/package.json @@ -0,0 +1,40 @@ +{ + "name": "tanstack-solid-start-e2e-basic-nitro-spa", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev:v2": "vite dev -c vite.config.v2.ts --port 3000", + "dev:v3": "vite dev -c vite.config.v3.ts --port 3000", + "dev:e2e:v2": "vite dev -c vite.config.v2.ts", + "dev:e2e:v3": "vite dev -c vite.config.v3.ts", + "build:v2": "vite build -c vite.config.v2.ts && tsc --noEmit", + "build:v3": "vite build -c vite.config.v3.ts && tsc --noEmit", + "preview:v2": "vite preview -c vite.config.v2.ts", + "preview:v3": "vite preview -c vite.config.v3.ts", + "test:e2e:shared": "rm -rf port*.txt; playwright test --project=chromium", + "test:e2e:v2": "rm -rf .output dist .nitro && NITRO_VARIANT=v2 pnpm run test:e2e:shared", + "test:e2e:v3": "rm -rf .output dist .nitro && NITRO_VARIANT=v3 pnpm run test:e2e:shared", + "test:e2e": "pnpm run test:e2e:v2 && pnpm run test:e2e:v3" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/solid-router-devtools": "workspace:^", + "@tanstack/solid-start": "workspace:^", + "solid-js": "^1.9.10" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tailwindcss/postcss": "^4.1.15", + "@tanstack/router-e2e-utils": "workspace:^", + "@tanstack/nitro-v2-vite-plugin": "workspace:^", + "@types/node": "^22.10.2", + "nitro": "npm:nitro-nightly@latest", + "postcss": "^8.5.1", + "tailwindcss": "^4.1.15", + "typescript": "^5.7.2", + "vite": "^7.1.7", + "vite-plugin-solid": "^2.11.10", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/solid-start/basic-nitro-spa/playwright.config.ts b/e2e/solid-start/basic-nitro-spa/playwright.config.ts new file mode 100644 index 00000000000..b26f5493b47 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/playwright.config.ts @@ -0,0 +1,44 @@ +import { defineConfig, devices } from '@playwright/test' +import { getTestServerPort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = await getTestServerPort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +const nitroVariant = process.env.NITRO_VARIANT +if (nitroVariant !== 'v2' && nitroVariant !== 'v3') { + throw new Error('Set NITRO_VARIANT to "v2" or "v3" for Nitro e2e tests.') +} +const buildScript = nitroVariant === 'v2' ? 'build:v2' : 'build:v3' +const buildCommand = `pnpm run ${buildScript}` + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + // Note: We run node directly instead of vite preview because Nitro's + // configurePreviewServer spawns on a random port. The prerendering during + // build uses vite.preview() correctly. + command: `${buildCommand} && PORT=${PORT} node .output/server/index.mjs`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-start/basic-nitro-spa/postcss.config.mjs b/e2e/solid-start/basic-nitro-spa/postcss.config.mjs new file mode 100644 index 00000000000..a7f73a2d1d7 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/e2e/solid-start/basic-nitro-spa/public/android-chrome-192x192.png b/e2e/solid-start/basic-nitro-spa/public/android-chrome-192x192.png new file mode 100644 index 00000000000..09c8324f8c6 Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/android-chrome-192x192.png differ diff --git a/e2e/solid-start/basic-nitro-spa/public/android-chrome-512x512.png b/e2e/solid-start/basic-nitro-spa/public/android-chrome-512x512.png new file mode 100644 index 00000000000..11d626ea3d0 Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/android-chrome-512x512.png differ diff --git a/e2e/solid-start/basic-nitro-spa/public/apple-touch-icon.png b/e2e/solid-start/basic-nitro-spa/public/apple-touch-icon.png new file mode 100644 index 00000000000..5a9423cc02c Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/apple-touch-icon.png differ diff --git a/e2e/solid-start/basic-nitro-spa/public/favicon-16x16.png b/e2e/solid-start/basic-nitro-spa/public/favicon-16x16.png new file mode 100644 index 00000000000..e3389b00443 Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/favicon-16x16.png differ diff --git a/e2e/solid-start/basic-nitro-spa/public/favicon-32x32.png b/e2e/solid-start/basic-nitro-spa/public/favicon-32x32.png new file mode 100644 index 00000000000..900c77d444c Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/favicon-32x32.png differ diff --git a/e2e/solid-start/basic-nitro-spa/public/favicon.ico b/e2e/solid-start/basic-nitro-spa/public/favicon.ico new file mode 100644 index 00000000000..1a1751676f7 Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/favicon.ico differ diff --git a/e2e/solid-start/basic-nitro-spa/public/favicon.png b/e2e/solid-start/basic-nitro-spa/public/favicon.png new file mode 100644 index 00000000000..1e77bc06091 Binary files /dev/null and b/e2e/solid-start/basic-nitro-spa/public/favicon.png differ diff --git a/e2e/solid-start/basic-nitro-spa/public/site.webmanifest b/e2e/solid-start/basic-nitro-spa/public/site.webmanifest new file mode 100644 index 00000000000..fa99de77db6 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/e2e/solid-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx b/e2e/solid-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx new file mode 100644 index 00000000000..2c0d464a066 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/src/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/solid-router' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot() ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/e2e/solid-start/basic-nitro-spa/src/components/NotFound.tsx b/e2e/solid-start/basic-nitro-spa/src/components/NotFound.tsx new file mode 100644 index 00000000000..c48444862b5 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/src/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from '@tanstack/solid-router' + +export function NotFound({ children }: { children?: any }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts b/e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts new file mode 100644 index 00000000000..2bd11546dd6 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as StaticRouteImport } from './routes/static' +import { Route as IndexRouteImport } from './routes/index' + +const StaticRoute = StaticRouteImport.update({ + id: '/static', + path: '/static', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/static': typeof StaticRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/static': typeof StaticRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/static': typeof StaticRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/static' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/static' + id: '__root__' | '/' | '/static' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StaticRoute: typeof StaticRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/static': { + id: '/static' + path: '/static' + fullPath: '/static' + preLoaderRoute: typeof StaticRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StaticRoute: StaticRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/solid-start/basic-nitro-spa/src/router.tsx b/e2e/solid-start/basic-nitro-spa/src/router.tsx new file mode 100644 index 00000000000..5da353c1ce2 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function getRouter() { + const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + }) + + return router +} diff --git a/e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx b/e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx new file mode 100644 index 00000000000..7020b288737 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/src/routes/__root.tsx @@ -0,0 +1,75 @@ +/// +import { + HeadContent, + Link, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' +import { HydrationScript } from 'solid-js/web' +import type * as Solid from 'solid-js' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' +import { seo } from '~/utils/seo' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charset: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ...seo({ + title: 'TanStack Start + Nitro SPA E2E Test', + description: 'Testing nitro SPA integration with TanStack Start', + }), + ], + links: [ + { rel: 'stylesheet', href: appCss }, + { rel: 'icon', href: '/favicon.ico' }, + ], + }), + errorComponent: DefaultCatchBoundary, + notFoundComponent: () => , + shellComponent: RootDocument, +}) + +function RootDocument({ children }: { children: Solid.JSX.Element }) { + return ( + + + + + + +
+ + Home + + + Static + +
+
+ {children} + + + + + ) +} diff --git a/e2e/solid-start/basic-nitro-spa/src/routes/index.tsx b/e2e/solid-start/basic-nitro-spa/src/routes/index.tsx new file mode 100644 index 00000000000..fdcf05ce075 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/src/routes/index.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' + +export const Route = createFileRoute('/')({ + loader: () => getData(), + component: Home, +}) + +const getData = createServerFn().handler(() => { + return { + message: 'Hello from Nitro server!', + timestamp: new Date().toISOString(), + } +}) + +function Home() { + const data = Route.useLoaderData() + + return ( +
+

Welcome Home!

+

{data().message}

+

Loaded at: {data().timestamp}

+
+ ) +} diff --git a/e2e/solid-start/basic-nitro-spa/src/routes/static.tsx b/e2e/solid-start/basic-nitro-spa/src/routes/static.tsx new file mode 100644 index 00000000000..55c781f286c --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/src/routes/static.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' + +export const Route = createFileRoute('/static')({ + loader: () => getStaticData(), + component: StaticPage, +}) + +const getStaticData = createServerFn().handler(() => { + return { + content: 'This page was prerendered at build time', + buildTime: new Date().toISOString(), + } +}) + +function StaticPage() { + const data = Route.useLoaderData() + + return ( +
+

Static Page

+

{data().content}

+

Build time: {data().buildTime}

+
+ ) +} diff --git a/e2e/solid-start/basic-nitro-spa/src/styles/app.css b/e2e/solid-start/basic-nitro-spa/src/styles/app.css new file mode 100644 index 00000000000..c36c737cd46 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/src/styles/app.css @@ -0,0 +1,30 @@ +@import 'tailwindcss'; + +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/solid-start/basic-nitro-spa/src/utils/seo.ts b/e2e/solid-start/basic-nitro-spa/src/utils/seo.ts new file mode 100644 index 00000000000..d18ad84b74e --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/src/utils/seo.ts @@ -0,0 +1,33 @@ +export const seo = ({ + title, + description, + keywords, + image, +}: { + title: string + description?: string + image?: string + keywords?: string +}) => { + const tags = [ + { title }, + { name: 'description', content: description }, + { name: 'keywords', content: keywords }, + { name: 'twitter:title', content: title }, + { name: 'twitter:description', content: description }, + { name: 'twitter:creator', content: '@tannerlinsley' }, + { name: 'twitter:site', content: '@tannerlinsley' }, + { name: 'og:type', content: 'website' }, + { name: 'og:title', content: title }, + { name: 'og:description', content: description }, + ...(image + ? [ + { name: 'twitter:image', content: image }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'og:image', content: image }, + ] + : []), + ] + + return tags +} diff --git a/e2e/solid-start/basic-nitro-spa/tests/app.spec.ts b/e2e/solid-start/basic-nitro-spa/tests/app.spec.ts new file mode 100644 index 00000000000..b0f96e373af --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/tests/app.spec.ts @@ -0,0 +1,30 @@ +import { existsSync } from 'node:fs' +import { join } from 'node:path' +import { expect, test } from '@playwright/test' + +test('SPA shell is prerendered during build with nitro', async ({ page }) => { + const outputDir = join(process.cwd(), '.output', 'public') + expect(existsSync(join(outputDir, 'index.html'))).toBe(true) + + await page.goto('/') + await expect(page.getByTestId('home-heading')).toBeVisible() +}) + +test('server functions work with nitro', async ({ page }) => { + await page.goto('/') + await expect(page.getByTestId('home-heading')).toHaveText('Welcome Home!') + await expect(page.getByTestId('message')).toHaveText( + 'Hello from Nitro server!', + ) +}) + +test('client-side navigation works in SPA mode', async ({ page }) => { + await page.goto('/') + await expect(page.getByTestId('home-heading')).toBeVisible() + + await page.click('a[href="/static"]') + await expect(page.getByTestId('static-heading')).toBeVisible() + + await page.click('a[href="/"]') + await expect(page.getByTestId('home-heading')).toBeVisible() +}) diff --git a/e2e/solid-start/basic-nitro-spa/tsconfig.json b/e2e/solid-start/basic-nitro-spa/tsconfig.json new file mode 100644 index 00000000000..ed8b73fa2dd --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/tsconfig.json @@ -0,0 +1,24 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true, + "types": ["vite/client"] + } +} diff --git a/e2e/solid-start/basic-nitro-spa/vite.config.v2.ts b/e2e/solid-start/basic-nitro-spa/vite.config.v2.ts new file mode 100644 index 00000000000..6677a660004 --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/vite.config.v2.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin' +import viteSolid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart({ + spa: { + enabled: true, + prerender: { + outputPath: 'index.html', + }, + }, + }), + viteSolid({ ssr: true }), + nitroV2Plugin(), + ], +}) diff --git a/e2e/solid-start/basic-nitro-spa/vite.config.v3.ts b/e2e/solid-start/basic-nitro-spa/vite.config.v3.ts new file mode 100644 index 00000000000..2a9b082bf2e --- /dev/null +++ b/e2e/solid-start/basic-nitro-spa/vite.config.v3.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import { nitro } from 'nitro/vite' +import viteSolid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart({ + spa: { + enabled: true, + prerender: { + outputPath: 'index.html', + }, + }, + }), + viteSolid({ ssr: true }), + nitro(), + ], +}) diff --git a/e2e/solid-start/basic-nitro/.gitignore b/e2e/solid-start/basic-nitro/.gitignore new file mode 100644 index 00000000000..114d10aa0e4 --- /dev/null +++ b/e2e/solid-start/basic-nitro/.gitignore @@ -0,0 +1,12 @@ +node_modules +.DS_Store +.cache +.env +dist +.output +.nitro + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-start/basic-nitro/.prettierignore b/e2e/solid-start/basic-nitro/.prettierignore new file mode 100644 index 00000000000..a16e01379d7 --- /dev/null +++ b/e2e/solid-start/basic-nitro/.prettierignore @@ -0,0 +1 @@ +routeTree.gen.ts diff --git a/e2e/solid-start/basic-nitro/package.json b/e2e/solid-start/basic-nitro/package.json new file mode 100644 index 00000000000..92363500e09 --- /dev/null +++ b/e2e/solid-start/basic-nitro/package.json @@ -0,0 +1,40 @@ +{ + "name": "tanstack-solid-start-e2e-basic-nitro", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev:v2": "vite dev -c vite.config.v2.ts --port 3000", + "dev:v3": "vite dev -c vite.config.v3.ts --port 3000", + "dev:e2e:v2": "vite dev -c vite.config.v2.ts", + "dev:e2e:v3": "vite dev -c vite.config.v3.ts", + "build:v2": "vite build -c vite.config.v2.ts && tsc --noEmit", + "build:v3": "vite build -c vite.config.v3.ts && tsc --noEmit", + "preview:v2": "vite preview -c vite.config.v2.ts", + "preview:v3": "vite preview -c vite.config.v3.ts", + "test:e2e:shared": "rm -rf port*.txt; playwright test --project=chromium", + "test:e2e:v2": "rm -rf .output dist .nitro && NITRO_VARIANT=v2 pnpm run test:e2e:shared", + "test:e2e:v3": "rm -rf .output dist .nitro && NITRO_VARIANT=v3 pnpm run test:e2e:shared", + "test:e2e": "pnpm run test:e2e:v2 && pnpm run test:e2e:v3" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/solid-router-devtools": "workspace:^", + "@tanstack/solid-start": "workspace:^", + "solid-js": "^1.9.10" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tailwindcss/postcss": "^4.1.15", + "@tanstack/router-e2e-utils": "workspace:^", + "@tanstack/nitro-v2-vite-plugin": "workspace:^", + "@types/node": "^22.10.2", + "nitro": "npm:nitro-nightly@latest", + "postcss": "^8.5.1", + "tailwindcss": "^4.1.15", + "typescript": "^5.7.2", + "vite": "^7.1.7", + "vite-plugin-solid": "^2.11.10", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/solid-start/basic-nitro/playwright.config.ts b/e2e/solid-start/basic-nitro/playwright.config.ts new file mode 100644 index 00000000000..b26f5493b47 --- /dev/null +++ b/e2e/solid-start/basic-nitro/playwright.config.ts @@ -0,0 +1,44 @@ +import { defineConfig, devices } from '@playwright/test' +import { getTestServerPort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = await getTestServerPort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +const nitroVariant = process.env.NITRO_VARIANT +if (nitroVariant !== 'v2' && nitroVariant !== 'v3') { + throw new Error('Set NITRO_VARIANT to "v2" or "v3" for Nitro e2e tests.') +} +const buildScript = nitroVariant === 'v2' ? 'build:v2' : 'build:v3' +const buildCommand = `pnpm run ${buildScript}` + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + // Note: We run node directly instead of vite preview because Nitro's + // configurePreviewServer spawns on a random port. The prerendering during + // build uses vite.preview() correctly. + command: `${buildCommand} && PORT=${PORT} node .output/server/index.mjs`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-start/basic-nitro/postcss.config.mjs b/e2e/solid-start/basic-nitro/postcss.config.mjs new file mode 100644 index 00000000000..a7f73a2d1d7 --- /dev/null +++ b/e2e/solid-start/basic-nitro/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/e2e/solid-start/basic-nitro/public/android-chrome-192x192.png b/e2e/solid-start/basic-nitro/public/android-chrome-192x192.png new file mode 100644 index 00000000000..09c8324f8c6 Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/android-chrome-192x192.png differ diff --git a/e2e/solid-start/basic-nitro/public/android-chrome-512x512.png b/e2e/solid-start/basic-nitro/public/android-chrome-512x512.png new file mode 100644 index 00000000000..11d626ea3d0 Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/android-chrome-512x512.png differ diff --git a/e2e/solid-start/basic-nitro/public/apple-touch-icon.png b/e2e/solid-start/basic-nitro/public/apple-touch-icon.png new file mode 100644 index 00000000000..5a9423cc02c Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/apple-touch-icon.png differ diff --git a/e2e/solid-start/basic-nitro/public/favicon-16x16.png b/e2e/solid-start/basic-nitro/public/favicon-16x16.png new file mode 100644 index 00000000000..e3389b00443 Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/favicon-16x16.png differ diff --git a/e2e/solid-start/basic-nitro/public/favicon-32x32.png b/e2e/solid-start/basic-nitro/public/favicon-32x32.png new file mode 100644 index 00000000000..900c77d444c Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/favicon-32x32.png differ diff --git a/e2e/solid-start/basic-nitro/public/favicon.ico b/e2e/solid-start/basic-nitro/public/favicon.ico new file mode 100644 index 00000000000..1a1751676f7 Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/favicon.ico differ diff --git a/e2e/solid-start/basic-nitro/public/favicon.png b/e2e/solid-start/basic-nitro/public/favicon.png new file mode 100644 index 00000000000..1e77bc06091 Binary files /dev/null and b/e2e/solid-start/basic-nitro/public/favicon.png differ diff --git a/e2e/solid-start/basic-nitro/public/site.webmanifest b/e2e/solid-start/basic-nitro/public/site.webmanifest new file mode 100644 index 00000000000..fa99de77db6 --- /dev/null +++ b/e2e/solid-start/basic-nitro/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/e2e/solid-start/basic-nitro/src/components/DefaultCatchBoundary.tsx b/e2e/solid-start/basic-nitro/src/components/DefaultCatchBoundary.tsx new file mode 100644 index 00000000000..2c0d464a066 --- /dev/null +++ b/e2e/solid-start/basic-nitro/src/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/solid-router' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot() ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/e2e/solid-start/basic-nitro/src/components/NotFound.tsx b/e2e/solid-start/basic-nitro/src/components/NotFound.tsx new file mode 100644 index 00000000000..c48444862b5 --- /dev/null +++ b/e2e/solid-start/basic-nitro/src/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from '@tanstack/solid-router' + +export function NotFound({ children }: { children?: any }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/e2e/solid-start/basic-nitro/src/routeTree.gen.ts b/e2e/solid-start/basic-nitro/src/routeTree.gen.ts new file mode 100644 index 00000000000..2bd11546dd6 --- /dev/null +++ b/e2e/solid-start/basic-nitro/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as StaticRouteImport } from './routes/static' +import { Route as IndexRouteImport } from './routes/index' + +const StaticRoute = StaticRouteImport.update({ + id: '/static', + path: '/static', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/static': typeof StaticRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/static': typeof StaticRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/static': typeof StaticRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/static' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/static' + id: '__root__' | '/' | '/static' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StaticRoute: typeof StaticRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/static': { + id: '/static' + path: '/static' + fullPath: '/static' + preLoaderRoute: typeof StaticRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StaticRoute: StaticRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/solid-start/basic-nitro/src/router.tsx b/e2e/solid-start/basic-nitro/src/router.tsx new file mode 100644 index 00000000000..5da353c1ce2 --- /dev/null +++ b/e2e/solid-start/basic-nitro/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function getRouter() { + const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + }) + + return router +} diff --git a/e2e/solid-start/basic-nitro/src/routes/__root.tsx b/e2e/solid-start/basic-nitro/src/routes/__root.tsx new file mode 100644 index 00000000000..9d2f56b6756 --- /dev/null +++ b/e2e/solid-start/basic-nitro/src/routes/__root.tsx @@ -0,0 +1,94 @@ +/// +import { + HeadContent, + Link, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' +import { HydrationScript } from 'solid-js/web' +import type * as Solid from 'solid-js' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' +import { seo } from '~/utils/seo' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charset: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ...seo({ + title: + 'TanStack Start | Type-Safe, Client-First, Full-Stack Solid Framework', + description: `TanStack Start is a type-safe, client-first, full-stack Solid framework. `, + }), + ], + links: [ + { rel: 'stylesheet', href: appCss }, + { + rel: 'apple-touch-icon', + sizes: '180x180', + href: '/apple-touch-icon.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '32x32', + href: '/favicon-32x32.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '16x16', + href: '/favicon-16x16.png', + }, + { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' }, + { rel: 'icon', href: '/favicon.ico' }, + ], + }), + errorComponent: DefaultCatchBoundary, + notFoundComponent: () => , + shellComponent: RootDocument, +}) + +function RootDocument({ children }: { children: Solid.JSX.Element }) { + return ( + + + + + + +
+ + Home + + + Static + +
+
+ {children} + + + + + ) +} diff --git a/e2e/solid-start/basic-nitro/src/routes/index.tsx b/e2e/solid-start/basic-nitro/src/routes/index.tsx new file mode 100644 index 00000000000..681a335b355 --- /dev/null +++ b/e2e/solid-start/basic-nitro/src/routes/index.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' + +export const Route = createFileRoute('/')({ + loader: () => getData(), + component: Home, +}) + +const getData = createServerFn().handler(() => { + return { + message: `Running in ${typeof navigator !== 'undefined' ? navigator.userAgent : 'Unknown'}`, + runtime: 'Nitro', + } +}) + +function Home() { + const data = Route.useLoaderData() + + return ( +
+

Welcome Home!!!

+

{data().message}

+

{data().runtime}

+
+ ) +} diff --git a/e2e/solid-start/basic-nitro/src/routes/static.tsx b/e2e/solid-start/basic-nitro/src/routes/static.tsx new file mode 100644 index 00000000000..5d7a478efd6 --- /dev/null +++ b/e2e/solid-start/basic-nitro/src/routes/static.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/static')({ + component: StaticPage, +}) + +function StaticPage() { + return ( +
+

Static Page

+

This page was prerendered with Nitro

+
+ ) +} diff --git a/e2e/solid-start/basic-nitro/src/styles/app.css b/e2e/solid-start/basic-nitro/src/styles/app.css new file mode 100644 index 00000000000..c36c737cd46 --- /dev/null +++ b/e2e/solid-start/basic-nitro/src/styles/app.css @@ -0,0 +1,30 @@ +@import 'tailwindcss'; + +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/solid-start/basic-nitro/src/utils/seo.ts b/e2e/solid-start/basic-nitro/src/utils/seo.ts new file mode 100644 index 00000000000..d18ad84b74e --- /dev/null +++ b/e2e/solid-start/basic-nitro/src/utils/seo.ts @@ -0,0 +1,33 @@ +export const seo = ({ + title, + description, + keywords, + image, +}: { + title: string + description?: string + image?: string + keywords?: string +}) => { + const tags = [ + { title }, + { name: 'description', content: description }, + { name: 'keywords', content: keywords }, + { name: 'twitter:title', content: title }, + { name: 'twitter:description', content: description }, + { name: 'twitter:creator', content: '@tannerlinsley' }, + { name: 'twitter:site', content: '@tannerlinsley' }, + { name: 'og:type', content: 'website' }, + { name: 'og:title', content: title }, + { name: 'og:description', content: description }, + ...(image + ? [ + { name: 'twitter:image', content: image }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'og:image', content: image }, + ] + : []), + ] + + return tags +} diff --git a/e2e/solid-start/basic-nitro/tests/app.spec.ts b/e2e/solid-start/basic-nitro/tests/app.spec.ts new file mode 100644 index 00000000000..c48116ea805 --- /dev/null +++ b/e2e/solid-start/basic-nitro/tests/app.spec.ts @@ -0,0 +1,36 @@ +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import { expect } from '@playwright/test' +import { test } from '@tanstack/router-e2e-utils' + +test('returns correct runtime info', async ({ page }) => { + await page.goto('/') + await expect(page.getByTestId('message')).toContainText('Running in Node.js') + await expect(page.getByTestId('runtime')).toHaveText('Nitro') +}) + +test('prerender with Nitro', async ({ page }) => { + const distDir = join(process.cwd(), '.output', 'public') + const staticHtmlPath = join(distDir, 'static', 'index.html') + expect(existsSync(staticHtmlPath)).toBe(true) + const staticHtml = readFileSync(staticHtmlPath, 'utf8') + const normalizedHtml = staticHtml.replace(//g, '') + expect(normalizedHtml).toContain('This page was prerendered with Nitro') + + await page.goto('/static') + await expect(page.getByTestId('static-heading')).toHaveText('Static Page') + await expect(page.getByTestId('static-content')).toHaveText( + 'This page was prerendered with Nitro', + ) +}) + +test('client-side navigation works', async ({ page }) => { + await page.goto('/') + await expect(page.getByTestId('message')).toContainText('Running in Node.js') + + await page.getByRole('link', { name: 'Static' }).click() + await expect(page.getByTestId('static-heading')).toHaveText('Static Page') + + await page.getByRole('link', { name: 'Home' }).click() + await expect(page.getByTestId('message')).toContainText('Running in Node.js') +}) diff --git a/e2e/solid-start/basic-nitro/tsconfig.json b/e2e/solid-start/basic-nitro/tsconfig.json new file mode 100644 index 00000000000..ed8b73fa2dd --- /dev/null +++ b/e2e/solid-start/basic-nitro/tsconfig.json @@ -0,0 +1,24 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true, + "types": ["vite/client"] + } +} diff --git a/e2e/solid-start/basic-nitro/vite.config.v2.ts b/e2e/solid-start/basic-nitro/vite.config.v2.ts new file mode 100644 index 00000000000..15ad2fabfc3 --- /dev/null +++ b/e2e/solid-start/basic-nitro/vite.config.v2.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin' +import viteSolid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart({ + prerender: { + enabled: true, + filter: (page) => page.path === '/static', + }, + }), + viteSolid({ ssr: true }), + nitroV2Plugin(), + ], +}) diff --git a/e2e/solid-start/basic-nitro/vite.config.v3.ts b/e2e/solid-start/basic-nitro/vite.config.v3.ts new file mode 100644 index 00000000000..5f6a5f48e73 --- /dev/null +++ b/e2e/solid-start/basic-nitro/vite.config.v3.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import { nitro } from 'nitro/vite' +import viteSolid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart({ + prerender: { + enabled: true, + filter: (page) => page.path === '/static', + }, + }), + viteSolid({ ssr: true }), + nitro(), + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ab9c9061b6..ff91aeb6675 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1293,6 +1293,122 @@ importers: specifier: ^4.49.1 version: 4.49.1 + e2e/react-start/basic-nitro: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/react-router-devtools': + specifier: workspace:^ + version: link:../../../packages/react-router-devtools + '@tanstack/react-start': + specifier: workspace:* + version: link:../../../packages/react-start + react: + specifier: ^19.2.0 + version: 19.2.0 + react-dom: + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) + devDependencies: + '@playwright/test': + specifier: ^1.56.1 + version: 1.56.1 + '@tailwindcss/postcss': + specifier: ^4.1.15 + version: 4.1.18 + '@tanstack/nitro-v2-vite-plugin': + specifier: workspace:* + version: link:../../../packages/nitro-v2-vite-plugin + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/node': + specifier: 22.10.2 + version: 22.10.2 + '@types/react': + specifier: ^19.2.2 + version: 19.2.2 + '@types/react-dom': + specifier: ^19.2.2 + version: 19.2.2(@types/react@19.2.2) + nitro: + specifier: npm:nitro-nightly@latest + version: nitro-nightly@3.0.1-20251230-165713-6e801e22(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@4.0.3)(ioredis@5.8.0)(lru-cache@11.2.2)(mysql2@3.15.3)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + postcss: + specifier: ^8.5.1 + version: 8.5.6 + tailwindcss: + specifier: ^4.1.15 + version: 4.1.18 + typescript: + specifier: ^5.7.2 + version: 5.9.2 + vite: + specifier: ^7.1.7 + version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + + e2e/react-start/basic-nitro-spa: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/react-router-devtools': + specifier: workspace:^ + version: link:../../../packages/react-router-devtools + '@tanstack/react-start': + specifier: workspace:* + version: link:../../../packages/react-start + react: + specifier: ^19.2.0 + version: 19.2.0 + react-dom: + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) + devDependencies: + '@playwright/test': + specifier: ^1.56.1 + version: 1.56.1 + '@tailwindcss/postcss': + specifier: ^4.1.15 + version: 4.1.18 + '@tanstack/nitro-v2-vite-plugin': + specifier: workspace:* + version: link:../../../packages/nitro-v2-vite-plugin + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/node': + specifier: 22.10.2 + version: 22.10.2 + '@types/react': + specifier: ^19.2.2 + version: 19.2.2 + '@types/react-dom': + specifier: ^19.2.2 + version: 19.2.2(@types/react@19.2.2) + nitro: + specifier: npm:nitro-nightly@latest + version: nitro-nightly@3.0.1-20251230-165713-6e801e22(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@4.0.3)(ioredis@5.8.0)(lru-cache@11.2.2)(mysql2@3.15.3)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + postcss: + specifier: ^8.5.1 + version: 8.5.6 + tailwindcss: + specifier: ^4.1.15 + version: 4.1.18 + typescript: + specifier: ^5.7.2 + version: 5.9.2 + vite: + specifier: ^7.1.7 + version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + e2e/react-start/basic-react-query: dependencies: '@tanstack/react-query': @@ -3251,6 +3367,110 @@ importers: specifier: ^4.49.1 version: 4.49.1 + e2e/solid-start/basic-nitro: + dependencies: + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../../../packages/solid-router + '@tanstack/solid-router-devtools': + specifier: workspace:^ + version: link:../../../packages/solid-router-devtools + '@tanstack/solid-start': + specifier: workspace:* + version: link:../../../packages/solid-start + solid-js: + specifier: 1.9.10 + version: 1.9.10 + devDependencies: + '@playwright/test': + specifier: ^1.56.1 + version: 1.56.1 + '@tailwindcss/postcss': + specifier: ^4.1.15 + version: 4.1.18 + '@tanstack/nitro-v2-vite-plugin': + specifier: workspace:* + version: link:../../../packages/nitro-v2-vite-plugin + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/node': + specifier: 22.10.2 + version: 22.10.2 + nitro: + specifier: npm:nitro-nightly@latest + version: nitro-nightly@3.0.1-20251230-165713-6e801e22(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@4.0.3)(ioredis@5.8.0)(lru-cache@11.2.2)(mysql2@3.15.3)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + postcss: + specifier: ^8.5.1 + version: 8.5.6 + tailwindcss: + specifier: ^4.1.15 + version: 4.1.18 + typescript: + specifier: ^5.7.2 + version: 5.9.2 + vite: + specifier: ^7.1.7 + version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vite-plugin-solid: + specifier: ^2.11.10 + version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + + e2e/solid-start/basic-nitro-spa: + dependencies: + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../../../packages/solid-router + '@tanstack/solid-router-devtools': + specifier: workspace:^ + version: link:../../../packages/solid-router-devtools + '@tanstack/solid-start': + specifier: workspace:* + version: link:../../../packages/solid-start + solid-js: + specifier: 1.9.10 + version: 1.9.10 + devDependencies: + '@playwright/test': + specifier: ^1.56.1 + version: 1.56.1 + '@tailwindcss/postcss': + specifier: ^4.1.15 + version: 4.1.18 + '@tanstack/nitro-v2-vite-plugin': + specifier: workspace:* + version: link:../../../packages/nitro-v2-vite-plugin + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/node': + specifier: 22.10.2 + version: 22.10.2 + nitro: + specifier: npm:nitro-nightly@latest + version: nitro-nightly@3.0.1-20251230-165713-6e801e22(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@4.0.3)(ioredis@5.8.0)(lru-cache@11.2.2)(mysql2@3.15.3)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + postcss: + specifier: ^8.5.1 + version: 8.5.6 + tailwindcss: + specifier: ^4.1.15 + version: 4.1.18 + typescript: + specifier: ^5.7.2 + version: 5.9.2 + vite: + specifier: ^7.1.7 + version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vite-plugin-solid: + specifier: ^2.11.10 + version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + e2e/solid-start/basic-solid-query: dependencies: '@tanstack/solid-query': @@ -12846,15 +13066,9 @@ packages: '@electric-sql/pglite@0.3.2': resolution: {integrity: sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==} - '@emnapi/core@1.5.0': - resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} - '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} - '@emnapi/runtime@1.5.0': - resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} - '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} @@ -24640,25 +24854,14 @@ snapshots: '@electric-sql/pglite@0.3.2': {} - '@emnapi/core@1.5.0': - dependencies: - '@emnapi/wasi-threads': 1.1.0 - tslib: 2.8.1 - '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.5.0': - dependencies: - tslib: 2.8.1 '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 - optional: true '@emnapi/wasi-threads@1.1.0': dependencies: @@ -25719,12 +25922,12 @@ snapshots: '@img/sharp-wasm32@0.33.5': dependencies: - '@emnapi/runtime': 1.5.0 + '@emnapi/runtime': 1.7.1 optional: true '@img/sharp-wasm32@0.34.4': dependencies: - '@emnapi/runtime': 1.5.0 + '@emnapi/runtime': 1.7.1 optional: true '@img/sharp-win32-arm64@0.34.4': @@ -26200,15 +26403,15 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.5.0 - '@emnapi/runtime': 1.5.0 + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 '@tybys/wasm-util': 0.10.1 optional: true '@napi-rs/wasm-runtime@0.2.4': dependencies: - '@emnapi/core': 1.5.0 - '@emnapi/runtime': 1.5.0 + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 '@tybys/wasm-util': 0.9.0 '@napi-rs/wasm-runtime@1.1.0':