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/clean-rsbuild-stylesheets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/start-plugin-core': patch
---

Fix Rsbuild SSR stylesheet discovery for shared async chunks.
18 changes: 18 additions & 0 deletions e2e/react-start/css-modules/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dev:cloudflare": "echo 'Cloudflare dev mode has React duplication issues - use build+preview instead' && exit 1",
"dev:e2e:cloudflare": "echo 'Cloudflare dev mode has React duplication issues - use build+preview instead' && exit 1",
"build": "vite build && tsc --noEmit",
"build:rsbuild": "rsbuild build && tsc --noEmit",
"preview": "vite preview",
"start": "pnpx srvx --prod -s ../client dist/server/server.js",
"test:e2e:dev": "MODE=dev playwright test --project=chromium",
Expand All @@ -36,6 +37,9 @@
"devDependencies": {
"@cloudflare/vite-plugin": "^1.29.0",
"@playwright/test": "^1.61.0",
"@rsbuild/core": "^2.1.0",
"@rsbuild/plugin-react": "^2.0.0",
"@rsbuild/plugin-sass": "^2.0.1",
"@tanstack/router-e2e-utils": "workspace:*",
"@types/node": "^22.10.2",
"@types/react": "^19.0.8",
Expand All @@ -48,5 +52,19 @@
"typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"wrangler": "^4.74.0"
},
"nx": {
"metadata": {
"playwrightModes": [
{
"toolchain": "vite",
"mode": "ssr"
},
{
"toolchain": "rsbuild",
"mode": "ssr"
}
]
}
}
}
8 changes: 7 additions & 1 deletion e2e/react-start/css-modules/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { appServerReady } from '@tanstack/router-e2e-utils'

const mode = process.env.MODE ?? 'prod'
const isDev = mode === 'dev'
const distDir = process.env.E2E_DIST_DIR
const viteConfig = process.env.VITE_CONFIG // 'nitro' | 'basepath' | 'cloudflare' | undefined
const PORT = Number(process.env.E2E_APP_PORT ?? 0)

Expand All @@ -17,6 +18,7 @@ const devCommand = viteConfig

export default defineConfig({
testDir: './tests',
testMatch: isDev ? '**/*.dev.spec.ts' : '**/*.prod.spec.ts',
workers: 1,
reporter: [['line']],

Expand All @@ -27,7 +29,11 @@ export default defineConfig({
},

webServer: {
command: isDev ? devCommand : `pnpm build && PORT=${PORT} pnpm start`,
command: isDev
? devCommand
: distDir
? `pnpm exec srvx --prod --dir=. -s ${distDir}/client --entry ${distDir}/server/server.js`
: `pnpm build && PORT=${PORT} pnpm start`,
wait: appServerReady,
reuseExistingServer: false,
stdout: 'pipe',
Expand Down
38 changes: 38 additions & 0 deletions e2e/react-start/css-modules/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { defineConfig } from '@rsbuild/core'
import { pluginReact } from '@rsbuild/plugin-react'
import { pluginSass } from '@rsbuild/plugin-sass'
import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild'

export default defineConfig({
plugins: [pluginReact(), pluginSass(), tanstackStart()],
output: {
distPath: {
root: process.env.E2E_DIST_DIR ?? 'dist-rsbuild-ssr',
},
},
tools: {
rspack(config, { environment }) {
if (environment.name === 'ssr') {
config.output ??= {}
config.output.filename = 'server.js'
}
if (environment.name !== 'client') {
return
}
config.optimization ??= {}
// Extract the small shared component to reproduce #8415 reliably.
config.optimization.splitChunks = {
cacheGroups: {
shared: {
test: /[\\/]src[\\/]components[\\/]/,
chunks: 'async',
minChunks: 2,
minSize: 0,
enforce: true,
name: 'shared-header',
},
},
}
},
},
})
14 changes: 14 additions & 0 deletions e2e/react-start/css-modules/src/components/Header.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.header {
--repro-shared-header: 1;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
height: 64px;
padding-inline: 16px;
background: #e4f2e9;
}

.header h1 {
margin: 0;
font-size: 24px;
}
10 changes: 10 additions & 0 deletions e2e/react-start/css-modules/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styles from './Header.module.css'

export function Header({ title }: { title: string }) {
return (
<header className={styles.header} data-testid="shared-header">
<h1>{title}</h1>
<button type="button">Save</button>
</header>
)
}
49 changes: 46 additions & 3 deletions e2e/react-start/css-modules/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AlphaRouteImport } from './routes/alpha'
import { Route as BetaRouteImport } from './routes/beta'
import { Route as ModulesRouteImport } from './routes/modules'
import { Route as QuotesRouteImport } from './routes/quotes'
import { Route as SassMixinRouteImport } from './routes/sass-mixin'
Expand All @@ -19,6 +21,16 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AlphaRoute = AlphaRouteImport.update({
id: '/alpha',
path: '/alpha',
getParentRoute: () => rootRouteImport,
} as any)
const BetaRoute = BetaRouteImport.update({
id: '/beta',
path: '/beta',
getParentRoute: () => rootRouteImport,
} as any)
const ModulesRoute = ModulesRouteImport.update({
id: '/modules',
path: '/modules',
Expand All @@ -37,33 +49,48 @@ const SassMixinRoute = SassMixinRouteImport.update({

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/alpha': typeof AlphaRoute
'/beta': typeof BetaRoute
'/modules': typeof ModulesRoute
'/quotes': typeof QuotesRoute
'/sass-mixin': typeof SassMixinRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/alpha': typeof AlphaRoute
'/beta': typeof BetaRoute
'/modules': typeof ModulesRoute
'/quotes': typeof QuotesRoute
'/sass-mixin': typeof SassMixinRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/alpha': typeof AlphaRoute
'/beta': typeof BetaRoute
'/modules': typeof ModulesRoute
'/quotes': typeof QuotesRoute
'/sass-mixin': typeof SassMixinRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/modules' | '/quotes' | '/sass-mixin'
fullPaths: '/' | '/alpha' | '/beta' | '/modules' | '/quotes' | '/sass-mixin'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/modules' | '/quotes' | '/sass-mixin'
id: '__root__' | '/' | '/modules' | '/quotes' | '/sass-mixin'
to: '/' | '/alpha' | '/beta' | '/modules' | '/quotes' | '/sass-mixin'
id:
| '__root__'
| '/'
| '/alpha'
| '/beta'
| '/modules'
| '/quotes'
| '/sass-mixin'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AlphaRoute: typeof AlphaRoute
BetaRoute: typeof BetaRoute
ModulesRoute: typeof ModulesRoute
QuotesRoute: typeof QuotesRoute
SassMixinRoute: typeof SassMixinRoute
Expand All @@ -78,6 +105,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/alpha': {
id: '/alpha'
path: '/alpha'
fullPath: '/alpha'
preLoaderRoute: typeof AlphaRouteImport
parentRoute: typeof rootRouteImport
}
'/beta': {
id: '/beta'
path: '/beta'
fullPath: '/beta'
preLoaderRoute: typeof BetaRouteImport
parentRoute: typeof rootRouteImport
}
'/modules': {
id: '/modules'
path: '/modules'
Expand All @@ -104,6 +145,8 @@ declare module '@tanstack/react-router' {

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AlphaRoute: AlphaRoute,
BetaRoute: BetaRoute,
ModulesRoute: ModulesRoute,
QuotesRoute: QuotesRoute,
SassMixinRoute: SassMixinRoute,
Expand Down
13 changes: 13 additions & 0 deletions e2e/react-start/css-modules/src/routes/alpha.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createFileRoute } from '@tanstack/react-router'
import { Header } from '../components/Header'
import styles from '../styles/alpha.module.css'

export const Route = createFileRoute('/alpha')({ component: Alpha })

function Alpha() {
return (
<div className={styles.page} data-testid="route-page">
<Header title="Alpha" />
</div>
)
}
13 changes: 13 additions & 0 deletions e2e/react-start/css-modules/src/routes/beta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createFileRoute } from '@tanstack/react-router'
import { Header } from '../components/Header'
import styles from '../styles/beta.module.css'

export const Route = createFileRoute('/beta')({ component: Beta })

function Beta() {
return (
<div className={styles.page} data-testid="route-page">
<Header title="Beta" />
</div>
)
}
4 changes: 4 additions & 0 deletions e2e/react-start/css-modules/src/styles/alpha.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.page {
--repro-alpha: 1;
border: 4px solid #226844;
}
4 changes: 4 additions & 0 deletions e2e/react-start/css-modules/src/styles/beta.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.page {
--repro-beta: 1;
border: 4px solid #ac344c;
}
62 changes: 62 additions & 0 deletions e2e/react-start/css-modules/tests/shared-css.prod.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'
import type { Page } from '@playwright/test'

test.skip(
process.env.VITE_CSS_CODE_SPLIT === 'false',
'Route stylesheet isolation requires CSS code splitting',
)
test.use({ javaScriptEnabled: false })

async function getStylesheetContent(page: Page) {
const hrefs = await page
.locator('link[rel="stylesheet"]')
.evaluateAll((links: Array<HTMLLinkElement>) =>
links.map((link) => link.href),
)
expect(hrefs.length).toBeGreaterThan(0)
const stylesheets = await Promise.all(
hrefs.map(async (href) => {
const response = await page.request.get(href)
expect(response.ok()).toBe(true)
return response.text()
}),
)
return stylesheets.join('\n')
}

for (const route of ['alpha', 'beta']) {
test(`${route} has complete and isolated CSS without JavaScript`, async ({
page,
}) => {
await page.goto(`/${route}`)

const header = page.getByTestId('shared-header')
await expect(header).toHaveCSS('display', 'grid')
await expect(header).toHaveCSS('height', '64px')
await expect(page.getByTestId('route-page')).toHaveCSS(
`--repro-${route}`,
'1',
)

const css = await getStylesheetContent(page)
expect(css).toContain('--repro-shared-header')
expect(css).toContain(`--repro-${route}`)
expect(css).not.toContain(`--repro-${route === 'alpha' ? 'beta' : 'alpha'}`)
})
}

test('home does not load the shared header or route styles', async ({
page,
}) => {
await page.goto('/')
await expect(page.getByTestId('global-styled')).toHaveCSS(
'background-color',
'rgb(59, 130, 246)',
)

const css = await getStylesheetContent(page)
expect(css).not.toContain('--repro-shared-header')
expect(css).not.toContain('--repro-alpha')
expect(css).not.toContain('--repro-beta')
})
1 change: 1 addition & 0 deletions e2e/react-start/css-modules/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default defineConfig(async () => {
port: 3000,
},
build: {
outDir: process.env.E2E_DIST_DIR ?? 'dist',
cssCodeSplit,
},
plugins: [
Expand Down
Loading
Loading