Skip to content
Merged
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
2 changes: 0 additions & 2 deletions e2e/react-start/css-modules/.gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
node_modules
dist
.routeTree.gen.ts
src/routeTree.gen.ts
test-results
playwright-report
port*.txt
122 changes: 122 additions & 0 deletions e2e/react-start/css-modules/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/* 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 SassMixinRouteImport } from './routes/sass-mixin'
import { Route as QuotesRouteImport } from './routes/quotes'
import { Route as ModulesRouteImport } from './routes/modules'
import { Route as IndexRouteImport } from './routes/index'

const SassMixinRoute = SassMixinRouteImport.update({
id: '/sass-mixin',
path: '/sass-mixin',
getParentRoute: () => rootRouteImport,
} as any)
const QuotesRoute = QuotesRouteImport.update({
id: '/quotes',
path: '/quotes',
getParentRoute: () => rootRouteImport,
} as any)
const ModulesRoute = ModulesRouteImport.update({
id: '/modules',
path: '/modules',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/modules': typeof ModulesRoute
'/quotes': typeof QuotesRoute
'/sass-mixin': typeof SassMixinRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/modules': typeof ModulesRoute
'/quotes': typeof QuotesRoute
'/sass-mixin': typeof SassMixinRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/modules': typeof ModulesRoute
'/quotes': typeof QuotesRoute
'/sass-mixin': typeof SassMixinRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/modules' | '/quotes' | '/sass-mixin'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/modules' | '/quotes' | '/sass-mixin'
id: '__root__' | '/' | '/modules' | '/quotes' | '/sass-mixin'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ModulesRoute: typeof ModulesRoute
QuotesRoute: typeof QuotesRoute
SassMixinRoute: typeof SassMixinRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/sass-mixin': {
id: '/sass-mixin'
path: '/sass-mixin'
fullPath: '/sass-mixin'
preLoaderRoute: typeof SassMixinRouteImport
parentRoute: typeof rootRouteImport
}
'/quotes': {
id: '/quotes'
path: '/quotes'
fullPath: '/quotes'
preLoaderRoute: typeof QuotesRouteImport
parentRoute: typeof rootRouteImport
}
'/modules': {
id: '/modules'
path: '/modules'
fullPath: '/modules'
preLoaderRoute: typeof ModulesRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ModulesRoute: ModulesRoute,
QuotesRoute: QuotesRoute,
SassMixinRoute: SassMixinRoute,
}
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>>
}
}
7 changes: 7 additions & 0 deletions e2e/react-start/css-modules/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ function RootComponent() {
>
Sass Mixin
</Link>
<Link
to="/quotes"
style={{ color: '#0284c7', textDecoration: 'none' }}
data-testid="nav-quotes"
>
Quoted CSS
</Link>
</nav>

<main style={{ padding: '20px' }}>
Expand Down
23 changes: 23 additions & 0 deletions e2e/react-start/css-modules/src/routes/quotes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createFileRoute } from '@tanstack/react-router'
import '~/styles/quotes.css'

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

function Quotes() {
return (
<div>
<h1>CSS Collection Test - Quoted Content</h1>
<p>This page tests that CSS with quoted content is properly extracted.</p>

<div className="quote-test" data-testid="quote-styled">
This element uses CSS with content: "..." property
</div>

<div className="after-quote" data-testid="after-quote-styled">
This element's styles come after the quoted content - should still work
</div>
</div>
)
}
17 changes: 17 additions & 0 deletions e2e/react-start/css-modules/src/styles/quotes.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* CSS file with quotes that could break regex extraction */

.quote-test {
/* Using content with quotes - common pattern that could break extraction */
content: 'Hello World';
background-color: #ef4444; /* red-500 */
padding: 20px;
border-radius: 8px;
color: white;
}

.after-quote {
/* This style comes after quoted content - should still be present */
background-color: #f59e0b; /* amber-500 */
padding: 16px;
font-weight: bold;
}
28 changes: 28 additions & 0 deletions e2e/react-start/css-modules/tests/css.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,34 @@ test.describe('CSS styles in SSR (dev mode)', () => {
)
expect(padding).toBe('24px')
})

test('CSS with quoted content is fully extracted', async ({
page,
baseURL,
}) => {
await page.goto(buildUrl(baseURL!, '/quotes'))

// Verify the element using CSS with content:"..." is styled
const quoteElement = page.getByTestId('quote-styled')
await expect(quoteElement).toBeVisible()

// #ef4444 (red-500) in RGB is rgb(239, 68, 68)
const quoteBackgroundColor = await quoteElement.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(quoteBackgroundColor).toBe('rgb(239, 68, 68)')

// Verify styles AFTER the quoted content are also extracted
// This is the key test - the regex bug would cut off CSS at the first quote
const afterQuoteElement = page.getByTestId('after-quote-styled')
await expect(afterQuoteElement).toBeVisible()

// #f59e0b (amber-500) in RGB is rgb(245, 158, 11)
const afterQuoteBackgroundColor = await afterQuoteElement.evaluate(
(el) => getComputedStyle(el).backgroundColor,
)
expect(afterQuoteBackgroundColor).toBe('rgb(245, 158, 11)')
})
})

test('styles persist after hydration', async ({ page, baseURL }) => {
Expand Down
49 changes: 40 additions & 9 deletions packages/start-plugin-core/src/dev-server-plugin/dev-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export function normalizeCssModuleCacheKey(idOrFile: string): string {
// URL params that indicate CSS should not be injected (e.g., ?url, ?inline)
const CSS_SIDE_EFFECT_FREE_PARAMS = ['url', 'inline', 'raw', 'inline-css']

const VITE_CSS_REGEX = /const\s+__vite__css\s*=\s*["'`]([\s\S]*?)["'`]/
// Marker to find the CSS string in Vite's transformed output
const VITE_CSS_MARKER = 'const __vite__css = '

const ESCAPE_CSS_COMMENT_START_REGEX = /\/\*/g
const ESCAPE_CSS_COMMENT_END_REGEX = /\*\//g
Expand Down Expand Up @@ -248,13 +249,43 @@ async function fetchCssFromModule(
}
}

function extractCssFromCode(code: string): string | undefined {
const match = VITE_CSS_REGEX.exec(code)
if (!match?.[1]) return undefined
/**
* Extract CSS content from Vite's transformed CSS module code.
*
* Vite embeds CSS into the module as a JS string via `JSON.stringify(cssContent)`,
* e.g. `const __vite__css = ${JSON.stringify('...css...')}`.
*
* We locate that JSON string literal and run `JSON.parse` on it to reverse the
* escaping (\\n, \\t, \\", \\\\, \\uXXXX, etc.).
*/
export function extractCssFromCode(code: string): string | undefined {
const startIdx = code.indexOf(VITE_CSS_MARKER)
if (startIdx === -1) return undefined

const valueStart = startIdx + VITE_CSS_MARKER.length
// Vite emits `const __vite__css = ${JSON.stringify(cssContent)}` which always
// produces double-quoted JSON string literals.
if (code.charCodeAt(valueStart) !== 34) return undefined

const codeLength = code.length
let i = valueStart + 1
while (i < codeLength) {
const charCode = code.charCodeAt(i)
// 34 = '"'
if (charCode === 34) {
try {
return JSON.parse(code.slice(valueStart, i + 1))
} catch {
return undefined
}
}
// 92 = '\\'
if (charCode === 92) {
i += 2
} else {
i++
}
}

return match[1]
.replace(/\\n/g, '\n')
.replace(/\\t/g, '\t')
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\')
return undefined
}
Loading