From b2b64001552f1c8532c6b1057036adeec7dc8914 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Mon, 13 Apr 2026 15:48:56 -0400 Subject: [PATCH 1/2] test: add e2e fixtures for new TanStack Start RSC See: - https://tanstack.com/blog/react-server-components - https://tanstack.com/start/latest/docs/framework/react/guide/server-components --- .../test/e2e/build.test.ts | 30 ++++++++ .../test/fixtures/start-basic-rc/package.json | 7 +- .../start-basic-rc/src/components/Counter.tsx | 18 +++++ .../start-basic-rc/src/routeTree.gen.ts | 42 +++++++++++ .../start-basic-rc/src/routes/__root.tsx | 16 ++++ .../start-basic-rc/src/routes/rsc-basic.tsx | 27 +++++++ .../src/routes/rsc-composite.tsx | 26 +++++++ .../fixtures/start-basic-rc/src/utils/rsc.tsx | 75 +++++++++++++++++++ .../fixtures/start-basic-rc/vite.config.ts | 3 + 9 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/components/Counter.tsx create mode 100644 packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/rsc-basic.tsx create mode 100644 packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/rsc-composite.tsx create mode 100644 packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/utils/rsc.tsx diff --git a/packages/vite-plugin-tanstack-start/test/e2e/build.test.ts b/packages/vite-plugin-tanstack-start/test/e2e/build.test.ts index 06048bd26..26669ccc4 100644 --- a/packages/vite-plugin-tanstack-start/test/e2e/build.test.ts +++ b/packages/vite-plugin-tanstack-start/test/e2e/build.test.ts @@ -71,4 +71,34 @@ describe.runIf(isSupportedNode && !isWindows)('build output when deployed to Net await page.click('text=sunt aut facere repe') await page.waitForSelector('text=quia et suscipit suscipit') }) + + test('Renders basic RSC page with server components', async () => { + const response = await page.goto(`${baseUrl}/rsc-basic`) + expect(response?.status()).toBe(200) + // The greeting is rendered by a server component via renderServerComponent + await page.waitForSelector('[data-testid="rsc-greeting"]') + const greetingText = await page.textContent('[data-testid="rsc-greeting"]') + expect(greetingText).toContain('Hello from a Server Component!') + // The user list is also rendered as a server component with data fetching + await page.waitForSelector('[data-testid="rsc-user-list"]') + const userListText = await page.textContent('[data-testid="rsc-user-list"]') + expect(userListText).toContain('Ervin Howell') + }) + + test('Renders composite RSC page with client component children', async () => { + const response = await page.goto(`${baseUrl}/rsc-composite`) + expect(response?.status()).toBe(200) + // The card shell is rendered on the server via createCompositeComponent + await page.waitForSelector('[data-testid="rsc-card"]') + const cardText = await page.textContent('[data-testid="rsc-card"]') + expect(cardText).toContain('Server-Rendered Card') + // The counter is a client component passed as a children slot + await page.waitForSelector('[data-testid="client-counter"]') + const initialValue = await page.textContent('[data-testid="counter-value"]') + expect(initialValue).toBe('0') + // Verify client interactivity works within the server-rendered shell + await page.click('[data-testid="counter-button"]') + const updatedValue = await page.textContent('[data-testid="counter-value"]') + expect(updatedValue).toBe('1') + }) }) diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/package.json b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/package.json index 078da2d0c..5398d96fc 100644 --- a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/package.json +++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/package.json @@ -10,9 +10,9 @@ "start": "node .output/server/index.mjs" }, "dependencies": { - "@tanstack/react-router": "^1.154.5", - "@tanstack/react-router-devtools": "^1.154.5", - "@tanstack/react-start": "^1.154.5", + "@tanstack/react-router": "^1.168.19", + "@tanstack/react-router-devtools": "^1.166.13", + "@tanstack/react-start": "^1.167.34", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwind-merge": "^2.6.0", @@ -21,6 +21,7 @@ "devDependencies": { "@netlify/vite-plugin-tanstack-start": "file:../../..", "@tailwindcss/vite": "^4.1.18", + "@vitejs/plugin-rsc": "^0.5.24", "@types/node": "^22.5.4", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/components/Counter.tsx b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/components/Counter.tsx new file mode 100644 index 000000000..9414aa38d --- /dev/null +++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/components/Counter.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' + +/** + * A client-only interactive counter component. + * Used as a child slot inside a server-rendered composite component + * to demonstrate RSC composition with client interactivity. + */ +export function Counter() { + const [count, setCount] = React.useState(0) + return ( +
+

Client-side counter: {count}

+ +
+ ) +} diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routeTree.gen.ts b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routeTree.gen.ts index 0bd698a66..abee081ae 100644 --- a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routeTree.gen.ts +++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routeTree.gen.ts @@ -12,6 +12,8 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as UsersRouteImport } from './routes/users' import { Route as RedirectRouteImport } from './routes/redirect' import { Route as PostsRouteImport } from './routes/posts' +import { Route as RscBasicRouteImport } from './routes/rsc-basic' +import { Route as RscCompositeRouteImport } from './routes/rsc-composite' import { Route as DeferredRouteImport } from './routes/deferred' import { Route as CustomScriptDotjsRouteImport } from './routes/customScript[.]js' import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout' @@ -42,6 +44,16 @@ const PostsRoute = PostsRouteImport.update({ path: '/posts', getParentRoute: () => rootRouteImport, } as any) +const RscBasicRoute = RscBasicRouteImport.update({ + id: '/rsc-basic', + path: '/rsc-basic', + getParentRoute: () => rootRouteImport, +} as any) +const RscCompositeRoute = RscCompositeRouteImport.update({ + id: '/rsc-composite', + path: '/rsc-composite', + getParentRoute: () => rootRouteImport, +} as any) const DeferredRoute = DeferredRouteImport.update({ id: '/deferred', path: '/deferred', @@ -120,6 +132,8 @@ export interface FileRoutesByFullPath { '/deferred': typeof DeferredRoute '/posts': typeof PostsRouteWithChildren '/redirect': typeof RedirectRoute + '/rsc-basic': typeof RscBasicRoute + '/rsc-composite': typeof RscCompositeRoute '/users': typeof UsersRouteWithChildren '/api/users': typeof ApiUsersRouteWithChildren '/posts/$postId': typeof PostsPostIdRoute @@ -136,6 +150,8 @@ export interface FileRoutesByTo { '/customScript.js': typeof CustomScriptDotjsRoute '/deferred': typeof DeferredRoute '/redirect': typeof RedirectRoute + '/rsc-basic': typeof RscBasicRoute + '/rsc-composite': typeof RscCompositeRoute '/api/users': typeof ApiUsersRouteWithChildren '/posts/$postId': typeof PostsPostIdRoute '/users/$userId': typeof UsersUserIdRoute @@ -154,6 +170,8 @@ export interface FileRoutesById { '/deferred': typeof DeferredRoute '/posts': typeof PostsRouteWithChildren '/redirect': typeof RedirectRoute + '/rsc-basic': typeof RscBasicRoute + '/rsc-composite': typeof RscCompositeRoute '/users': typeof UsersRouteWithChildren '/_pathlessLayout/_nested-layout': typeof PathlessLayoutNestedLayoutRouteWithChildren '/api/users': typeof ApiUsersRouteWithChildren @@ -174,6 +192,8 @@ export interface FileRouteTypes { | '/deferred' | '/posts' | '/redirect' + | '/rsc-basic' + | '/rsc-composite' | '/users' | '/api/users' | '/posts/$postId' @@ -190,6 +210,8 @@ export interface FileRouteTypes { | '/customScript.js' | '/deferred' | '/redirect' + | '/rsc-basic' + | '/rsc-composite' | '/api/users' | '/posts/$postId' | '/users/$userId' @@ -207,6 +229,8 @@ export interface FileRouteTypes { | '/deferred' | '/posts' | '/redirect' + | '/rsc-basic' + | '/rsc-composite' | '/users' | '/_pathlessLayout/_nested-layout' | '/api/users' @@ -227,6 +251,8 @@ export interface RootRouteChildren { DeferredRoute: typeof DeferredRoute PostsRoute: typeof PostsRouteWithChildren RedirectRoute: typeof RedirectRoute + RscBasicRoute: typeof RscBasicRoute + RscCompositeRoute: typeof RscCompositeRoute UsersRoute: typeof UsersRouteWithChildren ApiUsersRoute: typeof ApiUsersRouteWithChildren PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute @@ -262,6 +288,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DeferredRouteImport parentRoute: typeof rootRouteImport } + '/rsc-basic': { + id: '/rsc-basic' + path: '/rsc-basic' + fullPath: '/rsc-basic' + preLoaderRoute: typeof RscBasicRouteImport + parentRoute: typeof rootRouteImport + } + '/rsc-composite': { + id: '/rsc-composite' + path: '/rsc-composite' + fullPath: '/rsc-composite' + preLoaderRoute: typeof RscCompositeRouteImport + parentRoute: typeof rootRouteImport + } '/customScript.js': { id: '/customScript.js' path: '/customScript.js' @@ -429,6 +469,8 @@ const rootRouteChildren: RootRouteChildren = { DeferredRoute: DeferredRoute, PostsRoute: PostsRouteWithChildren, RedirectRoute: RedirectRoute, + RscBasicRoute: RscBasicRoute, + RscCompositeRoute: RscCompositeRoute, UsersRoute: UsersRouteWithChildren, ApiUsersRoute: ApiUsersRouteWithChildren, PostsPostIdDeepRoute: PostsPostIdDeepRoute, diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/__root.tsx b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/__root.tsx index 346409e9d..ded2f3ba6 100644 --- a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/__root.tsx +++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/__root.tsx @@ -111,6 +111,22 @@ function RootDocument({ children }: { children: React.ReactNode }) { > Deferred {' '} + + RSC Basic + {' '} + + RSC Composite + {' '} { + const [greeting, users] = await Promise.all([ + fetchRscGreeting(), + fetchRscUserList(), + ]) + return { ...greeting, ...users } + }, + component: RscBasicPage, +}) + +function RscBasicPage() { + const { Greeting, UserList } = Route.useLoaderData() + return ( +
+

RSC Basic Demo

+

This page demonstrates basic React Server Components rendered via renderServerComponent.

+
+ {Greeting} +
+ {UserList} +
+ ) +} diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/rsc-composite.tsx b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/rsc-composite.tsx new file mode 100644 index 000000000..6d31c820c --- /dev/null +++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/rsc-composite.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/react-router' +import { CompositeComponent } from '@tanstack/react-start/rsc' +import { fetchRscCard } from '~/utils/rsc' +import { Counter } from '~/components/Counter' + +export const Route = createFileRoute('/rsc-composite')({ + loader: () => fetchRscCard(), + component: RscCompositePage, +}) + +function RscCompositePage() { + const { Card } = Route.useLoaderData() + return ( +
+

RSC Composite Demo

+

+ This page demonstrates createCompositeComponent with children slots. + The card shell is rendered on the server, while the counter inside is a client component. +

+
+ + + +
+ ) +} diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/utils/rsc.tsx b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/utils/rsc.tsx new file mode 100644 index 000000000..1c39560a3 --- /dev/null +++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/utils/rsc.tsx @@ -0,0 +1,75 @@ +import * as React from 'react' +import { createServerFn } from '@tanstack/react-start' +import { + renderServerComponent, + createCompositeComponent, +} from '@tanstack/react-start/rsc' + +/** + * A basic server function that renders a server component using renderServerComponent. + * The component fetches data on the server and renders it — the fetch logic never + * reaches the client bundle. + */ +export const fetchRscGreeting = createServerFn().handler(async () => { + // Simulate server-only work (e.g., DB query, secret API call) + const timestamp = new Date().toISOString() + + const Greeting = await renderServerComponent( +
+

Hello from a Server Component!

+

This was rendered on the server at {timestamp}.

+

+ This content comes from a React Server Component. The rendering logic + and any server-only dependencies never reach the client bundle. +

+
, + ) + + return { Greeting } +}) + +/** + * A server function that fetches data and renders it as a server component. + * Demonstrates data fetching colocated within an RSC. + */ +export const fetchRscUserList = createServerFn().handler(async () => { + const res = await fetch('https://jsonplaceholder.typicode.com/users') + if (!res.ok) { + throw new Error('Failed to fetch users for RSC') + } + const users = (await res.json()) as Array<{ id: number; name: string; email: string }> + + const UserList = await renderServerComponent( +
+

Users (Server Component)

+ +
, + ) + + return { UserList } +}) + +/** + * A composite component that accepts children slots. + * The server renders the outer shell, and client components can be + * passed as children. + */ +export const fetchRscCard = createServerFn().handler(async () => { + const src = await createCompositeComponent( + (props: { children?: React.ReactNode }) => ( +
+

Server-Rendered Card

+

This card shell is rendered on the server.

+
{props.children}
+
+ ), + ) + + return { Card: src } +}) diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/vite.config.ts b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/vite.config.ts index 081d950c0..50160d26e 100644 --- a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/vite.config.ts +++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/vite.config.ts @@ -2,6 +2,7 @@ import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import { defineConfig } from "vite"; import tsConfigPaths from "vite-tsconfig-paths"; import viteReact from "@vitejs/plugin-react"; +import rsc from "@vitejs/plugin-rsc"; import tailwindcss from "@tailwindcss/vite"; import netlify from "@netlify/vite-plugin-tanstack-start"; @@ -16,7 +17,9 @@ export default defineConfig({ }), tanstackStart({ srcDirectory: "src", + rsc: { enabled: true }, }), + rsc(), viteReact(), netlify(), ], From 4210df15c8d241561c89a75cf661824782b34ae2 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Mon, 13 Apr 2026 17:40:57 -0400 Subject: [PATCH 2/2] fix: bump to fixed tanstack start react --- .../test/fixtures/start-basic-rc/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/package.json b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/package.json index 5398d96fc..9944e0118 100644 --- a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/package.json +++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/package.json @@ -12,7 +12,7 @@ "dependencies": { "@tanstack/react-router": "^1.168.19", "@tanstack/react-router-devtools": "^1.166.13", - "@tanstack/react-start": "^1.167.34", + "@tanstack/react-start": "^1.167.37", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwind-merge": "^2.6.0",