diff --git a/e2e/react-start/server-routes/.gitignore b/e2e/react-start/server-routes/.gitignore new file mode 100644 index 00000000000..ca63f498851 --- /dev/null +++ b/e2e/react-start/server-routes/.gitignore @@ -0,0 +1,18 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output +/build/ +/api/ +/server/build +/public/build# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/react-start/server-routes/.prettierignore b/e2e/react-start/server-routes/.prettierignore new file mode 100644 index 00000000000..2be5eaa6ece --- /dev/null +++ b/e2e/react-start/server-routes/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/e2e/react-start/server-routes/package.json b/e2e/react-start/server-routes/package.json new file mode 100644 index 00000000000..e7352c35ced --- /dev/null +++ b/e2e/react-start/server-routes/package.json @@ -0,0 +1,40 @@ +{ + "name": "tanstack-react-start-e2e-server-routes", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "dev:e2e": "vite dev", + "build": "vite build && tsc --noEmit", + "start": "node .output/server/index.mjs", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/react-router": "workspace:^", + "@tanstack/react-router-devtools": "workspace:^", + "@tanstack/react-start": "workspace:^", + "js-cookie": "^3.0.5", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "redaxios": "^0.5.1", + "tailwind-merge": "^2.6.0", + "vite": "^6.3.5", + "zod": "^3.24.2" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/js-cookie": "^3.0.6", + "@types/node": "^22.10.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "combinate": "^1.1.11", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/react-start/server-routes/playwright.config.ts b/e2e/react-start/server-routes/playwright.config.ts new file mode 100644 index 00000000000..5fa107c0202 --- /dev/null +++ b/e2e/react-start/server-routes/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +export const PORT = await derivePort(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: { + command: `VITE_SERVER_PORT=${PORT} pnpm build && PORT=${PORT} VITE_SERVER_PORT=${PORT} pnpm start`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/react-start/server-routes/postcss.config.mjs b/e2e/react-start/server-routes/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/react-start/server-routes/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/react-start/server-routes/public/favicon.ico b/e2e/react-start/server-routes/public/favicon.ico new file mode 100644 index 00000000000..1a1751676f7 Binary files /dev/null and b/e2e/react-start/server-routes/public/favicon.ico differ diff --git a/e2e/react-start/server-routes/public/favicon.png b/e2e/react-start/server-routes/public/favicon.png new file mode 100644 index 00000000000..1e77bc06091 Binary files /dev/null and b/e2e/react-start/server-routes/public/favicon.png differ diff --git a/e2e/react-start/server-routes/src/components/DefaultCatchBoundary.tsx b/e2e/react-start/server-routes/src/components/DefaultCatchBoundary.tsx new file mode 100644 index 00000000000..15f316681cc --- /dev/null +++ b/e2e/react-start/server-routes/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/server-routes/src/components/NotFound.tsx b/e2e/react-start/server-routes/src/components/NotFound.tsx new file mode 100644 index 00000000000..af4e0e74946 --- /dev/null +++ b/e2e/react-start/server-routes/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/server-routes/src/routeTree.gen.ts b/e2e/react-start/server-routes/src/routeTree.gen.ts new file mode 100644 index 00000000000..1d1d61ae496 --- /dev/null +++ b/e2e/react-start/server-routes/src/routeTree.gen.ts @@ -0,0 +1,127 @@ +/* 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 { createServerRootRoute } from '@tanstack/react-start/server' + +import { Route as rootRouteImport } from './routes/__root' +import { Route as MergeServerFnMiddlewareContextRouteImport } from './routes/merge-server-fn-middleware-context' +import { Route as IndexRouteImport } from './routes/index' +import { ServerRoute as ApiMiddlewareContextServerRouteImport } from './routes/api/middleware-context' + +const rootServerRouteImport = createServerRootRoute() + +const MergeServerFnMiddlewareContextRoute = + MergeServerFnMiddlewareContextRouteImport.update({ + id: '/merge-server-fn-middleware-context', + path: '/merge-server-fn-middleware-context', + getParentRoute: () => rootRouteImport, + } as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiMiddlewareContextServerRoute = + ApiMiddlewareContextServerRouteImport.update({ + id: '/api/middleware-context', + path: '/api/middleware-context', + getParentRoute: () => rootServerRouteImport, + } as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/merge-server-fn-middleware-context': typeof MergeServerFnMiddlewareContextRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/merge-server-fn-middleware-context': typeof MergeServerFnMiddlewareContextRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/merge-server-fn-middleware-context': typeof MergeServerFnMiddlewareContextRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/merge-server-fn-middleware-context' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/merge-server-fn-middleware-context' + id: '__root__' | '/' | '/merge-server-fn-middleware-context' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + MergeServerFnMiddlewareContextRoute: typeof MergeServerFnMiddlewareContextRoute +} +export interface FileServerRoutesByFullPath { + '/api/middleware-context': typeof ApiMiddlewareContextServerRoute +} +export interface FileServerRoutesByTo { + '/api/middleware-context': typeof ApiMiddlewareContextServerRoute +} +export interface FileServerRoutesById { + __root__: typeof rootServerRouteImport + '/api/middleware-context': typeof ApiMiddlewareContextServerRoute +} +export interface FileServerRouteTypes { + fileServerRoutesByFullPath: FileServerRoutesByFullPath + fullPaths: '/api/middleware-context' + fileServerRoutesByTo: FileServerRoutesByTo + to: '/api/middleware-context' + id: '__root__' | '/api/middleware-context' + fileServerRoutesById: FileServerRoutesById +} +export interface RootServerRouteChildren { + ApiMiddlewareContextServerRoute: typeof ApiMiddlewareContextServerRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/merge-server-fn-middleware-context': { + id: '/merge-server-fn-middleware-context' + path: '/merge-server-fn-middleware-context' + fullPath: '/merge-server-fn-middleware-context' + preLoaderRoute: typeof MergeServerFnMiddlewareContextRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} +declare module '@tanstack/react-start/server' { + interface ServerFileRoutesByPath { + '/api/middleware-context': { + id: '/api/middleware-context' + path: '/api/middleware-context' + fullPath: '/api/middleware-context' + preLoaderRoute: typeof ApiMiddlewareContextServerRouteImport + parentRoute: typeof rootServerRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + MergeServerFnMiddlewareContextRoute: MergeServerFnMiddlewareContextRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() +const rootServerRouteChildren: RootServerRouteChildren = { + ApiMiddlewareContextServerRoute: ApiMiddlewareContextServerRoute, +} +export const serverRouteTree = rootServerRouteImport + ._addFileChildren(rootServerRouteChildren) + ._addFileTypes() diff --git a/e2e/react-start/server-routes/src/router.tsx b/e2e/react-start/server-routes/src/router.tsx new file mode 100644 index 00000000000..c76eb0210cc --- /dev/null +++ b/e2e/react-start/server-routes/src/router.tsx @@ -0,0 +1,22 @@ +import { createRouter as createTanStackRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + }) + + return router +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/e2e/react-start/server-routes/src/routes/__root.tsx b/e2e/react-start/server-routes/src/routes/__root.tsx new file mode 100644 index 00000000000..64f5cbaf34c --- /dev/null +++ b/e2e/react-start/server-routes/src/routes/__root.tsx @@ -0,0 +1,82 @@ +/// +import * as React from 'react' +import { + HeadContent, + Link, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ], + links: [{ rel: 'stylesheet', href: appCss }], + }), + errorComponent: (props) => { + return ( + + + + ) + }, + notFoundComponent: () => , + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + ) +} + +const RouterDevtools = + process.env.NODE_ENV === 'production' + ? () => null // Render nothing in production + : React.lazy(() => + // Lazy load in development + import('@tanstack/react-router-devtools').then((res) => ({ + default: res.TanStackRouterDevtools, + })), + ) + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + +
+ + Home + +
+
+ {children} + + + + + ) +} diff --git a/e2e/react-start/server-routes/src/routes/api/middleware-context.ts b/e2e/react-start/server-routes/src/routes/api/middleware-context.ts new file mode 100644 index 00000000000..bc75e29126d --- /dev/null +++ b/e2e/react-start/server-routes/src/routes/api/middleware-context.ts @@ -0,0 +1,28 @@ +import { createServerFileRoute } from '@tanstack/react-start/server' +import { createMiddleware, json } from '@tanstack/react-start' + +const testParentMiddleware = createMiddleware({ type: 'request' }).server( + async ({ next }) => { + const result = await next({ context: { testParent: true } }) + return result + }, +) + +const testMiddleware = createMiddleware({ type: 'request' }) + .middleware([testParentMiddleware]) + .server(async ({ next }) => { + const result = await next({ context: { test: true } }) + return result + }) + +export const ServerRoute = createServerFileRoute('/api/middleware-context') + .middleware([testMiddleware]) + .methods({ + GET: ({ request, context }) => { + return json({ + url: request.url, + context: context, + expectedContext: { testParent: true, test: true }, + }) + }, + }) diff --git a/e2e/react-start/server-routes/src/routes/index.tsx b/e2e/react-start/server-routes/src/routes/index.tsx new file mode 100644 index 00000000000..ef9dc9ab85d --- /dev/null +++ b/e2e/react-start/server-routes/src/routes/index.tsx @@ -0,0 +1,20 @@ +import { Link, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Server Routes E2E tests

+
    +
  • + + server function middleware context is merged correctly + +
  • +
+
+ ) +} diff --git a/e2e/react-start/server-routes/src/routes/merge-server-fn-middleware-context.tsx b/e2e/react-start/server-routes/src/routes/merge-server-fn-middleware-context.tsx new file mode 100644 index 00000000000..5c358bb9204 --- /dev/null +++ b/e2e/react-start/server-routes/src/routes/merge-server-fn-middleware-context.tsx @@ -0,0 +1,68 @@ +import { createFileRoute } from '@tanstack/react-router' +import * as React from 'react' + +export const Route = createFileRoute('/merge-server-fn-middleware-context')({ + component: () => , +}) + +function MergeServerFnMiddlewareContext() { + const [apiResponse, setApiResponse] = React.useState(null) + + const fetchMiddlewareContext = async () => { + try { + const response = await fetch('/api/middleware-context') + const data = await response.json() + setApiResponse(data) + } catch (error) { + console.error('Error fetching middleware context:', error) + setApiResponse({ error: 'Failed to fetch' }) + } + } + + return ( +
+

Merge Server Function Middleware Context Test

+
+ + + {apiResponse && ( +
+

API Response:

+
+              {JSON.stringify(apiResponse, null, 2)}
+            
+ +
+

Context Verification:

+
+ {JSON.stringify(apiResponse.context, null, 2)} +
+ +
+ Has testParent:{' '} + {apiResponse.context?.testParent ? 'true' : 'false'} +
+ +
+ Has test: {apiResponse.context?.test ? 'true' : 'false'} +
+
+
+ )} +
+
+ ) +} diff --git a/e2e/react-start/server-routes/src/styles/app.css b/e2e/react-start/server-routes/src/styles/app.css new file mode 100644 index 00000000000..c53c8706654 --- /dev/null +++ b/e2e/react-start/server-routes/src/styles/app.css @@ -0,0 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/react-start/server-routes/tailwind.config.mjs b/e2e/react-start/server-routes/tailwind.config.mjs new file mode 100644 index 00000000000..e49f4eb776e --- /dev/null +++ b/e2e/react-start/server-routes/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}'], +} diff --git a/e2e/react-start/server-routes/tests/fixture.ts b/e2e/react-start/server-routes/tests/fixture.ts new file mode 100644 index 00000000000..abb7b1d564d --- /dev/null +++ b/e2e/react-start/server-routes/tests/fixture.ts @@ -0,0 +1,28 @@ +import { test as base, expect } from '@playwright/test' + +export interface TestFixtureOptions { + whitelistErrors: Array +} +export const test = base.extend({ + whitelistErrors: [[], { option: true }], + page: async ({ page, whitelistErrors }, use) => { + const errorMessages: Array = [] + page.on('console', (m) => { + if (m.type() === 'error') { + const text = m.text() + for (const whitelistError of whitelistErrors) { + if ( + (typeof whitelistError === 'string' && + text.includes(whitelistError)) || + (whitelistError instanceof RegExp && whitelistError.test(text)) + ) { + return + } + } + errorMessages.push(text) + } + }) + await use(page) + expect(errorMessages).toEqual([]) + }, +}) diff --git a/e2e/react-start/server-routes/tests/server-routes.spec.ts b/e2e/react-start/server-routes/tests/server-routes.spec.ts new file mode 100644 index 00000000000..098a40b2efe --- /dev/null +++ b/e2e/react-start/server-routes/tests/server-routes.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test' + +test('merge-server-fn-middleware-context', async ({ page }) => { + await page.goto('/merge-server-fn-middleware-context') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('test-middleware-context-btn').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('has-test-parent')).toContainText('true') + await expect(page.getByTestId('has-test')).toContainText('true') + + const contextResult = await page.getByTestId('context-result').textContent() + expect(contextResult).toContain('testParent') + expect(contextResult).toContain('test') +}) diff --git a/e2e/react-start/server-routes/tsconfig.json b/e2e/react-start/server-routes/tsconfig.json new file mode 100644 index 00000000000..d35a4b17f48 --- /dev/null +++ b/e2e/react-start/server-routes/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*.ts", "**/*.tsx", "public/script*.js"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true, + "types": ["vite/client"] + } +} diff --git a/e2e/react-start/server-routes/vite.config.ts b/e2e/react-start/server-routes/vite.config.ts new file mode 100644 index 00000000000..1df337cd40d --- /dev/null +++ b/e2e/react-start/server-routes/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' + +export default defineConfig({ + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart(), + ], +}) diff --git a/e2e/solid-start/server-routes/.gitignore b/e2e/solid-start/server-routes/.gitignore new file mode 100644 index 00000000000..a79d5cf1299 --- /dev/null +++ b/e2e/solid-start/server-routes/.gitignore @@ -0,0 +1,20 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output + +/build/ +/api/ +/server/build +/public/build +# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-start/server-routes/.prettierignore b/e2e/solid-start/server-routes/.prettierignore new file mode 100644 index 00000000000..2be5eaa6ece --- /dev/null +++ b/e2e/solid-start/server-routes/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/e2e/solid-start/server-routes/package.json b/e2e/solid-start/server-routes/package.json new file mode 100644 index 00000000000..c2bec1aa27a --- /dev/null +++ b/e2e/solid-start/server-routes/package.json @@ -0,0 +1,37 @@ +{ + "name": "tanstack-solid-start-e2e-server-routes", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "dev:e2e": "vite dev", + "build": "vite build && tsc --noEmit", + "start": "node .output/server/index.mjs", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/solid-router-devtools": "workspace:^", + "@tanstack/solid-start": "workspace:^", + "js-cookie": "^3.0.5", + "redaxios": "^0.5.1", + "solid-js": "^1.9.5", + "tailwind-merge": "^2.6.0", + "vite": "6.3.5", + "zod": "^3.24.2" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/js-cookie": "^3.0.6", + "@types/node": "^22.10.2", + "autoprefixer": "^10.4.20", + "combinate": "^1.1.11", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite-plugin-solid": "^2.11.6", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/solid-start/server-routes/playwright.config.ts b/e2e/solid-start/server-routes/playwright.config.ts new file mode 100644 index 00000000000..953f7bd4971 --- /dev/null +++ b/e2e/solid-start/server-routes/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test' +import { derivePort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +export const PORT = await derivePort(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: { + command: `pnpm build && VITE_SERVER_PORT=${PORT} PORT=${PORT} pnpm start`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-start/server-routes/postcss.config.mjs b/e2e/solid-start/server-routes/postcss.config.mjs new file mode 100644 index 00000000000..2e7af2b7f1a --- /dev/null +++ b/e2e/solid-start/server-routes/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/e2e/solid-start/server-routes/public/favicon.ico b/e2e/solid-start/server-routes/public/favicon.ico new file mode 100644 index 00000000000..1a1751676f7 Binary files /dev/null and b/e2e/solid-start/server-routes/public/favicon.ico differ diff --git a/e2e/solid-start/server-routes/public/favicon.png b/e2e/solid-start/server-routes/public/favicon.png new file mode 100644 index 00000000000..1e77bc06091 Binary files /dev/null and b/e2e/solid-start/server-routes/public/favicon.png differ diff --git a/e2e/solid-start/server-routes/src/components/DefaultCatchBoundary.tsx b/e2e/solid-start/server-routes/src/components/DefaultCatchBoundary.tsx new file mode 100644 index 00000000000..32aed20e675 --- /dev/null +++ b/e2e/solid-start/server-routes/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/server-routes/src/components/NotFound.tsx b/e2e/solid-start/server-routes/src/components/NotFound.tsx new file mode 100644 index 00000000000..eb0a968d393 --- /dev/null +++ b/e2e/solid-start/server-routes/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/server-routes/src/routeTree.gen.ts b/e2e/solid-start/server-routes/src/routeTree.gen.ts new file mode 100644 index 00000000000..17ffa3450c0 --- /dev/null +++ b/e2e/solid-start/server-routes/src/routeTree.gen.ts @@ -0,0 +1,127 @@ +/* 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 { createServerRootRoute } from '@tanstack/solid-start/server' + +import { Route as rootRouteImport } from './routes/__root' +import { Route as MergeServerFnMiddlewareContextRouteImport } from './routes/merge-server-fn-middleware-context' +import { Route as IndexRouteImport } from './routes/index' +import { ServerRoute as ApiMiddlewareContextServerRouteImport } from './routes/api/middleware-context' + +const rootServerRouteImport = createServerRootRoute() + +const MergeServerFnMiddlewareContextRoute = + MergeServerFnMiddlewareContextRouteImport.update({ + id: '/merge-server-fn-middleware-context', + path: '/merge-server-fn-middleware-context', + getParentRoute: () => rootRouteImport, + } as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiMiddlewareContextServerRoute = + ApiMiddlewareContextServerRouteImport.update({ + id: '/api/middleware-context', + path: '/api/middleware-context', + getParentRoute: () => rootServerRouteImport, + } as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/merge-server-fn-middleware-context': typeof MergeServerFnMiddlewareContextRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/merge-server-fn-middleware-context': typeof MergeServerFnMiddlewareContextRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/merge-server-fn-middleware-context': typeof MergeServerFnMiddlewareContextRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/merge-server-fn-middleware-context' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/merge-server-fn-middleware-context' + id: '__root__' | '/' | '/merge-server-fn-middleware-context' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + MergeServerFnMiddlewareContextRoute: typeof MergeServerFnMiddlewareContextRoute +} +export interface FileServerRoutesByFullPath { + '/api/middleware-context': typeof ApiMiddlewareContextServerRoute +} +export interface FileServerRoutesByTo { + '/api/middleware-context': typeof ApiMiddlewareContextServerRoute +} +export interface FileServerRoutesById { + __root__: typeof rootServerRouteImport + '/api/middleware-context': typeof ApiMiddlewareContextServerRoute +} +export interface FileServerRouteTypes { + fileServerRoutesByFullPath: FileServerRoutesByFullPath + fullPaths: '/api/middleware-context' + fileServerRoutesByTo: FileServerRoutesByTo + to: '/api/middleware-context' + id: '__root__' | '/api/middleware-context' + fileServerRoutesById: FileServerRoutesById +} +export interface RootServerRouteChildren { + ApiMiddlewareContextServerRoute: typeof ApiMiddlewareContextServerRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/merge-server-fn-middleware-context': { + id: '/merge-server-fn-middleware-context' + path: '/merge-server-fn-middleware-context' + fullPath: '/merge-server-fn-middleware-context' + preLoaderRoute: typeof MergeServerFnMiddlewareContextRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} +declare module '@tanstack/solid-start/server' { + interface ServerFileRoutesByPath { + '/api/middleware-context': { + id: '/api/middleware-context' + path: '/api/middleware-context' + fullPath: '/api/middleware-context' + preLoaderRoute: typeof ApiMiddlewareContextServerRouteImport + parentRoute: typeof rootServerRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + MergeServerFnMiddlewareContextRoute: MergeServerFnMiddlewareContextRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() +const rootServerRouteChildren: RootServerRouteChildren = { + ApiMiddlewareContextServerRoute: ApiMiddlewareContextServerRoute, +} +export const serverRouteTree = rootServerRouteImport + ._addFileChildren(rootServerRouteChildren) + ._addFileTypes() diff --git a/e2e/solid-start/server-routes/src/router.tsx b/e2e/solid-start/server-routes/src/router.tsx new file mode 100644 index 00000000000..c45bed4758c --- /dev/null +++ b/e2e/solid-start/server-routes/src/router.tsx @@ -0,0 +1,22 @@ +import { createRouter as createTanStackRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function createRouter() { + const router = createTanStackRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + }) + + return router +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/e2e/solid-start/server-routes/src/routes/__root.tsx b/e2e/solid-start/server-routes/src/routes/__root.tsx new file mode 100644 index 00000000000..c1855d436ac --- /dev/null +++ b/e2e/solid-start/server-routes/src/routes/__root.tsx @@ -0,0 +1,34 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ], + links: [{ rel: 'stylesheet', href: appCss }], + }), + errorComponent: (props) => { + return

{props.error.stack}

+ }, + notFoundComponent: () => , + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/e2e/solid-start/server-routes/src/routes/api/middleware-context.ts b/e2e/solid-start/server-routes/src/routes/api/middleware-context.ts new file mode 100644 index 00000000000..ef338345943 --- /dev/null +++ b/e2e/solid-start/server-routes/src/routes/api/middleware-context.ts @@ -0,0 +1,28 @@ +import { createServerFileRoute } from '@tanstack/solid-start/server' +import { createMiddleware, json } from '@tanstack/solid-start' + +const testParentMiddleware = createMiddleware({ type: 'request' }).server( + async ({ next }) => { + const result = await next({ context: { testParent: true } }) + return result + }, +) + +const testMiddleware = createMiddleware({ type: 'request' }) + .middleware([testParentMiddleware]) + .server(async ({ next }) => { + const result = await next({ context: { test: true } }) + return result + }) + +export const ServerRoute = createServerFileRoute('/api/middleware-context') + .middleware([testMiddleware]) + .methods({ + GET: ({ request, context }) => { + return json({ + url: request.url, + context: context, + expectedContext: { testParent: true, test: true }, + }) + }, + }) diff --git a/e2e/solid-start/server-routes/src/routes/index.tsx b/e2e/solid-start/server-routes/src/routes/index.tsx new file mode 100644 index 00000000000..8f4906f3d49 --- /dev/null +++ b/e2e/solid-start/server-routes/src/routes/index.tsx @@ -0,0 +1,20 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Server routes E2E tests

+
    +
  • + + server function middleware context is merged correctly + +
  • +
+
+ ) +} diff --git a/e2e/solid-start/server-routes/src/routes/merge-server-fn-middleware-context.tsx b/e2e/solid-start/server-routes/src/routes/merge-server-fn-middleware-context.tsx new file mode 100644 index 00000000000..a8aa2db9664 --- /dev/null +++ b/e2e/solid-start/server-routes/src/routes/merge-server-fn-middleware-context.tsx @@ -0,0 +1,68 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createSignal } from 'solid-js' + +export const Route = createFileRoute('/merge-server-fn-middleware-context')({ + component: () => , +}) + +function MergeServerFnMiddlewareContext() { + const [apiResponse, setApiResponse] = createSignal(null) + + const fetchMiddlewareContext = async () => { + try { + const response = await fetch('/api/middleware-context') + const data = await response.json() + setApiResponse(data) + } catch (error) { + console.error('Error fetching middleware context:', error) + setApiResponse({ error: 'Failed to fetch' }) + } + } + + return ( +
+

Merge Server Function Middleware Context Test

+
+ + + {apiResponse() && ( +
+

API Response:

+
+              {JSON.stringify(apiResponse(), null, 2)}
+            
+ +
+

Context Verification:

+
+ {JSON.stringify(apiResponse()?.context, null, 2)} +
+ +
+ Has testParent:{' '} + {apiResponse()?.context?.testParent ? 'true' : 'false'} +
+ +
+ Has test: {apiResponse()?.context?.test ? 'true' : 'false'} +
+
+
+ )} +
+
+ ) +} diff --git a/e2e/solid-start/server-routes/src/styles/app.css b/e2e/solid-start/server-routes/src/styles/app.css new file mode 100644 index 00000000000..c53c8706654 --- /dev/null +++ b/e2e/solid-start/server-routes/src/styles/app.css @@ -0,0 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/solid-start/server-routes/src/vite-env.d.ts b/e2e/solid-start/server-routes/src/vite-env.d.ts new file mode 100644 index 00000000000..0b2af560d60 --- /dev/null +++ b/e2e/solid-start/server-routes/src/vite-env.d.ts @@ -0,0 +1,4 @@ +declare module '*?url' { + const url: string + export default url +} diff --git a/e2e/solid-start/server-routes/tailwind.config.mjs b/e2e/solid-start/server-routes/tailwind.config.mjs new file mode 100644 index 00000000000..e49f4eb776e --- /dev/null +++ b/e2e/solid-start/server-routes/tailwind.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}'], +} diff --git a/e2e/solid-start/server-routes/tests/fixture.ts b/e2e/solid-start/server-routes/tests/fixture.ts new file mode 100644 index 00000000000..abb7b1d564d --- /dev/null +++ b/e2e/solid-start/server-routes/tests/fixture.ts @@ -0,0 +1,28 @@ +import { test as base, expect } from '@playwright/test' + +export interface TestFixtureOptions { + whitelistErrors: Array +} +export const test = base.extend({ + whitelistErrors: [[], { option: true }], + page: async ({ page, whitelistErrors }, use) => { + const errorMessages: Array = [] + page.on('console', (m) => { + if (m.type() === 'error') { + const text = m.text() + for (const whitelistError of whitelistErrors) { + if ( + (typeof whitelistError === 'string' && + text.includes(whitelistError)) || + (whitelistError instanceof RegExp && whitelistError.test(text)) + ) { + return + } + } + errorMessages.push(text) + } + }) + await use(page) + expect(errorMessages).toEqual([]) + }, +}) diff --git a/e2e/solid-start/server-routes/tests/server-routes.spec.ts b/e2e/solid-start/server-routes/tests/server-routes.spec.ts new file mode 100644 index 00000000000..098a40b2efe --- /dev/null +++ b/e2e/solid-start/server-routes/tests/server-routes.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test' + +test('merge-server-fn-middleware-context', async ({ page }) => { + await page.goto('/merge-server-fn-middleware-context') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('test-middleware-context-btn').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('has-test-parent')).toContainText('true') + await expect(page.getByTestId('has-test')).toContainText('true') + + const contextResult = await page.getByTestId('context-result').textContent() + expect(contextResult).toContain('testParent') + expect(contextResult).toContain('test') +}) diff --git a/e2e/solid-start/server-routes/tsconfig.json b/e2e/solid-start/server-routes/tsconfig.json new file mode 100644 index 00000000000..57ea27b2868 --- /dev/null +++ b/e2e/solid-start/server-routes/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*.ts", "**/*.tsx", "public/script*.js"], + "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 + } +} diff --git a/e2e/solid-start/server-routes/vite.config.ts b/e2e/solid-start/server-routes/vite.config.ts new file mode 100644 index 00000000000..bae1bfaad6e --- /dev/null +++ b/e2e/solid-start/server-routes/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' + +export default defineConfig({ + server: { + port: 3000, + }, + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart(), + ], +}) diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts index f42b7619ce4..bcdb8032f38 100644 --- a/packages/start-server-core/src/createStartHandler.ts +++ b/packages/start-server-core/src/createStartHandler.ts @@ -452,7 +452,14 @@ function executeMiddleware(middlewares: TODO, ctx: TODO) { // Allow the middleware to call the next middleware in the chain next: async (nextCtx: TODO) => { // Allow the caller to extend the context for the next middleware - const nextResult = await next({ ...ctx, ...nextCtx }) + const nextResult = await next({ + ...ctx, + ...nextCtx, + context: { + ...ctx.context, + ...(nextCtx?.context || {}), + }, + }) // Merge the result into the context\ return Object.assign(ctx, handleCtxResult(nextResult)) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3878ee802c..be9aaec9ea4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1488,6 +1488,79 @@ importers: specifier: ^5.1.4 version: 5.1.4(typescript@5.8.2)(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + e2e/react-start/server-routes: + 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 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 + react: + specifier: ^19.0.0 + version: 19.0.0 + react-dom: + specifier: ^19.0.0 + version: 19.0.0(react@19.0.0) + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 + vite: + specifier: 6.3.5 + version: 6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0) + zod: + specifier: ^3.24.2 + version: 3.25.57 + devDependencies: + '@playwright/test': + specifier: ^1.52.0 + version: 1.52.0 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 + '@types/node': + specifier: ^22.10.2 + version: 22.13.4 + '@types/react': + specifier: ^19.0.8 + version: 19.0.8 + '@types/react-dom': + specifier: ^19.0.3 + version: 19.0.3(@types/react@19.0.8) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.6.0(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.5.3) + combinate: + specifier: ^1.1.11 + version: 1.1.11 + postcss: + specifier: ^8.5.1 + version: 8.5.3 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + typescript: + specifier: ^5.7.2 + version: 5.8.3 + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + e2e/react-start/spa-mode: dependencies: '@tanstack/react-router': @@ -2502,6 +2575,70 @@ importers: specifier: ^5.1.4 version: 5.1.4(typescript@5.8.2)(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + e2e/solid-start/server-routes: + 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 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + solid-js: + specifier: ^1.9.5 + version: 1.9.5 + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 + vite: + specifier: 6.3.5 + version: 6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0) + zod: + specifier: ^3.24.2 + version: 3.25.57 + devDependencies: + '@playwright/test': + specifier: ^1.52.0 + version: 1.52.0 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 + '@types/node': + specifier: ^22.10.2 + version: 22.13.4 + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.5.3) + combinate: + specifier: ^1.1.11 + version: 1.1.11 + postcss: + specifier: ^8.5.1 + version: 8.5.3 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + typescript: + specifier: ^5.7.2 + version: 5.8.3 + vite-plugin-solid: + specifier: ^2.11.6 + version: 2.11.7(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)) + e2e/solid-start/spa-mode: dependencies: '@tanstack/solid-router':