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

Fix dev SSR style collection to preserve CSS order without duplicating imported styles, while retaining styles from code-split routes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import styles from '../styles/code-split-route.module.css'

export function CodeSplitStyledBox() {
return (
<div className={styles.styledBox} data-testid="styled-box">
This box should have a blue background when dev styles are enabled.
</div>
)
}
24 changes: 21 additions & 3 deletions e2e/react-start/dev-ssr-styles/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@
// 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 CssImportOrderRouteImport } from './routes/css-import-order'
import { Route as IndexRouteImport } from './routes/index'

const CssImportOrderRoute = CssImportOrderRouteImport.update({
id: '/css-import-order',
path: '/css-import-order',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
Expand All @@ -19,28 +25,39 @@ const IndexRoute = IndexRouteImport.update({

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/css-import-order': typeof CssImportOrderRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/css-import-order': typeof CssImportOrderRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/css-import-order': typeof CssImportOrderRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fullPaths: '/' | '/css-import-order'
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/'
to: '/' | '/css-import-order'
id: '__root__' | '/' | '/css-import-order'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
CssImportOrderRoute: typeof CssImportOrderRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/css-import-order': {
id: '/css-import-order'
path: '/css-import-order'
fullPath: '/css-import-order'
preLoaderRoute: typeof CssImportOrderRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
Expand All @@ -53,6 +70,7 @@ declare module '@tanstack/react-router' {

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
CssImportOrderRoute: CssImportOrderRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
Expand Down
17 changes: 17 additions & 0 deletions e2e/react-start/dev-ssr-styles/src/routes/css-import-order.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createFileRoute } from '@tanstack/react-router'
import '../styles/css-import-order.css'

export const Route = createFileRoute('/css-import-order')({
component: CssImportOrder,
})

function CssImportOrder() {
return (
<main>
<h1>CSS import order</h1>
<div className="css-import-order" data-testid="css-import-order">
The route stylesheet should override its imported base stylesheet.
</div>
</main>
)
}
5 changes: 2 additions & 3 deletions e2e/react-start/dev-ssr-styles/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
import { CodeSplitStyledBox } from '../components/CodeSplitStyledBox'

export const Route = createFileRoute('/')({
component: Home,
Expand All @@ -8,9 +9,7 @@ function Home() {
return (
<div>
<h1 data-testid="home-heading">Dev SSR Styles Test</h1>
<div className="styled-box" data-testid="styled-box">
This box should have a blue background when dev styles are enabled.
</div>
<CodeSplitStyledBox />
</div>
)
}
7 changes: 0 additions & 7 deletions e2e/react-start/dev-ssr-styles/src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,3 @@ body {
padding: 20px;
background-color: #f0f4f8;
}

.styled-box {
background-color: #3b82f6;
color: white;
padding: 24px;
border-radius: 12px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.styledBox {
background-color: #3b82f6;
color: white;
padding: 24px;
border-radius: 12px;
}
5 changes: 5 additions & 0 deletions e2e/react-start/dev-ssr-styles/src/styles/css-import-base.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.css-import-order {
--css-import-base-marker: issue-7794;
background-color: #ffffff;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Stylelint error before merging.

Stylelint reports declaration-empty-line-before on Line 3. Add an empty line between the custom property and background-color.

Proposed fix
 .css-import-order {
   --css-import-base-marker: issue-7794;
+
   background-color: `#ffffff`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
background-color: #ffffff;
.css-import-order {
--css-import-base-marker: issue-7794;
background-color: `#ffffff`;
}
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 3-3: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/react-start/dev-ssr-styles/src/styles/css-import-base.css` at line 3, In
the stylesheet declaration block, add an empty line between the custom property
and the background-color declaration to satisfy Stylelint’s
declaration-empty-line-before rule.

Source: Linters/SAST tools

color: #000000;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@import './css-import-base.css';

.css-import-order {
background-color: #111827;
color: #ffffff;
padding: 24px;
}
65 changes: 64 additions & 1 deletion e2e/react-start/dev-ssr-styles/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,37 @@ test.describe(`dev.ssrStyles (mode=${ssrStylesMode})`, () => {
})

if (ssrStylesMode === 'default') {
test('dev CSS order is stable after client modules load', async ({
page,
}) => {
const cssBodies: Array<Promise<string>> = []
page.on('response', (response) => {
if (
new URL(response.url()).pathname.endsWith(
'/@tanstack-start/styles.css',
)
) {
cssBodies.push(response.text())
}
})

await page.goto('/css-import-order')
await expect(page.getByTestId('css-import-order')).toBeVisible()
await expect.poll(() => cssBodies.length).toBeGreaterThanOrEqual(1)

await page.reload()
await expect(page.getByTestId('css-import-order')).toBeVisible()
await expect.poll(() => cssBodies.length).toBeGreaterThanOrEqual(2)

const [initialCss, reloadedCss] = await Promise.all(cssBodies.slice(0, 2))
Comment on lines +28 to +47

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Associate each CSS response with its navigation.

The listener may collect multiple stylesheet responses before reload(), so slice(0, 2) can compare two responses from the first navigation and never validate the reload.

Proposed fix
-      const cssBodies: Array<Promise<string>> = []
-      page.on('response', (response) => {
-        if (
-          new URL(response.url()).pathname.endsWith(
-            '/@tanstack-start/styles.css',
-          )
-        ) {
-          cssBodies.push(response.text())
-        }
-      })
+      const isDevStylesResponse = (response: { url(): string }) =>
+        new URL(response.url()).pathname.endsWith(
+          '/@tanstack-start/styles.css',
+        )
 
+      const initialResponsePromise =
+        page.waitForResponse(isDevStylesResponse)
       await page.goto('/css-import-order')
       await expect(page.getByTestId('css-import-order')).toBeVisible()
-      await expect.poll(() => cssBodies.length).toBeGreaterThanOrEqual(1)
+      const initialCss = await (await initialResponsePromise).text()
 
+      const reloadedResponsePromise =
+        page.waitForResponse(isDevStylesResponse)
       await page.reload()
       await expect(page.getByTestId('css-import-order')).toBeVisible()
-      await expect.poll(() => cssBodies.length).toBeGreaterThanOrEqual(2)
-
-      const [initialCss, reloadedCss] = await Promise.all(cssBodies.slice(0, 2))
+      const reloadedCss = await (await reloadedResponsePromise).text()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cssBodies: Array<Promise<string>> = []
page.on('response', (response) => {
if (
new URL(response.url()).pathname.endsWith(
'/@tanstack-start/styles.css',
)
) {
cssBodies.push(response.text())
}
})
await page.goto('/css-import-order')
await expect(page.getByTestId('css-import-order')).toBeVisible()
await expect.poll(() => cssBodies.length).toBeGreaterThanOrEqual(1)
await page.reload()
await expect(page.getByTestId('css-import-order')).toBeVisible()
await expect.poll(() => cssBodies.length).toBeGreaterThanOrEqual(2)
const [initialCss, reloadedCss] = await Promise.all(cssBodies.slice(0, 2))
const isDevStylesResponse = (response: { url(): string }) =>
new URL(response.url()).pathname.endsWith(
'/@tanstack-start/styles.css',
)
const initialResponsePromise =
page.waitForResponse(isDevStylesResponse)
await page.goto('/css-import-order')
await expect(page.getByTestId('css-import-order')).toBeVisible()
const initialCss = await (await initialResponsePromise).text()
const reloadedResponsePromise =
page.waitForResponse(isDevStylesResponse)
await page.reload()
await expect(page.getByTestId('css-import-order')).toBeVisible()
const reloadedCss = await (await reloadedResponsePromise).text()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/react-start/dev-ssr-styles/tests/app.spec.ts` around lines 28 - 47,
Update the CSS response collection in the test around the page response listener
and navigation steps so each navigation records its own stylesheet response.
Capture the response associated with the initial page.goto separately from the
response observed after page.reload, then compare those two navigation-specific
promises instead of taking cssBodies.slice(0, 2).

expect(reloadedCss).toBe(initialCss)
expect(initialCss).toContain('/* /src/styles/app.css */')
expect(initialCss).toContain('/* /src/styles/css-import-order.css */')
expect(initialCss.indexOf('/* /src/styles/app.css */')).toBeLessThan(
initialCss.indexOf('/* /src/styles/css-import-order.css */'),
)
})

test.describe('default (enabled, basepath = vite base)', () => {
test.use({ javaScriptEnabled: false, whitelistErrors })

Expand Down Expand Up @@ -52,7 +83,9 @@ test.describe(`dev.ssrStyles (mode=${ssrStylesMode})`, () => {
expect(href).toMatch(/^\/@tanstack-start\/styles\.css/)
})

test('CSS is applied on initial page load (SSR)', async ({ page }) => {
test('CSS from a code-split route component is applied during SSR', async ({
page,
}) => {
await page.goto('/')

const element = page.getByTestId('styled-box')
Expand All @@ -64,6 +97,36 @@ test.describe(`dev.ssrStyles (mode=${ssrStylesMode})`, () => {
)
expect(backgroundColor).toBe('rgb(59, 130, 246)')
})

test('CSS @import dependencies are not appended after their importer', async ({
page,
}) => {
await page.goto('/css-import-order')

const element = page.getByTestId('css-import-order')
await expect(element).toBeVisible()

// css-import-order.css imports a white base rule, then overrides it
// with this dark background. Collecting the imported file separately
// appends the white rule and reverses the intended cascade.
const backgroundColor = await element.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(backgroundColor).toBe('rgb(17, 24, 39)')

const devStylesHref = await page
.locator(`link[${DEV_STYLES_ATTR}]`)
.getAttribute('href')
expect(devStylesHref).toBeTruthy()

const cssResponse = await page.request.get(
new URL(devStylesHref!, page.url()).href,
)
expect(cssResponse.ok()).toBeTruthy()

const css = await cssResponse.text()
expect(css.match(/--css-import-base-marker/g)).toHaveLength(1)
})
})
}

Expand Down
7 changes: 7 additions & 0 deletions e2e/solid-start/dev-ssr-styles/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
.routeTree.gen.ts
src/routeTree.gen.ts
test-results
playwright-report
port*.txt
29 changes: 29 additions & 0 deletions e2e/solid-start/dev-ssr-styles/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "tanstack-solid-start-e2e-dev-ssr-styles",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"dev:e2e": "vite dev --port $PORT",
"build": "vite build && tsc --noEmit",
"start": "pnpx srvx --prod -s ../client dist/server/server.js",
"test:e2e:dev": "MODE=dev playwright test --project=chromium",
"test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev"
},
"dependencies": {
"@tanstack/solid-router": "workspace:*",
"@tanstack/solid-start": "workspace:*",
"solid-js": "^1.9.10"
},
"devDependencies": {
"@playwright/test": "^1.61.0",
"@tanstack/router-core": "workspace:*",
"@tanstack/router-e2e-utils": "workspace:*",
"@types/node": "^22.10.2",
"srvx": "^0.11.9",
"typescript": "^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^2.11.11"
}
}
31 changes: 31 additions & 0 deletions e2e/solid-start/dev-ssr-styles/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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'] },
},
],
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import styles from '../styles/code-split-route.module.css'

export function CodeSplitStyledBox() {
return (
<div class={styles.styledBox} data-testid="styled-box">
This box is styled by a code-split route component.
</div>
)
}
10 changes: 10 additions & 0 deletions e2e/solid-start/dev-ssr-styles/src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createRouter } from '@tanstack/solid-router'
import { routeTree } from './routeTree.gen'

export function getRouter() {
return createRouter({
routeTree,
scrollRestoration: true,
defaultPreload: false,
})
}
27 changes: 27 additions & 0 deletions e2e/solid-start/dev-ssr-styles/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
HeadContent,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/solid-router'
import { HydrationScript } from 'solid-js/web'
import '~/styles/app.css'

export const Route = createRootRoute({
component: RootComponent,
})

function RootComponent() {
return (
<html>
<head>
<HydrationScript />
<HeadContent />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
)
}
15 changes: 15 additions & 0 deletions e2e/solid-start/dev-ssr-styles/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createFileRoute } from '@tanstack/solid-router'
import { CodeSplitStyledBox } from '../components/CodeSplitStyledBox'

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

function Home() {
return (
<main>
<h1 data-testid="home-heading">Solid Dev SSR Styles Test</h1>
<CodeSplitStyledBox />
</main>
)
}
5 changes: 5 additions & 0 deletions e2e/solid-start/dev-ssr-styles/src/styles/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
body {
font-family: sans-serif;
margin: 0;
padding: 20px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.styledBox {
--solid-code-split-style-marker: present;
background-color: #3b82f6;
Comment on lines +2 to +3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Stylelint violation.

Insert an empty line between the custom property and background-color to satisfy declaration-empty-line-before.

Suggested fix
 .styledBox {
   --solid-code-split-style-marker: present;
+
   background-color: `#3b82f6`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
--solid-code-split-style-marker: present;
background-color: #3b82f6;
--solid-code-split-style-marker: present;
background-color: `#3b82f6`;
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 3-3: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/solid-start/dev-ssr-styles/src/styles/code-split-route.module.css` around
lines 2 - 3, In the stylesheet rule containing --solid-code-split-style-marker,
insert an empty line before the background-color declaration to satisfy the
declaration-empty-line-before Stylelint rule.

Source: Linters/SAST tools

color: white;
padding: 24px;
}
Loading
Loading