Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions e2e/react-start/monorepo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
dist
.nitro
.output
port*.txt
test-results
34 changes: 34 additions & 0 deletions e2e/react-start/monorepo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "tanstack-react-start-e2e-monorepo",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"dev:e2e": "vite dev",
"build": "vite build && tsc --noEmit",
"preview": "vite preview",
"start": "pnpx srvx --prod -s ../client dist/server/server.js",
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
"@tanstack/react-start": "workspace:^",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"vite": "^7.3.1"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/router-e2e-utils": "workspace:^",
"@types/node": "^22.10.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"srvx": "^0.11.7",
"tailwindcss": "^4.1.18",
"typescript": "^5.7.2",
"vite-tsconfig-paths": "^5.1.4"
}
}
44 changes: 44 additions & 0 deletions e2e/react-start/monorepo/packages/analytics/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Analytics feature package.
// This simulates a separate package in a monorepo that uses
// startInstance.createServerFn and startInstance.createMiddleware
// to create server functions and middleware with fully-typed context
// flowing from the global request middleware — WITHOUT needing
// access to the app's routeTree.gen.ts or Register module augmentation.

import { startInstance } from '@repo/start-config'

// --- 1) Direct server function via startInstance.createServerFn ---
// context.locale and context.userId flow automatically from global request middleware
export const getAnalyticsContext = startInstance
.createServerFn({ method: 'GET' })
.handler(({ context }) => {
return {
locale: context.locale,
userId: context.userId,
}
})

// --- 2) Middleware created via startInstance.createMiddleware ---
// The global request context (locale, userId) is visible in .server() callback
const analyticsMiddleware = startInstance
.createMiddleware()
.server(({ next, context }) => {
const sessionId = `session-${context.userId}-${context.locale}` as string
return next({
context: {
sessionId,
},
})
})

// --- 3) Server function that uses local middleware (extends global context) ---
export const getAnalyticsSession = startInstance
.createServerFn({ method: 'GET' })
.middleware([analyticsMiddleware])
.handler(({ context }) => {
return {
locale: context.locale,
userId: context.userId,
sessionId: context.sessionId,
}
})
31 changes: 31 additions & 0 deletions e2e/react-start/monorepo/packages/start-config/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Shared start configuration for the monorepo.
// This simulates an external package that defines createStart() with
// global request middleware, then exports the startInstance so that
// other packages can use startInstance.createServerFn / startInstance.createMiddleware
// to get fully-typed context WITHOUT needing access to the app's routeTree.gen.ts.

import { createMiddleware, createStart } from '@tanstack/react-start'

export const localeMiddleware = createMiddleware({ type: 'request' }).server(
({ next }) => {
return next({
context: {
locale: 'en-us' as string,
},
})
},
)

export const authMiddleware = createMiddleware({ type: 'request' }).server(
({ next }) => {
return next({
context: {
userId: 'user-42' as string,
},
})
},
)

export const startInstance = createStart(() => ({
requestMiddleware: [localeMiddleware, authMiddleware],
}))
35 changes: 35 additions & 0 deletions e2e/react-start/monorepo/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { defineConfig, devices } from '@playwright/test'
import { getTestServerPort } from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

export const PORT = await getTestServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
workers: 1,

reporter: [['line']],

use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL,
},

webServer: {
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'] },
},
],
})
105 changes: 105 additions & 0 deletions e2e/react-start/monorepo/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* 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 AnalyticsSessionRouteImport } from './routes/analytics-session'
import { Route as AnalyticsContextRouteImport } from './routes/analytics-context'
import { Route as IndexRouteImport } from './routes/index'

const AnalyticsSessionRoute = AnalyticsSessionRouteImport.update({
id: '/analytics-session',
path: '/analytics-session',
getParentRoute: () => rootRouteImport,
} as any)
const AnalyticsContextRoute = AnalyticsContextRouteImport.update({
id: '/analytics-context',
path: '/analytics-context',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/analytics-context': typeof AnalyticsContextRoute
'/analytics-session': typeof AnalyticsSessionRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/analytics-context': typeof AnalyticsContextRoute
'/analytics-session': typeof AnalyticsSessionRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/analytics-context': typeof AnalyticsContextRoute
'/analytics-session': typeof AnalyticsSessionRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/analytics-context' | '/analytics-session'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/analytics-context' | '/analytics-session'
id: '__root__' | '/' | '/analytics-context' | '/analytics-session'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AnalyticsContextRoute: typeof AnalyticsContextRoute
AnalyticsSessionRoute: typeof AnalyticsSessionRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/analytics-session': {
id: '/analytics-session'
path: '/analytics-session'
fullPath: '/analytics-session'
preLoaderRoute: typeof AnalyticsSessionRouteImport
parentRoute: typeof rootRouteImport
}
'/analytics-context': {
id: '/analytics-context'
path: '/analytics-context'
fullPath: '/analytics-context'
preLoaderRoute: typeof AnalyticsContextRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AnalyticsContextRoute: AnalyticsContextRoute,
AnalyticsSessionRoute: AnalyticsSessionRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

import type { getRouter } from './router.tsx'
import type { startInstance } from './start.ts'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
config: Awaited<ReturnType<typeof startInstance.getOptions>>
}
}
12 changes: 12 additions & 0 deletions e2e/react-start/monorepo/src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function getRouter() {
const router = createRouter({
routeTree,
defaultPreload: 'intent',
scrollRestoration: true,
})

return router
}
52 changes: 52 additions & 0 deletions e2e/react-start/monorepo/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// <reference types="vite/client" />
import * as React from 'react'
import {
HeadContent,
Link,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/react-router'
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 }],
}),
component: RootComponent,
})

function RootComponent() {
return (
<html>
<head>
<HeadContent />
</head>
<body>
<div className="p-2 flex gap-2 text-lg">
<Link
to="/"
activeProps={{
className: 'font-bold',
}}
activeOptions={{ exact: true }}
>
Home
</Link>
</div>
<hr />
<Outlet />
<Scripts />
</body>
</html>
)
}
32 changes: 32 additions & 0 deletions e2e/react-start/monorepo/src/routes/analytics-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { getAnalyticsContext } from '@repo/analytics'

export const Route = createFileRoute('/analytics-context')({
component: AnalyticsContextComponent,
loader: () => getAnalyticsContext(),
})

function AnalyticsContextComponent() {
const data = Route.useLoaderData()

return (
<div className="p-8">
<h1 className="font-bold text-lg mb-4">Analytics Context</h1>
<p className="text-gray-600 mb-4">
This route calls getAnalyticsContext() from @repo/analytics. The server
function was created via startInstance.createServerFn() and
automatically receives context from global request middleware (locale,
userId) without needing access to routeTree.gen.ts.
</p>
<div className="space-y-2">
<div>
Locale: <span data-testid="locale">{data.locale}</span>
</div>
<div>
User ID: <span data-testid="userId">{data.userId}</span>
</div>
</div>
</div>
)
}
35 changes: 35 additions & 0 deletions e2e/react-start/monorepo/src/routes/analytics-session.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as React from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { getAnalyticsSession } from '@repo/analytics'

export const Route = createFileRoute('/analytics-session')({
component: AnalyticsSessionComponent,
loader: () => getAnalyticsSession(),
})

function AnalyticsSessionComponent() {
const data = Route.useLoaderData()

return (
<div className="p-8">
<h1 className="font-bold text-lg mb-4">Analytics Session</h1>
<p className="text-gray-600 mb-4">
This route calls getAnalyticsSession() from @repo/analytics. The server
function uses a local middleware created via
startInstance.createMiddleware(), which extends the global request
context with a sessionId — all without needing routeTree.gen.ts.
</p>
<div className="space-y-2">
<div>
Locale: <span data-testid="locale">{data.locale}</span>
</div>
<div>
User ID: <span data-testid="userId">{data.userId}</span>
</div>
<div>
Session ID: <span data-testid="sessionId">{data.sessionId}</span>
</div>
</div>
</div>
)
}
Loading
Loading