diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index cc6cadd0..b9bce51c 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -379,6 +379,75 @@ function defineTest(f: Fixture) { }) }) + test('css queries @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await testCssQueries(page) + }) + + testNoJs('css queries @nojs', async ({ page }) => { + await page.goto(f.url()) + await testCssQueries(page) + }) + + testNoJs('css queries ssr collection', async ({ page }) => { + await page.goto(f.url()) + + // Get the HTML source to check what CSS links are included + const htmlContent = await page.content() + + // Normal CSS import should be included as a link tag + expect(htmlContent).toMatch( + /]*href="[^"]*test\.css[^"]*"[^>]*rel="stylesheet"/, + ) + + // CSS imports with special queries should NOT be included as link tags + expect(htmlContent).not.toMatch( + /]*href="[^"]*test\.css\?url[^"]*"[^>]*rel="stylesheet"/, + ) + expect(htmlContent).not.toMatch( + /]*href="[^"]*test\.css\?inline[^"]*"[^>]*rel="stylesheet"/, + ) + expect(htmlContent).not.toMatch( + /]*href="[^"]*test\.css\?raw[^"]*"[^>]*rel="stylesheet"/, + ) + }) + + async function testCssQueries(page: Page) { + // Normal CSS import should have styles applied in SSR + await expect(page.locator('[data-testid="css-normal"]')).toHaveCSS( + 'color', + 'rgb(75, 85, 99)', // gray-600 + ) + await expect(page.locator('[data-testid="css-normal"]')).toHaveCSS( + 'font-weight', + '700', // bold + ) + + // CSS?url should return a URL string + const urlValue = await page + .locator('[data-testid="css-url-value"]') + .textContent() + expect(urlValue).toContain('CSS URL value: ') + expect(urlValue).toMatch(/\.css(\?|$)/) // Should contain .css + + // CSS?inline should return CSS content as string + const inlineContent = await page + .locator('[data-testid="css-inline-content"]') + .textContent() + expect(inlineContent).toContain('CSS inline content length: ') + const inlineLength = parseInt(inlineContent!.split(': ')[1]) + expect(inlineLength).toBeGreaterThan(0) // Should have CSS content + + // CSS?raw should return raw CSS content + const rawContent = await page + .locator('[data-testid="css-raw-content"]') + .textContent() + expect(rawContent).toContain('CSS raw content length: ') + const rawLength = parseInt(rawContent!.split(': ')[1]) + expect(rawLength).toBeGreaterThan(0) // Should have CSS content + } + test('css @js', async ({ page }) => { await page.goto(f.url()) await waitForHydration(page) diff --git a/packages/plugin-rsc/examples/basic/src/routes/css-queries/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/css-queries/client.tsx new file mode 100644 index 00000000..bd10ddae --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/css-queries/client.tsx @@ -0,0 +1,23 @@ +'use client' + +import './test.css' // Normal CSS import - should be collected for SSR +import cssUrl from './test.css?url' // URL query - should NOT be collected +import cssInline from './test.css?inline' // Inline query - should NOT be collected +import cssRaw from './test.css?raw' // Raw query - should NOT be collected + +export function TestCssQueries() { + return ( +
+
+ Normal CSS import (should have styles in SSR) +
+
CSS URL value: {cssUrl}
+
+ CSS inline content length: {cssInline.length} +
+
+ CSS raw content length: {cssRaw.length} +
+
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/css-queries/test.css b/packages/plugin-rsc/examples/basic/src/routes/css-queries/test.css new file mode 100644 index 00000000..fc0a21e4 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/css-queries/test.css @@ -0,0 +1,19 @@ +.test-css-query-normal { + color: rgb(75, 85, 99); /* gray-600 */ + font-weight: bold; +} + +.test-css-query-url { + color: rgb(220, 38, 127); /* pink-600 */ + text-decoration: underline; +} + +.test-css-query-inline { + color: rgb(101, 163, 13); /* lime-600 */ + font-style: italic; +} + +.test-css-query-raw { + color: rgb(147, 51, 234); /* purple-600 */ + text-transform: uppercase; +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/root.tsx b/packages/plugin-rsc/examples/basic/src/routes/root.tsx index 5a3421ed..7b0d832f 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/root.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/root.tsx @@ -29,6 +29,7 @@ import { TestTemporaryReference } from './temporary-reference/client' import { TestUseCache } from './use-cache/server' import { TestHydrationMismatch } from './hydration-mismatch/server' import { TestBrowserOnly } from './browser-only/client' +import { TestCssQueries } from './css-queries/client' export function Root(props: { url: URL }) { return ( @@ -69,6 +70,7 @@ export function Root(props: { url: URL }) { + diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 8f7f7f0f..528932cb 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1456,6 +1456,24 @@ export async function findSourceMapURL( // css support // +/** + * Check if a CSS import ID has special queries that should be excluded from CSS collection. + * These queries transform CSS imports to return different data types rather than actual CSS to be linked. + */ +function hasSpecialCssQuery(id: string): boolean { + try { + const url = new URL(id, 'file://') + return ( + url.searchParams.has('url') || + url.searchParams.has('inline') || + url.searchParams.has('raw') + ) + } catch { + // If URL parsing fails, check with simple string matching as fallback + return id.includes('?url') || id.includes('?inline') || id.includes('?raw') + } +} + export function vitePluginRscCss( rscCssOptions?: Pick, ): Plugin[] { @@ -1475,7 +1493,7 @@ export function vitePluginRscCss( } for (const next of mod?.importedModules ?? []) { if (next.id) { - if (isCSSRequest(next.id)) { + if (isCSSRequest(next.id) && !hasSpecialCssQuery(next.id)) { cssIds.add(next.id) } else { recurse(next.id)