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
5 changes: 5 additions & 0 deletions .changeset/fix-rsc-stale-css-hmr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/react-start-rsc': patch
---

Fix stale CSS surviving HMR edits in dev by skipping preinit outside production
5 changes: 5 additions & 0 deletions e2e/react-start/rsc-hmr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
dist
test-results
playwright-report
port*.txt
24 changes: 24 additions & 0 deletions e2e/react-start/rsc-hmr/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// @ts-check

import tsParser from '@typescript-eslint/parser'
import startPlugin from '@tanstack/eslint-plugin-start'

export default [
{
files: ['src/**/*.{ts,tsx}'],
languageOptions: {
parser: tsParser,
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
'@tanstack/start': startPlugin,
},
rules: {
'@tanstack/start/no-client-code-in-server-component': 'error',
'@tanstack/start/no-async-client-component': 'error',
},
},
]
35 changes: 35 additions & 0 deletions e2e/react-start/rsc-hmr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "tanstack-react-start-e2e-rsc-hmr",
"private": true,
"sideEffects": [
"**/*.css"
],
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"dev:e2e": "vite dev --port $PORT",
"build": "vite build && tsc --noEmit",
"test:e2e": "MODE=dev playwright test --project=chromium"
},
"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",
"@tanstack/eslint-plugin-start": "workspace:^",
"@tanstack/router-e2e-utils": "workspace:^",
"@types/node": "^22.10.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@typescript-eslint/parser": "^8.23.0",
"@vitejs/plugin-react": "^6.0.1",
"@vitejs/plugin-rsc": "^0.5.20",
"eslint": "^9.22.0",
"typescript": "^5.7.2",
"vite": "^8.0.0"
}
}
39 changes: 39 additions & 0 deletions e2e/react-start/rsc-hmr/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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}`

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: `pnpm dev:e2e`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
env: {
VITE_NODE_ENV: 'test',
PORT: String(PORT),
},
},

projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
})
104 changes: 104 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,104 @@
/* 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 RscHmrGlobalCssRouteImport } from './routes/rsc-hmr-global-css'
import { Route as RscHmrCssModulesRouteImport } from './routes/rsc-hmr-css-modules'
import { Route as IndexRouteImport } from './routes/index'

const RscHmrGlobalCssRoute = RscHmrGlobalCssRouteImport.update({
id: '/rsc-hmr-global-css',
path: '/rsc-hmr-global-css',
getParentRoute: () => rootRouteImport,
} as any)
const RscHmrCssModulesRoute = RscHmrCssModulesRouteImport.update({
id: '/rsc-hmr-css-modules',
path: '/rsc-hmr-css-modules',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/rsc-hmr-css-modules': typeof RscHmrCssModulesRoute
'/rsc-hmr-global-css': typeof RscHmrGlobalCssRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/rsc-hmr-css-modules': typeof RscHmrCssModulesRoute
'/rsc-hmr-global-css': typeof RscHmrGlobalCssRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/rsc-hmr-css-modules': typeof RscHmrCssModulesRoute
'/rsc-hmr-global-css': typeof RscHmrGlobalCssRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/rsc-hmr-css-modules' | '/rsc-hmr-global-css'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/rsc-hmr-css-modules' | '/rsc-hmr-global-css'
id: '__root__' | '/' | '/rsc-hmr-css-modules' | '/rsc-hmr-global-css'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
RscHmrCssModulesRoute: typeof RscHmrCssModulesRoute
RscHmrGlobalCssRoute: typeof RscHmrGlobalCssRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/rsc-hmr-global-css': {
id: '/rsc-hmr-global-css'
path: '/rsc-hmr-global-css'
fullPath: '/rsc-hmr-global-css'
preLoaderRoute: typeof RscHmrGlobalCssRouteImport
parentRoute: typeof rootRouteImport
}
'/rsc-hmr-css-modules': {
id: '/rsc-hmr-css-modules'
path: '/rsc-hmr-css-modules'
fullPath: '/rsc-hmr-css-modules'
preLoaderRoute: typeof RscHmrCssModulesRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
RscHmrCssModulesRoute: RscHmrCssModulesRoute,
RscHmrGlobalCssRoute: RscHmrGlobalCssRoute,
}
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>>
}
}
12 changes: 12 additions & 0 deletions e2e/react-start/rsc-hmr/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,
scrollRestoration: true,
defaultPreload: 'intent',
})

return router
}
79 changes: 79 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,79 @@
/// <reference types="vite/client" />
import {
ClientOnly,
HeadContent,
Link,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/react-router'
import type { ReactNode } from 'react'

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

function RootComponent() {
return (
<RootDocument>
<RootContent />
</RootDocument>
)
}

function RootDocument({ children }: { children: ReactNode }) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
)
}

function RootContent() {
const navLinks = [
{ testId: 'nav-home', to: '/', label: 'Home' },
{
testId: 'nav-global-css',
to: '/rsc-hmr-global-css',
label: 'Global CSS',
},
{
testId: 'nav-css-modules',
to: '/rsc-hmr-css-modules',
label: 'CSS Modules',
},
] as const

return (
<>
<nav style={{ display: 'flex', gap: 12, padding: 12 }}>
{navLinks.map((link) => (
<Link
activeProps={{ style: { fontWeight: 'bold' } }}
data-testid={link.testId}
key={link.testId}
to={link.to}
>
{link.label}
</Link>
))}
</nav>
<ClientOnly>
<span data-testid="hydrated">hydrated</span>
</ClientOnly>
<Outlet />
</>
)
}
17 changes: 17 additions & 0 deletions e2e/react-start/rsc-hmr/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({
component: Home,
})

function Home() {
return (
<main data-testid="home" style={{ padding: 16 }}>
<h1>RSC CSS HMR playground</h1>
<p>
Open one of the routes above and edit the corresponding CSS file in
<code> src/utils/ </code> to exercise CSS HMR through the RSC renderer.
</p>
</main>
)
}
15 changes: 15 additions & 0 deletions e2e/react-start/rsc-hmr/src/routes/rsc-hmr-css-modules.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createFileRoute } from '@tanstack/react-router'
import { getCssModulesCardServerComponent } from '~/utils/cssModulesCardServerComponent'

export const Route = createFileRoute('/rsc-hmr-css-modules')({
loader: async () => {
const Server = await getCssModulesCardServerComponent()
return { Server }
},
component: RscHmrCssModules,
})

function RscHmrCssModules() {
const { Server } = Route.useLoaderData()
return <>{Server}</>
}
15 changes: 15 additions & 0 deletions e2e/react-start/rsc-hmr/src/routes/rsc-hmr-global-css.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createFileRoute } from '@tanstack/react-router'
import { getGlobalCssCardServerComponent } from '~/utils/globalCssCardServerComponent'

export const Route = createFileRoute('/rsc-hmr-global-css')({
loader: async () => {
const Server = await getGlobalCssCardServerComponent()
return { Server }
},
component: RscHmrGlobalCss,
})

function RscHmrGlobalCss() {
const { Server } = Route.useLoaderData()
return <>{Server}</>
}
13 changes: 13 additions & 0 deletions e2e/react-start/rsc-hmr/src/utils/CssModulesCard.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.card {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
margin: 20px;
}

.title {
color: rgb(128, 0, 128);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 1.5rem;
}
12 changes: 12 additions & 0 deletions e2e/react-start/rsc-hmr/src/utils/CssModulesCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference types="vite/client" />
import styles from './CssModulesCard.module.css'

export function CssModulesCard() {
return (
<div className={styles.card} data-testid="rsc-hmr-modules-card">
<h2 className={styles.title} data-testid="rsc-hmr-modules-title">
Server Rendered
</h2>
</div>
)
}
Loading