Skip to content
Open
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
43 changes: 43 additions & 0 deletions packages/plugin-rsc/e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,49 @@
const deps =
manifest.clientReferenceDeps[hashString('src/routes/client.tsx')]
expect(srcs).toEqual(expect.arrayContaining(deps.js))
expect(manifest.clientEntryDeps.js.length).toBeGreaterThan(0)
expect(deps.js).toEqual(
expect.arrayContaining(manifest.clientEntryDeps.js),
)
expect(
deps.js.some(
(href: string) => !manifest.clientEntryDeps.js.includes(href),
),
).toBe(true)

const clientOnlyDependencyCounts = new Map<string, number>()
for (const referenceDeps of Object.values(
manifest.clientReferenceDeps,
) as { js: string[] }[]) {
for (const href of referenceDeps.js) {
if (!manifest.clientEntryDeps.js.includes(href)) {
clientOnlyDependencyCounts.set(
href,
(clientOnlyDependencyCounts.get(href) ?? 0) + 1,
)
}
}
}
expect(
[...clientOnlyDependencyCounts.values()].some((count) => count > 1),
).toBe(true)

const clientBuildManifest: import('vite').Manifest = JSON.parse(
readFileSync(f.root + '/dist/client/.vite/manifest.json', 'utf-8'),
)
const allReferenceJs = new Set(
Object.values(manifest.clientReferenceDeps).flatMap(
(referenceDeps) => (referenceDeps as { js: string[] }).js,
),
)
for (const dynamicModuleId of [
'src/routes/browser-only/browser-dep.tsx',
'src/routes/lazy-css/client-to-client-child.tsx',
]) {
const dynamicChunk = clientBuildManifest[dynamicModuleId]
expect(dynamicChunk).toBeDefined()
expect(allReferenceJs).not.toContain(`/${dynamicChunk!.file}`)
}
})
})

Expand Down Expand Up @@ -927,7 +970,7 @@
`/* color: rgb(0, 165, 255); */`,
),
)
await expect(page.locator('.test-style-server')).toHaveCSS(

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

View workflow job for this annotation

GitHub Actions / test-rsc (ubuntu-latest / chromium) (react-canary)

[chromium] › e2e/basic.test.ts:956:5 › dev-default › css hmr server

1) [chromium] › e2e/basic.test.ts:956:5 › dev-default › css hmr server ─────────────────────────── Error: expect(locator).toHaveCSS(expected) failed Locator: locator('.test-style-server') Expected: "rgb(0, 0, 0)" Received: "rgb(0, 165, 255)" Timeout: 5000ms Call log: - Expect "toHaveCSS" with timeout 5000ms - waiting for locator('.test-style-server') 14 × locator resolved to <div class="test-style-server">test-style-server</div> - unexpected value "rgb(0, 165, 255)" 971 | ), 972 | ) > 973 | await expect(page.locator('.test-style-server')).toHaveCSS( | ^ 974 | 'color', 975 | 'rgb(0, 0, 0)', 976 | ) at /home/runner/work/vite-plugin-react/vite-plugin-react/packages/plugin-rsc/e2e/basic.test.ts:973:56

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

View workflow job for this annotation

GitHub Actions / test-rsc (ubuntu-latest / chromium)

[chromium] › e2e/basic.test.ts:956:5 › dev-default › css hmr server

1) [chromium] › e2e/basic.test.ts:956:5 › dev-default › css hmr server ─────────────────────────── Error: expect(locator).toHaveCSS(expected) failed Locator: locator('.test-style-server') Expected: "rgb(0, 0, 0)" Received: "rgb(0, 165, 255)" Timeout: 5000ms Call log: - Expect "toHaveCSS" with timeout 5000ms - waiting for locator('.test-style-server') 14 × locator resolved to <div class="test-style-server">test-style-server</div> - unexpected value "rgb(0, 165, 255)" 971 | ), 972 | ) > 973 | await expect(page.locator('.test-style-server')).toHaveCSS( | ^ 974 | 'color', 975 | 'rgb(0, 0, 0)', 976 | ) at /home/runner/work/vite-plugin-react/vite-plugin-react/packages/plugin-rsc/e2e/basic.test.ts:973:56
'color',
'rgb(0, 0, 0)',
)
Expand Down Expand Up @@ -1178,7 +1221,7 @@
'rgb(0, 165, 255)',
)
editor.reset()
await expect(page.locator('.test-style-url-server')).toHaveCSS(

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

View workflow job for this annotation

GitHub Actions / test-rsc (ubuntu-latest / chromium)

[chromium] › e2e/basic.test.ts:1213:5 › dev-default › css url server hmr

2) [chromium] › e2e/basic.test.ts:1213:5 › dev-default › css url server hmr ────────────────────── Error: expect(locator).toHaveCSS(expected) failed Locator: locator('.test-style-url-server') Expected: "rgb(255, 165, 0)" Received: "rgb(0, 165, 255)" Timeout: 5000ms Call log: - Expect "toHaveCSS" with timeout 5000ms - waiting for locator('.test-style-url-server') 14 × locator resolved to <div class="test-style-url-server">test-style-url-server</div> - unexpected value "rgb(0, 165, 255)" 1222 | ) 1223 | editor.reset() > 1224 | await expect(page.locator('.test-style-url-server')).toHaveCSS( | ^ 1225 | 'color', 1226 | 'rgb(255, 165, 0)', 1227 | ) at /home/runner/work/vite-plugin-react/vite-plugin-react/packages/plugin-rsc/e2e/basic.test.ts:1224:60
'color',
'rgb(255, 165, 0)',
)
Expand Down
49 changes: 49 additions & 0 deletions packages/plugin-rsc/e2e/preload-priority.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import fs from 'node:fs'
import { expect, test } from '@playwright/test'
import { setupInlineFixture, useFixture } from './fixture'

test.describe('custom client entry preload priority', () => {
const root = 'examples/e2e/temp/preload-priority-custom-entry'

test.beforeAll(async () => {
await setupInlineFixture({
src: 'examples/starter-extra',
dest: root,
files: {
'vite.config.ts': {
edit: (source) =>
source.replace('rsc()', 'rsc({ customClientEntry: true })'),
},
'src/framework/entry.ssr.tsx': {
edit: (source) =>
source.replace(
` const bootstrapScriptContent =
await import.meta.viteRsc.loadBootstrapScriptContent('index')`,
` const bootstrapScriptContent = undefined`,
),
},
},
})
})

const f = useFixture({ root, mode: 'build' })

test('leaves framework-managed entry dependencies unclassified', () => {
const manifest = JSON.parse(
fs
.readFileSync(
f.root + '/dist/ssr/__vite_rsc_assets_manifest.js',
'utf-8',
)
.slice('export default '.length),
)

expect(manifest.clientEntryDeps).toBeUndefined()
expect(Object.keys(manifest.clientReferenceDeps).length).toBeGreaterThan(0)
expect(
Object.values(manifest.clientReferenceDeps).some(
(deps) => (deps as { js: string[] }).js.length > 0,
),
).toBe(true)
})
})
26 changes: 26 additions & 0 deletions packages/plugin-rsc/e2e/render-built-url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,32 @@ test.describe(() => {
expect(manifestFileContent).toContain(
`__dynamicBase + "assets/entry.rsc-`,
)

const manifest = Function(
'__dynamicBase',
manifestFileContent.replace('export default', 'return'),
)('/custom-server/')
expect(manifest.clientEntryDeps.js.length).toBeGreaterThan(0)
expect(
manifest.clientEntryDeps.js.every((href: string) =>
href.startsWith('/custom-server/'),
),
).toBe(true)

const referenceDeps = Object.values(manifest.clientReferenceDeps) as {
js: string[]
}[]
expect(referenceDeps.length).toBeGreaterThan(0)
for (const deps of referenceDeps) {
expect(deps.js).toEqual(
expect.arrayContaining(manifest.clientEntryDeps.js),
)
}
expect(
referenceDeps.some((deps) =>
deps.js.some((href) => !manifest.clientEntryDeps.js.includes(href)),
),
).toBe(true)
})
})
})
Expand Down
152 changes: 152 additions & 0 deletions packages/plugin-rsc/src/core/ssr-resources.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import * as ReactDOM from 'react-dom'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { preloadClientReferenceDeps } from './ssr-resources'

vi.mock('react-dom', () => ({
preloadModule: vi.fn(),
preinit: vi.fn(),
}))

describe(preloadClientReferenceDeps, () => {
beforeEach(() => {
vi.clearAllMocks()
})

test('preloads client reference JavaScript with low priority', () => {
preloadClientReferenceDeps(
{
js: ['/assets/client.js', '/assets/shared.js'],
css: [],
},
[],
true,
)

expect(ReactDOM.preloadModule).toHaveBeenCalledTimes(2)
expect(ReactDOM.preloadModule).toHaveBeenNthCalledWith(
1,
'/assets/client.js',
{
as: 'script',
crossOrigin: '',
fetchPriority: 'low',
},
)
expect(ReactDOM.preloadModule).toHaveBeenNthCalledWith(
2,
'/assets/shared.js',
{
as: 'script',
crossOrigin: '',
fetchPriority: 'low',
},
)
})

test('preserves default priority for client entry dependencies', () => {
preloadClientReferenceDeps(
{
js: [
'/assets/client.js',
'/assets/entry.js',
'/assets/entry-shared.js',
],
css: [],
},
['/assets/entry.js', '/assets/entry-shared.js'],
true,
)

expect(ReactDOM.preloadModule).toHaveBeenNthCalledWith(
1,
'/assets/client.js',
{
as: 'script',
crossOrigin: '',
fetchPriority: 'low',
},
)
expect(ReactDOM.preloadModule).toHaveBeenNthCalledWith(
2,
'/assets/entry.js',
{
as: 'script',
crossOrigin: '',
},
)
expect(ReactDOM.preloadModule).toHaveBeenNthCalledWith(
3,
'/assets/entry-shared.js',
{
as: 'script',
crossOrigin: '',
},
)
})

test('preserves default priority when a reference only uses entry dependencies', () => {
preloadClientReferenceDeps(
{
js: ['/assets/entry.js', '/assets/entry-shared.js'],
css: [],
},
['/assets/entry.js', '/assets/entry-shared.js'],
true,
)

expect(ReactDOM.preloadModule).toHaveBeenCalledTimes(2)
expect(ReactDOM.preloadModule).toHaveBeenNthCalledWith(
1,
'/assets/entry.js',
{
as: 'script',
crossOrigin: '',
},
)
expect(ReactDOM.preloadModule).toHaveBeenNthCalledWith(
2,
'/assets/entry-shared.js',
{
as: 'script',
crossOrigin: '',
},
)
})

test('keeps client reference CSS render blocking', () => {
preloadClientReferenceDeps(
{
js: [],
css: ['/assets/client.css'],
},
[],
true,
)

expect(ReactDOM.preinit).toHaveBeenCalledWith('/assets/client.css', {
as: 'style',
precedence: 'vite-rsc/client-reference',
})
})

test('supports disabling CSS link precedence without changing JS priority', () => {
preloadClientReferenceDeps(
{
js: ['/assets/client.js'],
css: ['/assets/client.css'],
},
[],
false,
)

expect(ReactDOM.preloadModule).toHaveBeenCalledWith('/assets/client.js', {
as: 'script',
crossOrigin: '',
fetchPriority: 'low',
})
expect(ReactDOM.preinit).toHaveBeenCalledWith('/assets/client.css', {
as: 'style',
precedence: undefined,
})
})
})
38 changes: 38 additions & 0 deletions packages/plugin-rsc/src/core/ssr-resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as ReactDOM from 'react-dom'
import type { ResolvedAssetDeps } from '../plugin'

type PreloadModuleOptionsWithFetchPriority = NonNullable<
Parameters<typeof ReactDOM.preloadModule>[1]
> & {
fetchPriority?: 'high' | 'low' | 'auto'
}

export function preloadClientReferenceDeps(
deps: ResolvedAssetDeps,
clientEntryJs: readonly string[],
cssLinkPrecedence: boolean,
): void {
// Remove this cast once React's public types include fetchPriority.
const preloadModule = ReactDOM.preloadModule as (
href: string,
options: PreloadModuleOptionsWithFetchPriority,
) => void

const clientEntryJsSet = new Set(clientEntryJs)
for (const href of deps.js) {
const options: PreloadModuleOptionsWithFetchPriority = {
as: 'script',
crossOrigin: '',
}
if (!clientEntryJsSet.has(href)) {
options.fetchPriority = 'low'
}
preloadModule(href, options)
}
for (const href of deps.css) {
ReactDOM.preinit(href, {
as: 'style',
precedence: cssLinkPrecedence ? 'vite-rsc/client-reference' : undefined,
})
}
}
8 changes: 6 additions & 2 deletions packages/plugin-rsc/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,7 @@ export function createRpcClient(params) {

const assetDeps = collectAssetDeps(bundle)
let bootstrapScriptContent: string | RuntimeAsset = ''
let clientEntryDeps: AssetDeps | undefined

const clientReferenceDeps: Record<string, AssetDeps> = {}
for (const meta of Object.values(manager.clientReferenceMetaMap)) {
Expand All @@ -1182,9 +1183,9 @@ export function createRpcClient(params) {
`[vite-rsc] Client build must have an entry chunk named "index". Use 'customClientEntry' option to disable this requirement.`,
)
}
const entryDeps = assetsURLOfDeps(entry.deps, manager)
clientEntryDeps = assetsURLOfDeps(entry.deps, manager)
for (const [key, deps] of Object.entries(clientReferenceDeps)) {
clientReferenceDeps[key] = mergeAssetDeps(deps, entryDeps)
clientReferenceDeps[key] = mergeAssetDeps(deps, clientEntryDeps)
}
const entryUrl = assetsURL(entry.chunk.fileName, manager)
if (typeof entryUrl === 'string') {
Expand All @@ -1198,6 +1199,7 @@ export function createRpcClient(params) {

manager.buildAssetsManifest = {
bootstrapScriptContent,
clientEntryDeps,
clientReferenceDeps,
serverResources,
cssLinkPrecedence: rscPluginOptions.cssLinkPrecedence,
Expand Down Expand Up @@ -2190,6 +2192,7 @@ function assetsURLOfDeps(deps: AssetDeps, manager: RscPluginManager) {

export type AssetsManifest = {
bootstrapScriptContent: string | RuntimeAsset
clientEntryDeps?: AssetDeps
clientReferenceDeps: Record<string, AssetDeps>
serverResources?: Record<string, Pick<AssetDeps, 'css'>>
cssLinkPrecedence?: boolean
Expand All @@ -2202,6 +2205,7 @@ export type AssetDeps = {

export type ResolvedAssetsManifest = {
bootstrapScriptContent: string
clientEntryDeps?: ResolvedAssetDeps
clientReferenceDeps: Record<string, ResolvedAssetDeps>
serverResources?: Record<string, Pick<ResolvedAssetDeps, 'css'>>
cssLinkPrecedence?: boolean
Expand Down
Loading
Loading