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
70 changes: 70 additions & 0 deletions packages/plugin-rsc/e2e/basic.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import path from 'node:path'
Expand Down Expand Up @@ -1228,7 +1228,7 @@
// client is interactive before suspense is resolved
await page.goto(f.url('./?test-suspense=1000'), { waitUntil: 'commit' })
await waitForHydration(page)
await expect(page.getByTestId('suspense')).toContainText(

Check failure on line 1231 in packages/plugin-rsc/e2e/basic.test.ts

View workflow job for this annotation

GitHub Actions / test-rsc (macos-latest / webkit)

[webkit] › e2e/basic.test.ts:1227:3 › dev-default › hydrate while streaming @js

1) [webkit] › e2e/basic.test.ts:1227:3 › dev-default › hydrate while streaming @js ─────────────── Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toContainText(expected) failed Locator: getByTestId('suspense') Expected substring: "suspense-fallback" Received string: "suspense-resolved" Timeout: 5000ms Call log: - Expect "toContainText" with timeout 5000ms - waiting for getByTestId('suspense') 8 × locator resolved to <div data-testid="suspense">…</div> - unexpected value "suspense-resolved" 1229 | await page.goto(f.url('./?test-suspense=1000'), { waitUntil: 'commit' }) 1230 | await waitForHydration(page) > 1231 | await expect(page.getByTestId('suspense')).toContainText( | ^ 1232 | 'suspense-fallback', 1233 | ) 1234 | await expect(page.getByTestId('suspense')).toContainText( at /Users/runner/work/vite-plugin-react/vite-plugin-react/packages/plugin-rsc/e2e/basic.test.ts:1231:48

Check failure on line 1231 in packages/plugin-rsc/e2e/basic.test.ts

View workflow job for this annotation

GitHub Actions / test-rsc (macos-latest / webkit)

[webkit] › e2e/basic.test.ts:1227:3 › dev-default › hydrate while streaming @js

1) [webkit] › e2e/basic.test.ts:1227:3 › dev-default › hydrate while streaming @js ─────────────── Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toContainText(expected) failed Locator: getByTestId('suspense') Expected substring: "suspense-fallback" Received string: "suspense-resolved" Timeout: 5000ms Call log: - Expect "toContainText" with timeout 5000ms - waiting for getByTestId('suspense') 9 × locator resolved to <div data-testid="suspense">…</div> - unexpected value "suspense-resolved" 1229 | await page.goto(f.url('./?test-suspense=1000'), { waitUntil: 'commit' }) 1230 | await waitForHydration(page) > 1231 | await expect(page.getByTestId('suspense')).toContainText( | ^ 1232 | 'suspense-fallback', 1233 | ) 1234 | await expect(page.getByTestId('suspense')).toContainText( at /Users/runner/work/vite-plugin-react/vite-plugin-react/packages/plugin-rsc/e2e/basic.test.ts:1231:48

Check failure on line 1231 in packages/plugin-rsc/e2e/basic.test.ts

View workflow job for this annotation

GitHub Actions / test-rsc (macos-latest / webkit)

[webkit] › e2e/basic.test.ts:1227:3 › dev-default › hydrate while streaming @js

1) [webkit] › e2e/basic.test.ts:1227:3 › dev-default › hydrate while streaming @js ─────────────── Error: expect(locator).toContainText(expected) failed Locator: getByTestId('suspense') Expected substring: "suspense-fallback" Received string: "suspense-resolved" Timeout: 5000ms Call log: - Expect "toContainText" with timeout 5000ms - waiting for getByTestId('suspense') 9 × locator resolved to <div data-testid="suspense">…</div> - unexpected value "suspense-resolved" 1229 | await page.goto(f.url('./?test-suspense=1000'), { waitUntil: 'commit' }) 1230 | await waitForHydration(page) > 1231 | await expect(page.getByTestId('suspense')).toContainText( | ^ 1232 | 'suspense-fallback', 1233 | ) 1234 | await expect(page.getByTestId('suspense')).toContainText( at /Users/runner/work/vite-plugin-react/vite-plugin-react/packages/plugin-rsc/e2e/basic.test.ts:1231:48
'suspense-fallback',
)
await expect(page.getByTestId('suspense')).toContainText(
Expand Down Expand Up @@ -1620,4 +1620,74 @@
'test-tree-shake2:lib-client1|lib-server1',
)
})

test('virtual module with use client', async ({ page }) => {
await page.goto(f.url())
await waitForHydration(page)

// Test that the virtual client component renders and works
await expect(page.getByTestId('test-virtual-client')).toHaveText(
'test-virtual-client: not-clicked',
)
await page.getByTestId('test-virtual-client').click()
await expect(page.getByTestId('test-virtual-client')).toHaveText(
'test-virtual-client: clicked',
)
})

test('virtual css module', async ({ page }) => {
await page.goto(f.url())
await waitForHydration(page)

// Server CSS (loaded via <link>)
// Query-aware: works in both dev and build
await expect(page.locator('.test-virtual-style-server-query')).toHaveCSS(
'color',
'rgb(50, 100, 150)',
)
// Exact-match: fails via <link> in dev (Vite limitation), works in build
await expect(page.locator('.test-virtual-style-server-exact')).toHaveCSS(
'color',
f.mode === 'dev' ? 'rgb(0, 0, 0)' : 'rgb(200, 100, 50)',
)

// Client CSS (loaded via JS import, HMR injects styles)
// Both patterns work because no ?direct is involved in JS imports
await expect(page.locator('.test-virtual-style-client-query')).toHaveCSS(
'color',
'rgb(50, 150, 100)',
)
await expect(page.locator('.test-virtual-style-client-exact')).toHaveCSS(
'color',
'rgb(200, 50, 100)',
)
})

testNoJs('virtual css module @nojs', async ({ page }) => {
await page.goto(f.url())

// Server CSS (loaded via <link>)
// Query-aware: works in both dev and build
await expect(page.locator('.test-virtual-style-server-query')).toHaveCSS(
'color',
'rgb(50, 100, 150)',
)
// Exact-match: fails via <link> in dev (Vite limitation)
await expect(page.locator('.test-virtual-style-server-exact')).toHaveCSS(
'color',
f.mode === 'dev' ? 'rgb(0, 0, 0)' : 'rgb(200, 100, 50)',
)

// Client CSS (loaded via <link> in noJS mode)
// Query-aware: works in both dev and build
await expect(page.locator('.test-virtual-style-client-query')).toHaveCSS(
'color',
'rgb(50, 150, 100)',
)
// Exact-match: fails via <link> in dev (Vite limitation)
await expect(page.locator('.test-virtual-style-client-exact')).toHaveCSS(
'color',
f.mode === 'dev' ? 'rgb(0, 0, 0)' : 'rgb(200, 50, 100)',
)
})
}
2 changes: 2 additions & 0 deletions packages/plugin-rsc/examples/basic/src/routes/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { TestTreeShakeServer } from './tree-shake/server'
import { TestTreeShake2 } from './tree-shake2/server'
import { TestUseCache } from './use-cache/server'
import { TestUseId } from './use-id/server'
import { TestVirtualModule } from './virtual-module/server'

export function Root(props: { url: URL }) {
return (
Expand All @@ -70,6 +71,7 @@ export function Root(props: { url: URL }) {
<TestTailwind />
<TestDepCssInServer />
<TestHydrationMismatch url={props.url} />
<TestVirtualModule />
<TestHmrClientDep url={{ search: props.url.search }} />
<TestHmrClientDep2 url={{ search: props.url.search }} />
<TestHmrClientDep3 />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client'

// Client CSS is loaded via JS import (HMR injects styles)
// Both patterns work because no ?direct is involved
import 'virtual:test-style-client-query.css'
import 'virtual:test-style-client-exact.css'

export function TestClientWithVirtualCss() {
return (
<>
<div className="test-virtual-style-client-query">
test-virtual-style-client-query
</div>
<div className="test-virtual-style-client-exact">
test-virtual-style-client-exact
</div>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// @ts-expect-error virtual module
import { TestVirtualClient } from 'virtual:test-virtual-client'
import { TestClientWithVirtualCss } from './client'

// Server CSS is loaded via <link> tag in RSC
// Query-aware: works in dev (handles ?direct)
import 'virtual:test-style-server-query.css'
// Exact-match: fails in dev (Vite limitation), works in build
import 'virtual:test-style-server-exact.css'

export function TestVirtualModule() {
return (
<div data-testid="test-virtual-module">
<div className="test-virtual-style-server-query">
test-virtual-style-server-query
</div>
<div className="test-virtual-style-server-exact">
test-virtual-style-server-exact
</div>
<TestClientWithVirtualCss />
<TestVirtualClient />
</div>
)
}
83 changes: 83 additions & 0 deletions packages/plugin-rsc/examples/basic/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default defineConfig({
tailwindcss(),
react(),
vitePluginUseCache(),
vitePluginVirtualModuleTest(),
rsc({
entries: {
client: './src/framework/entry.browser.tsx',
Expand Down Expand Up @@ -334,3 +335,85 @@ function vitePluginUseCache(): Plugin[] {
},
]
}

function vitePluginVirtualModuleTest(): Plugin[] {
return [
{
name: 'test-virtual-client',
resolveId(source) {
if (source === 'virtual:test-virtual-client') {
return `\0${source}`
}
},
load(id) {
if (id === '\0virtual:test-virtual-client') {
return `
'use client'

import React from 'react'

export function TestVirtualClient() {
const [clicked, setClicked] = React.useState(false)
return React.createElement(
'button',
{
type: 'button',
'data-testid': 'test-virtual-client',
onClick: () => setClicked(true),
},
'test-virtual-client: ' + (clicked ? 'clicked' : 'not-clicked')
)
}
`
}
},
},
// Query-aware virtual CSS: handles ?direct query, works with <link> in dev
{
name: 'test-virtual-css-query-aware',
resolveId(source) {
const clean = source.split('?')[0]
if (
clean === 'virtual:test-style-server-query.css' ||
clean === 'virtual:test-style-client-query.css'
) {
// Preserve query in resolved id for Vite's CSS plugin to see ?direct
const query = source.includes('?')
? source.slice(source.indexOf('?'))
: ''
return `\0${clean}${query}`
}
},
load(id) {
const clean = id.split('?')[0]
if (clean === '\0virtual:test-style-server-query.css') {
return `.test-virtual-style-server-query { color: rgb(50, 100, 150); }`
}
if (clean === '\0virtual:test-style-client-query.css') {
return `.test-virtual-style-client-query { color: rgb(50, 150, 100); }`
}
},
},
// Exact-match virtual CSS: standard pattern, does NOT work with <link> in dev
// (works fine when imported via JS)
{
name: 'test-virtual-css-exact',
resolveId(source) {
if (source === 'virtual:test-style-server-exact.css') {
return `\0${source}`
}
if (source === 'virtual:test-style-client-exact.css') {
return `\0${source}`
}
},
load(id) {
if (id === '\0virtual:test-style-server-exact.css') {
return `.test-virtual-style-server-exact { color: rgb(200, 100, 50); }`
}
if (id === '\0virtual:test-style-client-exact.css') {
return `.test-virtual-style-client-exact { color: rgb(200, 50, 100); }`
}
},
},
]
}
9 changes: 7 additions & 2 deletions packages/plugin-rsc/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import { crawlFrameworkPkgs } from 'vitefu'
import vitePluginRscCore from './core/plugin'
import { cjsModuleRunnerPlugin } from './plugins/cjs'
import { vitePluginFindSourceMapURL } from './plugins/find-source-map-url'
import {
vitePluginResolvedIdProxy,
withResolvedIdProxy,
} from './plugins/resolved-id-proxy'
import { scanBuildStripPlugin } from './plugins/scan'
import {
parseCssVirtual,
Expand Down Expand Up @@ -299,6 +303,7 @@ export function vitePluginRscMinimal(
},
},
scanBuildStripPlugin({ manager }),
vitePluginResolvedIdProxy(),
]
}

Expand Down Expand Up @@ -1357,7 +1362,7 @@ function vitePluginUseClient(
if (manager.isScanBuild) {
let code = ``
for (const meta of Object.values(manager.clientReferenceMetaMap)) {
code += `import ${JSON.stringify(meta.importId)};\n`
code += `import ${JSON.stringify(withResolvedIdProxy(meta.importId))};\n`
}
return { code, map: null }
}
Expand Down Expand Up @@ -1411,7 +1416,7 @@ function vitePluginUseClient(
.sort()
.join('')
code += `
import * as import_${meta.referenceKey} from ${JSON.stringify(meta.importId)};
import * as import_${meta.referenceKey} from ${JSON.stringify(withResolvedIdProxy(meta.importId))};
export const export_${meta.referenceKey} = {${exports}};
`
}
Expand Down
Loading
Loading