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
7 changes: 7 additions & 0 deletions e2e/react-start/rsc-hmr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
dist-vite-*
dist-rsbuild-*
test-results
playwright-report
port*.txt
61 changes: 61 additions & 0 deletions e2e/react-start/rsc-hmr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "tanstack-react-start-e2e-rsc-hmr",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "pnpm dev:vite --port 3000",
"dev:e2e": "pnpm dev:vite",
"dev:vite": "vite dev",
"dev:rsbuild": "rsbuild dev",
"build": "pnpm build:vite",
"build:vite": "vite build && tsc --noEmit",
"build:rsbuild": "rsbuild build && tsc --noEmit",
"test:e2e": "MODE=dev playwright test --project=chromium"
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
"@tanstack/react-start": "workspace:^",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.61.0",
"@rsbuild/core": "^2.0.11",
"@rsbuild/plugin-react": "^2.0.0",
"@tanstack/router-e2e-utils": "workspace:^",
"@types/node": "^22.10.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^6.0.1",
"@vitejs/plugin-rsc": "^0.5.30",
"react-server-dom-rspack": "^0.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14"
},
"nx": {
"targets": {
"test:e2e": {
"parallelism": false
},
"test:e2e--vite-ssr": {
"parallelism": false
},
"test:e2e--rsbuild-ssr": {
"parallelism": false
}
},
"metadata": {
"playwrightModes": [
{
"toolchain": "vite",
"mode": "ssr"
},
{
"toolchain": "rsbuild",
"mode": "ssr"
}
]
}
}
}
50 changes: 50 additions & 0 deletions e2e/react-start/rsc-hmr/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { defineConfig, devices } from '@playwright/test'
import { getTestServerPort } from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

const toolchain = process.env.E2E_TOOLCHAIN ?? 'vite'
const e2ePortKey =
process.env.E2E_PORT_KEY ?? `${packageJson.name}-${toolchain}`
const distDir = process.env.E2E_DIST_DIR ?? 'dist'
const viteBundledDev = process.env.E2E_VITE_BUNDLED_DEV === 'true'
const PORT = await getTestServerPort(e2ePortKey)
const baseURL = `http://localhost:${PORT}`
const devCommand =
toolchain === 'rsbuild' ? 'pnpm dev:rsbuild' : 'pnpm dev:vite'

export default defineConfig({
testDir: './tests',
workers: 1,
reporter: [['line']],

globalSetup: './tests/setup/global.setup.ts',
globalTeardown: './tests/setup/global.teardown.ts',

use: {
baseURL,
},

webServer: {
command: `${devCommand} --port ${PORT}`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
env: {
VITE_NODE_ENV: 'test',
PORT: String(PORT),
E2E_TOOLCHAIN: toolchain,
E2E_DIST_DIR: distDir,
E2E_PORT_KEY: e2ePortKey,
E2E_VITE_BUNDLED_DEV: String(viteBundledDev),
},
},

projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
})
24 changes: 24 additions & 0 deletions e2e/react-start/rsc-hmr/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineConfig } from '@rsbuild/core'
import { pluginReact } from '@rsbuild/plugin-react'
import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild'

const outDir = process.env.E2E_DIST_DIR ?? 'dist'

export default defineConfig({
plugins: [
pluginReact({ splitChunks: false }),
tanstackStart({
rsc: {
enabled: true,
},
}),
],
output: {
distPath: {
root: outDir,
},
},
server: {
port: 3000,
},
})
127 changes: 127 additions & 0 deletions e2e/react-start/rsc-hmr/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -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 { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as CoLocatedRouteImport } from './routes/co-located'
import { Route as CoLocatedCssModuleRouteImport } from './routes/co-located-css-module'
import { Route as SeparateFileRouteImport } from './routes/separate-file'

const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const CoLocatedRoute = CoLocatedRouteImport.update({
id: '/co-located',
path: '/co-located',
getParentRoute: () => rootRouteImport,
} as any)
const CoLocatedCssModuleRoute = CoLocatedCssModuleRouteImport.update({
id: '/co-located-css-module',
path: '/co-located-css-module',
getParentRoute: () => rootRouteImport,
} as any)
const SeparateFileRoute = SeparateFileRouteImport.update({
id: '/separate-file',
path: '/separate-file',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/co-located': typeof CoLocatedRoute
'/co-located-css-module': typeof CoLocatedCssModuleRoute
'/separate-file': typeof SeparateFileRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/co-located': typeof CoLocatedRoute
'/co-located-css-module': typeof CoLocatedCssModuleRoute
'/separate-file': typeof SeparateFileRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/co-located': typeof CoLocatedRoute
'/co-located-css-module': typeof CoLocatedCssModuleRoute
'/separate-file': typeof SeparateFileRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/co-located' | '/co-located-css-module' | '/separate-file'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/co-located' | '/co-located-css-module' | '/separate-file'
id:
| '__root__'
| '/'
| '/co-located'
| '/co-located-css-module'
| '/separate-file'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
CoLocatedRoute: typeof CoLocatedRoute
CoLocatedCssModuleRoute: typeof CoLocatedCssModuleRoute
SeparateFileRoute: typeof SeparateFileRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/co-located': {
id: '/co-located'
path: '/co-located'
fullPath: '/co-located'
preLoaderRoute: typeof CoLocatedRouteImport
parentRoute: typeof rootRouteImport
}
'/co-located-css-module': {
id: '/co-located-css-module'
path: '/co-located-css-module'
fullPath: '/co-located-css-module'
preLoaderRoute: typeof CoLocatedCssModuleRouteImport
parentRoute: typeof rootRouteImport
}
'/separate-file': {
id: '/separate-file'
path: '/separate-file'
fullPath: '/separate-file'
preLoaderRoute: typeof SeparateFileRouteImport
parentRoute: typeof rootRouteImport
}
}
}

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

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

export function getRouter() {
return createRouter({
routeTree,
defaultPreload: false,
scrollRestoration: true,
})
}
10 changes: 10 additions & 0 deletions e2e/react-start/rsc-hmr/src/routes/-separateFile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createServerFn } from '@tanstack/react-start'
import { renderServerComponent } from '@tanstack/react-start/rsc'

export const getSeparateFileContent = createServerFn({
method: 'GET',
}).handler(async () => {
return renderServerComponent(
<p data-testid="separate-file-server-content">server-rendered content</p>,
)
})
52 changes: 52 additions & 0 deletions e2e/react-start/rsc-hmr/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// <reference types="vite/client" />
import {
ClientOnly,
HeadContent,
Link,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/react-router'

export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
],
}),
component: RootComponent,
})

function RootComponent() {
return (
<html>
<head>
<HeadContent />
</head>
<body>
<nav data-testid="main-nav">
<Link to="/" data-testid="nav-home">
Home
</Link>{' '}
<Link to="/co-located" data-testid="nav-co-located">
Co-located
</Link>{' '}
<Link to="/co-located-css-module" data-testid="nav-co-located-css">
Co-located + CSS module
</Link>{' '}
<Link to="/separate-file" data-testid="nav-separate-file">
Separate file
</Link>
</nav>

<ClientOnly>
<p data-testid="hydrated">hydrated</p>
</ClientOnly>

<Outlet />
<Scripts />
</body>
</html>
)
}
45 changes: 45 additions & 0 deletions e2e/react-start/rsc-hmr/src/routes/co-located-css-module.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { renderServerComponent } from '@tanstack/react-start/rsc'
import { useState } from 'react'
import styles from './coLocatedCssModule.module.css'

// Same co-location as ./co-located.tsx, plus a CSS module import. The upstream
// guard decides whether to suppress client HMR by classifying importers as CSS
// vs non-CSS, so a route that has both kinds of importer is the case most likely
// to regress -- in either direction: the component must still Fast Refresh, and
// a CSS-only edit must still hot-swap without a full reload.
const getServerContent = createServerFn({ method: 'GET' }).handler(async () => {
return renderServerComponent(
<p data-testid="css-module-server-content">server-rendered content</p>,
)
})

export const Route = createFileRoute('/co-located-css-module')({
loader: () => getServerContent(),
component: CoLocatedCssModuleComponent,
})

function CoLocatedCssModuleComponent() {
const Server = Route.useLoaderData()
const [count, setCount] = useState(0)

return (
<main>
<h1 className={styles.marker} data-testid="css-module-marker">
css-module-baseline
</h1>

<p data-testid="css-module-count">Count: {count}</p>
<button
type="button"
data-testid="css-module-increment"
onClick={() => setCount((c) => c + 1)}
>
Increment
</button>

{Server}
</main>
)
}
Loading