From 5afe2de86c076f060468a78acc129c529150c9f0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 25 Aug 2025 12:02:31 +0900 Subject: [PATCH 1/7] fix(rsc): fix CSS HMR with `?url` --- .../basic/src/routes/css-queries/client.tsx | 23 ++++++++----------- packages/plugin-rsc/src/plugin.ts | 5 ++++ packages/plugin-rsc/src/vite-utils.ts | 2 ++ 3 files changed, 17 insertions(+), 13 deletions(-) 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 index 73e0004e5..5998f65ee 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/css-queries/client.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/css-queries/client.tsx @@ -12,24 +12,21 @@ export function TestCssQueriesClient(props: { }) { const [enabled, setEnabled] = React.useState(false) - function urlWithHmr(href: string) { - if (import.meta.hot) { - href += '?t=' + Date.now() - } - return href - } - return (
{enabled && ( <> - - - - - - + + + + + + )}
test-css-url-client
diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index f081de664..7471cb2c8 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -36,6 +36,7 @@ import { cleanUrl, normalizeViteImportAnalysisUrl, prepareError, + urlRE, } from './vite-utils' import { cjsModuleRunnerPlugin } from './plugins/cjs' import { @@ -474,6 +475,10 @@ export default function vitePluginRsc( }, async hotUpdate(ctx) { if (isCSSRequest(ctx.file)) { + // keep default behavior for css with `?url` query, which uses `?direct` for HMR + if (ctx.modules.find((m) => m.id && urlRE.test(m.id))) { + return + } if (this.environment.name === 'client') { // filter out `.css?direct` (injected by SSR) to avoid browser full reload // when changing non-self accepting css such as `module.css`. diff --git a/packages/plugin-rsc/src/vite-utils.ts b/packages/plugin-rsc/src/vite-utils.ts index d65a5f008..fe93e1d79 100644 --- a/packages/plugin-rsc/src/vite-utils.ts +++ b/packages/plugin-rsc/src/vite-utils.ts @@ -152,3 +152,5 @@ function cleanStack(stack: string) { .filter((l) => /^\s*at/.test(l)) .join('\n') } + +export const urlRE: RegExp = /(\?|&)url(?:&|$)/ From cf1d1044f0a16a953676e8690d8385fa04f28362 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 25 Aug 2025 12:34:29 +0900 Subject: [PATCH 2/7] fix: tweak --- packages/plugin-rsc/src/plugin.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index d26287789..9ac425d18 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -479,14 +479,12 @@ export default function vitePluginRsc( }, async hotUpdate(ctx) { if (isCSSRequest(ctx.file)) { - // keep default behavior for css with `?url` query, which uses `?direct` for HMR - if (ctx.modules.find((m) => m.id && urlRE.test(m.id))) { - return - } if (this.environment.name === 'client') { // filter out `.css?direct` (injected by SSR) to avoid browser full reload // when changing non-self accepting css such as `module.css`. - return ctx.modules.filter((m) => !m.id?.includes('?direct')) + return ctx.modules.filter( + (m) => !(m.id?.includes('?direct') && !m.isSelfAccepting), + ) } } From 9764bcef15a8c35637f5634b64acbd51d7fe68b4 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 25 Aug 2025 12:36:17 +0900 Subject: [PATCH 3/7] chore: cleanup --- packages/plugin-rsc/src/plugin.ts | 1 - packages/plugin-rsc/src/vite-utils.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 9ac425d18..717bbf948 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -36,7 +36,6 @@ import { cleanUrl, normalizeViteImportAnalysisUrl, prepareError, - urlRE, } from './vite-utils' import { cjsModuleRunnerPlugin } from './plugins/cjs' import { diff --git a/packages/plugin-rsc/src/vite-utils.ts b/packages/plugin-rsc/src/vite-utils.ts index fe93e1d79..d65a5f008 100644 --- a/packages/plugin-rsc/src/vite-utils.ts +++ b/packages/plugin-rsc/src/vite-utils.ts @@ -152,5 +152,3 @@ function cleanStack(stack: string) { .filter((l) => /^\s*at/.test(l)) .join('\n') } - -export const urlRE: RegExp = /(\?|&)url(?:&|$)/ From 16dbefe723409ee76466ff7a99fd8b414302336b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 25 Aug 2025 12:37:50 +0900 Subject: [PATCH 4/7] chore: no precedence for this test --- .../basic/src/routes/css-queries/client.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) 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 index 5998f65ee..5840f9287 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/css-queries/client.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/css-queries/client.tsx @@ -17,16 +17,12 @@ export function TestCssQueriesClient(props: { {enabled && ( <> - - - - - - + + + + + + )}
test-css-url-client
From 7aedb19870b37788b8a655d77b92ff937d3bdd86 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 25 Aug 2025 12:48:35 +0900 Subject: [PATCH 5/7] test: wip --- .../examples/basic/src/routes/style-client/client-url.css | 3 +++ .../examples/basic/src/routes/style-client/client.tsx | 7 +++++++ .../examples/basic/src/routes/style-server/server-url.css | 3 +++ .../examples/basic/src/routes/style-server/server.tsx | 7 +++++++ 4 files changed, 20 insertions(+) create mode 100644 packages/plugin-rsc/examples/basic/src/routes/style-client/client-url.css create mode 100644 packages/plugin-rsc/examples/basic/src/routes/style-server/server-url.css diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-client/client-url.css b/packages/plugin-rsc/examples/basic/src/routes/style-client/client-url.css new file mode 100644 index 000000000..cabba9a9a --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-client/client-url.css @@ -0,0 +1,3 @@ +.test-style-url-client { + color: rgb(255, 165, 0); +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-client/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/style-client/client.tsx index bc715d672..b07b627c8 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/style-client/client.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/style-client/client.tsx @@ -3,6 +3,7 @@ import './client.css' import { TestClientDep } from './client-dep' import styles from './client.module.css' +import styleUrl from './client-url.css?url' export function TestStyleClient() { return ( @@ -11,6 +12,12 @@ export function TestStyleClient() {
test-css-module-client
+ +
test-style-url-client
) diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-server/server-url.css b/packages/plugin-rsc/examples/basic/src/routes/style-server/server-url.css new file mode 100644 index 000000000..5e249a05a --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-server/server-url.css @@ -0,0 +1,3 @@ +.test-style-url-server { + color: rgb(255, 165, 0); +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-server/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/style-server/server.tsx index 4a6e2180d..45d21e423 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/style-server/server.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/style-server/server.tsx @@ -1,5 +1,6 @@ import './server.css' import styles from './server.module.css' +import styleUrl from './server-url.css?url' export function TestStyleServer() { return ( @@ -8,6 +9,12 @@ export function TestStyleServer() {
test-css-module-server
+ +
test-style-url-server
Date: Mon, 25 Aug 2025 13:36:44 +0900 Subject: [PATCH 6/7] test: extend CSS HMR tests for URL-based CSS files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add test cases for client-url.css and server-url.css to cover CSS HMR functionality with URL-based CSS imports, following the same pattern as existing CSS module tests. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- packages/plugin-rsc/e2e/basic.test.ts | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index 559033174..4c3f763eb 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -854,6 +854,56 @@ function defineTest(f: Fixture) { ) }) + test('css url client @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.locator('.test-style-url-client')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + + if (f.mode !== 'dev') return + + // test client css url HMR + await using _ = await expectNoReload(page) + const editor = f.createEditor('src/routes/style-client/client-url.css') + editor.edit((s) => s.replaceAll('rgb(255, 165, 0)', 'rgb(0, 165, 255)')) + await expect(page.locator('.test-style-url-client')).toHaveCSS( + 'color', + 'rgb(0, 165, 255)', + ) + editor.reset() + await expect(page.locator('.test-style-url-client')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + }) + + test('css url server @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.locator('.test-style-url-server')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + + if (f.mode !== 'dev') return + + // test server css url HMR + await using _ = await expectNoReload(page) + const editor = f.createEditor('src/routes/style-server/server-url.css') + editor.edit((s) => s.replaceAll('rgb(255, 165, 0)', 'rgb(0, 165, 255)')) + await expect(page.locator('.test-style-url-server')).toHaveCSS( + 'color', + 'rgb(0, 165, 255)', + ) + editor.reset() + await expect(page.locator('.test-style-url-server')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + }) + test('tailwind @js', async ({ page }) => { await page.goto(f.url()) await waitForHydration(page) From 1dafe432aaaf6c05c9b58dceed27f793f4bad3d9 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 25 Aug 2025 14:07:41 +0900 Subject: [PATCH 7/7] test: update --- packages/plugin-rsc/e2e/basic.test.ts | 14 +++++++++----- .../basic/src/routes/style-client/client.tsx | 2 +- .../basic/src/routes/style-server/server.tsx | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index 4c3f763eb..3dc95656a 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -632,11 +632,15 @@ function defineTest(f: Fixture) { async function expectNoDuplicateServerCss(page: Page) { // check only manually inserted stylesheet link exists - // (toHaveAttribute passes only when locator matches single element) - await expect(page.locator('link[rel="stylesheet"]')).toHaveAttribute( - 'href', - '/test-style-server-manual.css', - ) + await expect(page.locator('link[rel="stylesheet"]')).toHaveCount(3) + for (const locator of await page + .locator('link[rel="stylesheet"]') + .all()) { + await expect(locator).toHaveAttribute( + 'data-precedence', + 'test-style-manual-link', + ) + } } test('no duplicate server css', async ({ page }) => { diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-client/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/style-client/client.tsx index b07b627c8..3ba274f33 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/style-client/client.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/style-client/client.tsx @@ -15,7 +15,7 @@ export function TestStyleClient() {
test-style-url-client
diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-server/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/style-server/server.tsx index 45d21e423..e2c028741 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/style-server/server.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/style-server/server.tsx @@ -12,13 +12,13 @@ export function TestStyleServer() {
test-style-url-server
test-style-server-manual