From d94045275ddfa842306fa4fad286662abd4bf66d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Jul 2025 02:42:07 +0000 Subject: [PATCH 1/3] Initial plan From 89d6670390a6323667b8a5312e71f1356232bbf2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Jul 2025 03:01:26 +0000 Subject: [PATCH 2/3] Implement CSS query filtering for ?url, ?inline, ?raw imports --- packages/plugin-rsc/src/css-query.test.ts | 76 +++++++++++++++++++++++ packages/plugin-rsc/src/plugin.ts | 20 +++++- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-rsc/src/css-query.test.ts diff --git a/packages/plugin-rsc/src/css-query.test.ts b/packages/plugin-rsc/src/css-query.test.ts new file mode 100644 index 000000000..1638b7eb1 --- /dev/null +++ b/packages/plugin-rsc/src/css-query.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, test } from 'vitest' + +/** + * 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') + } +} + +describe('hasSpecialCssQuery', () => { + test('should return true for CSS imports with ?url query', () => { + expect(hasSpecialCssQuery('/path/to/style.css?url')).toBe(true) + expect(hasSpecialCssQuery('/path/to/style.css?url&other=param')).toBe(true) + expect(hasSpecialCssQuery('/path/to/style.css?other=param&url')).toBe(true) + }) + + test('should return true for CSS imports with ?inline query', () => { + expect(hasSpecialCssQuery('/path/to/style.css?inline')).toBe(true) + expect(hasSpecialCssQuery('/path/to/style.css?inline&other=param')).toBe( + true, + ) + expect(hasSpecialCssQuery('/path/to/style.css?other=param&inline')).toBe( + true, + ) + }) + + test('should return true for CSS imports with ?raw query', () => { + expect(hasSpecialCssQuery('/path/to/style.css?raw')).toBe(true) + expect(hasSpecialCssQuery('/path/to/style.css?raw&other=param')).toBe(true) + expect(hasSpecialCssQuery('/path/to/style.css?other=param&raw')).toBe(true) + }) + + test('should return false for normal CSS imports', () => { + expect(hasSpecialCssQuery('/path/to/style.css')).toBe(false) + expect(hasSpecialCssQuery('/path/to/style.css?t=123456')).toBe(false) + expect(hasSpecialCssQuery('/path/to/style.css?other=param')).toBe(false) + }) + + test('should handle complex URLs with multiple parameters', () => { + expect(hasSpecialCssQuery('/path/to/style.css?t=123&url&v=1')).toBe(true) + expect(hasSpecialCssQuery('/path/to/style.css?t=123&inline&v=1')).toBe(true) + expect(hasSpecialCssQuery('/path/to/style.css?t=123&raw&v=1')).toBe(true) + expect(hasSpecialCssQuery('/path/to/style.css?t=123&other=param&v=1')).toBe( + false, + ) + }) + + test('should handle absolute URLs', () => { + expect(hasSpecialCssQuery('http://localhost:3000/style.css?url')).toBe(true) + expect(hasSpecialCssQuery('http://localhost:3000/style.css?inline')).toBe( + true, + ) + expect(hasSpecialCssQuery('http://localhost:3000/style.css?raw')).toBe(true) + expect(hasSpecialCssQuery('http://localhost:3000/style.css?t=123')).toBe( + false, + ) + }) + + test('should handle file URLs', () => { + expect(hasSpecialCssQuery('file:///path/to/style.css?url')).toBe(true) + expect(hasSpecialCssQuery('file:///path/to/style.css?inline')).toBe(true) + expect(hasSpecialCssQuery('file:///path/to/style.css?raw')).toBe(true) + expect(hasSpecialCssQuery('file:///path/to/style.css?t=123')).toBe(false) + }) +}) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 8f7f7f0f3..528932cbc 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) From 0f6af4142a3cd0b0b6d3be0aab9371b00e46167f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Jul 2025 03:15:54 +0000 Subject: [PATCH 3/3] Replace unit tests with e2e tests for CSS query filtering Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- packages/plugin-rsc/e2e/basic.test.ts | 69 +++++++++++++++++ .../basic/src/routes/css-queries/client.tsx | 23 ++++++ .../basic/src/routes/css-queries/test.css | 19 +++++ .../examples/basic/src/routes/root.tsx | 2 + packages/plugin-rsc/src/css-query.test.ts | 76 ------------------- 5 files changed, 113 insertions(+), 76 deletions(-) create mode 100644 packages/plugin-rsc/examples/basic/src/routes/css-queries/client.tsx create mode 100644 packages/plugin-rsc/examples/basic/src/routes/css-queries/test.css delete mode 100644 packages/plugin-rsc/src/css-query.test.ts diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index cc6cadd06..b9bce51c8 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 000000000..bd10ddae3 --- /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 000000000..fc0a21e4b --- /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 5a3421ed4..7b0d832f9 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/css-query.test.ts b/packages/plugin-rsc/src/css-query.test.ts deleted file mode 100644 index 1638b7eb1..000000000 --- a/packages/plugin-rsc/src/css-query.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { describe, expect, test } from 'vitest' - -/** - * 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') - } -} - -describe('hasSpecialCssQuery', () => { - test('should return true for CSS imports with ?url query', () => { - expect(hasSpecialCssQuery('/path/to/style.css?url')).toBe(true) - expect(hasSpecialCssQuery('/path/to/style.css?url&other=param')).toBe(true) - expect(hasSpecialCssQuery('/path/to/style.css?other=param&url')).toBe(true) - }) - - test('should return true for CSS imports with ?inline query', () => { - expect(hasSpecialCssQuery('/path/to/style.css?inline')).toBe(true) - expect(hasSpecialCssQuery('/path/to/style.css?inline&other=param')).toBe( - true, - ) - expect(hasSpecialCssQuery('/path/to/style.css?other=param&inline')).toBe( - true, - ) - }) - - test('should return true for CSS imports with ?raw query', () => { - expect(hasSpecialCssQuery('/path/to/style.css?raw')).toBe(true) - expect(hasSpecialCssQuery('/path/to/style.css?raw&other=param')).toBe(true) - expect(hasSpecialCssQuery('/path/to/style.css?other=param&raw')).toBe(true) - }) - - test('should return false for normal CSS imports', () => { - expect(hasSpecialCssQuery('/path/to/style.css')).toBe(false) - expect(hasSpecialCssQuery('/path/to/style.css?t=123456')).toBe(false) - expect(hasSpecialCssQuery('/path/to/style.css?other=param')).toBe(false) - }) - - test('should handle complex URLs with multiple parameters', () => { - expect(hasSpecialCssQuery('/path/to/style.css?t=123&url&v=1')).toBe(true) - expect(hasSpecialCssQuery('/path/to/style.css?t=123&inline&v=1')).toBe(true) - expect(hasSpecialCssQuery('/path/to/style.css?t=123&raw&v=1')).toBe(true) - expect(hasSpecialCssQuery('/path/to/style.css?t=123&other=param&v=1')).toBe( - false, - ) - }) - - test('should handle absolute URLs', () => { - expect(hasSpecialCssQuery('http://localhost:3000/style.css?url')).toBe(true) - expect(hasSpecialCssQuery('http://localhost:3000/style.css?inline')).toBe( - true, - ) - expect(hasSpecialCssQuery('http://localhost:3000/style.css?raw')).toBe(true) - expect(hasSpecialCssQuery('http://localhost:3000/style.css?t=123')).toBe( - false, - ) - }) - - test('should handle file URLs', () => { - expect(hasSpecialCssQuery('file:///path/to/style.css?url')).toBe(true) - expect(hasSpecialCssQuery('file:///path/to/style.css?inline')).toBe(true) - expect(hasSpecialCssQuery('file:///path/to/style.css?raw')).toBe(true) - expect(hasSpecialCssQuery('file:///path/to/style.css?t=123')).toBe(false) - }) -})