Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 2 additions & 3 deletions packages/react-start-rsc/src/CompositeComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Suspense } from 'react'
import ReactDOM from 'react-dom'

import { SlotProvider } from './SlotContext'
import { preinitCssHrefs } from './preinitCssHrefs'
import {
RSC_PROXY_GET_TREE,
RSC_PROXY_PATH,
Expand Down Expand Up @@ -76,9 +77,7 @@ function CompositeRenderComponent({
cssHrefs?: ReadonlySet<string>
jsPreloads?: ReadonlySet<string>
}): React.ReactNode {
for (const href of cssHrefs ?? []) {
ReactDOM.preinit(href, { as: 'style', precedence: 'high' })
}
preinitCssHrefs(cssHrefs)

if (jsPreloads) {
for (const href of jsPreloads) {
Expand Down
5 changes: 2 additions & 3 deletions packages/react-start-rsc/src/RscNodeRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Suspense } from 'react'
import ReactDOM from 'react-dom'

import { preinitCssHrefs } from './preinitCssHrefs'
import {
RSC_PROXY_GET_TREE,
RSC_PROXY_PATH,
Expand Down Expand Up @@ -56,9 +57,7 @@ export function RscNodeRenderer({ data }: { data: any }): React.ReactNode {
)
}

for (const href of cssHrefs ?? []) {
ReactDOM.preinit(href, { as: 'style', precedence: 'high' })
}
preinitCssHrefs(cssHrefs)

if (jsPreloads) {
for (const href of jsPreloads) {
Expand Down
23 changes: 23 additions & 0 deletions packages/react-start-rsc/src/preinitCssHrefs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import ReactDOM from 'react-dom'

/**
* Vite's HMR cache-bust query, e.g. `?t=1776450256042`
* Vite only adds this during dev, never in a prod build
*/
const HMR_CACHE_BUST = /[?&]t=\d+/

/**
* Tells React 19 Float to start fetching each CSS file early and put a
* <link rel="stylesheet" data-precedence="high"> in <head> during production builds
*/
export function preinitCssHrefs(cssHrefs: Iterable<string> | undefined): void {
if (!cssHrefs) return
for (const href of cssHrefs) {
// Skip Vite's HMR cache-bust query so that no stale <link>s pile up
// in <head> during dev from HMR edits. In prod builds, there are no `?t=` hrefs
if (HMR_CACHE_BUST.test(href)) continue
Comment thread
jantimon marked this conversation as resolved.
Outdated
ReactDOM.preinit(href, { as: 'style', precedence: 'high' })
}
}

export const _internals = { HMR_CACHE_BUST }
94 changes: 94 additions & 0 deletions packages/react-start-rsc/tests/preinitCssHrefs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import ReactDOM from 'react-dom'

import { _internals, preinitCssHrefs } from '../src/preinitCssHrefs'

describe('HMR_CACHE_BUST', () => {
const { HMR_CACHE_BUST } = _internals

it.each([
'/src/components/Card.css?t=1776450256042',
'/assets/app.css?v=abc&t=123',
'/a.css?t=0',
])('matches Vite HMR cache-bust hrefs (%s)', (href) => {
expect(HMR_CACHE_BUST.test(href)).toBe(true)
})

it.each([
'/src/components/Card.css',
'/assets/app.css?v=abc',
'/a.Card.abc123.css',
'/a.css?theme=dark',
// `t=` without digits (not a Vite cache-bust)
'/a.css?t=abc',
// `t=` as a substring of a larger param name
'/a.css?format=3',
])('does not match non-cache-busted hrefs (%s)', (href) => {
expect(HMR_CACHE_BUST.test(href)).toBe(false)
})
})

describe('preinitCssHrefs', () => {
let preinitSpy: ReturnType<typeof vi.spyOn>

beforeEach(() => {
preinitSpy = vi.spyOn(ReactDOM, 'preinit').mockImplementation(() => {})
})

afterEach(() => {
preinitSpy.mockRestore()
})

it('preinits non-cache-busted hrefs with {as:"style", precedence:"high"}', () => {
preinitCssHrefs(['/assets/app.abc123.css'])
expect(preinitSpy).toHaveBeenCalledTimes(1)
expect(preinitSpy).toHaveBeenCalledWith('/assets/app.abc123.css', {
as: 'style',
precedence: 'high',
})
})

it('skips hrefs matching the HMR cache-bust pattern', () => {
preinitCssHrefs([
'/src/components/Card.css?t=1776450256042',
'/assets/app.css?v=abc&t=123',
])
expect(preinitSpy).not.toHaveBeenCalled()
})

it('preinits only the non-cache-busted subset from a mixed list', () => {
preinitCssHrefs([
'/a.css?t=1',
'/b.Card.abc.css',
'/c.css?t=2',
'/d.css?theme=dark',
])
expect(preinitSpy).toHaveBeenCalledTimes(2)
expect(preinitSpy).toHaveBeenNthCalledWith(1, '/b.Card.abc.css', {
as: 'style',
precedence: 'high',
})
expect(preinitSpy).toHaveBeenNthCalledWith(2, '/d.css?theme=dark', {
as: 'style',
precedence: 'high',
})
})

it.each([undefined, [], new Set<string>()])(
'is a no-op for empty or missing input (%#)',
(input) => {
preinitCssHrefs(input)
expect(preinitSpy).not.toHaveBeenCalled()
},
)

it('accepts a ReadonlySet<string> (the actual call-site type)', () => {
const hrefs: ReadonlySet<string> = new Set(['/a.css?t=1', '/b.abc.css'])
preinitCssHrefs(hrefs)
expect(preinitSpy).toHaveBeenCalledTimes(1)
expect(preinitSpy).toHaveBeenCalledWith('/b.abc.css', {
as: 'style',
precedence: 'high',
})
})
})
Loading