Skip to content
Draft
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
27 changes: 27 additions & 0 deletions packages/plugin-rsc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ This module provides Vite-integrated RSC runtime APIs based on
- `decodeAction/decodeReply/decodeFormState/loadServerAction`
- `registerClientReference/registerServerReference`
- `createTemporaryReferenceSet`
- `setServerCssLinkProps` (see [below](#vite-specific-extension-setservercsslinkprops-experimental))

### `@vitejs/plugin-rsc/rsc/static`

Expand Down Expand Up @@ -544,6 +545,32 @@ function decodeReply(
// `decodeFormState(actionResult, body, extraOptions?)` accept the same `extraOptions`.
```

#### Vite-specific extension: `setServerCssLinkProps` (experimental)

> [!NOTE]
> This is a Vite-specific API. The official `react-server-dom` does not provide it.

`setServerCssLinkProps` registers a function that returns extra props for the `<link rel="stylesheet">` elements the plugin renders for server components' CSS, including the ones from [`import.meta.viteRsc.loadCss()`](#importmetaviterscloadcss). The plugin calls it while React renders each link, so it can read the framework's request-scoped state, for example a CSP nonce from the framework's own `AsyncLocalStorage`. The only supported prop is `nonce`, for a nonce-based CSP `style-src`.

```ts
import { AsyncLocalStorage } from 'node:async_hooks'
import {
renderToReadableStream,
setServerCssLinkProps,
} from '@vitejs/plugin-rsc/rsc/server'

const nonceStorage = new AsyncLocalStorage<string | undefined>()
setServerCssLinkProps(() => ({ nonce: nonceStorage.getStore() }))

const stream = nonceStorage.run(nonce, () => renderToReadableStream(payload))
```

Keep these in mind when choosing the value:

- On client navigation, React inserts the links from the RSC payload into the current document, so the nonce has to be the one of that document rather than one generated for the navigation request.
- A render whose output is cached across requests should not get a request's nonce, so run it outside the request's scope or return no nonce for it.
- Client components' CSS is linked during SSR through `ReactDOM.preinit`, which doesn't forward `nonce` for stylesheets, so those links don't get a nonce.

### `@vitejs/plugin-rsc/ssr`

This module provides Vite-integrated RSC runtime APIs based on
Expand Down
15 changes: 15 additions & 0 deletions packages/plugin-rsc/e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,21 @@ function defineTest(f: Fixture) {
).toBe(true)
})

testNoJs('css nonce', async ({ page }) => {
await page.goto(f.url())
const nonce = await page
.locator('meta[property="csp-nonce"]')
.evaluate((meta: HTMLMetaElement) => meta.nonce)
expect(nonce).toBeTruthy()
const nonces = await page
.locator('link[rel="stylesheet"][data-rsc-css-href]')
.evaluateAll((links) =>
links.map((link) => (link as HTMLLinkElement).nonce),
)
expect(nonces.length).toBeGreaterThan(0)
expect(nonces.every((value) => value === nonce)).toBe(true)
})

async function testCssBasic(page: Page) {
await testCss(page)
await expect(page.locator('.test-dep-css-in-server')).toHaveCSS(
Expand Down
22 changes: 17 additions & 5 deletions packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { AsyncLocalStorage } from 'node:async_hooks'
import {
renderToReadableStream,
createTemporaryReferenceSet,
decodeReply,
loadServerAction,
decodeAction,
decodeFormState,
setServerCssLinkProps,
} from '@vitejs/plugin-rsc/rsc/server'
import type React from 'react'
import type { ReactFormState } from 'react-dom/client'
import { parseRenderRequest } from './request.tsx'

// `Resources` renders server components' css links during the render below,
// so the framework provides their nonce through its own request-scoped storage.
const cssNonceStorage = new AsyncLocalStorage<string | undefined>()
setServerCssLinkProps(() => ({ nonce: cssNonceStorage.getStore() }))

export type RscPayload = {
root: React.ReactNode
returnValue?: { ok: boolean; data: unknown }
Expand Down Expand Up @@ -65,11 +72,16 @@ async function handleRequest({
const rscPayload: RscPayload = { root: getRoot(), formState, returnValue }
const rscOptions = { temporaryReferences }
const debugClientReferences: unknown[] = []
const rscStream = renderToReadableStream<RscPayload>(rscPayload, rscOptions, {
onClientReference(metadata) {
debugClientReferences.push(metadata)
},
})
const rscStream = cssNonceStorage.run(
// a client navigation's payload goes to a document with another nonce
renderRequest.isRsc ? undefined : nonce,
() =>
renderToReadableStream<RscPayload>(rscPayload, rscOptions, {
onClientReference(metadata) {
debugClientReferences.push(metadata)
},
}),
)

// test `onClientReference` callback
if (renderRequest.url.pathname === '/__test_onClientReference') {
Expand Down
32 changes: 32 additions & 0 deletions packages/plugin-rsc/src/core/rsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,35 @@ export interface CreateServerManifestOptions {
*/
onServerReference?: (metadata: ServerReferenceMetadata) => void
}

export interface ServerCssLinkProps {
/**
* CSP nonce for the stylesheet link.
*/
nonce?: string
}

export type GetServerCssLinkProps = (link: {
href: string
}) => ServerCssLinkProps | undefined

let getServerCssLinkProps: GetServerCssLinkProps | undefined

/**
* Registers a function that returns extra props for the stylesheet links
* rendered for server components' CSS. It's called while React renders the
* links, so it can read the framework's request-scoped state, such as a CSP
* nonce from its own `AsyncLocalStorage`.
* @experimental
*/
export function setServerCssLinkProps(
fn: GetServerCssLinkProps | undefined,
): void {
getServerCssLinkProps = fn
}

export function getServerCssLinkPropsInternal(link: {
href: string
}): ServerCssLinkProps | undefined {
return getServerCssLinkProps?.(link)
}
18 changes: 12 additions & 6 deletions packages/plugin-rsc/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from 'vite'
import { crawlFrameworkPkgs } from 'vitefu'
import vitePluginRscCore from './core/plugin'
import type { GetServerCssLinkProps } from './core/rsc'
import { cjsModuleRunnerPlugin } from './plugins/cjs'
import { vitePluginFindSourceMapURL } from './plugins/find-source-map-url'
import {
Expand Down Expand Up @@ -2742,21 +2743,24 @@ function generateResourcesCode(
const ResourcesFn = (
React: typeof import('react'),
deps: ResolvedAssetDeps,
RemoveDuplicateServerCss?: React.FC,
precedence?: string,
RemoveDuplicateServerCss: React.FC | undefined,
precedence: string | undefined,
getLinkProps: GetServerCssLinkProps,
) => {
return function Resources() {
return React.createElement(React.Fragment, null, [
...deps.css.map((href: string) =>
React.createElement('link', {
...deps.css.map((href: string) => {
const nonce = getLinkProps({ href })?.nonce
return React.createElement('link', {
key: 'css:' + href,
rel: 'stylesheet',
crossOrigin: 'anonymous',
...(nonce ? { nonce } : {}),
...(precedence ? { precedence } : {}),
href: href,
'data-rsc-css-href': href,
}),
),
})
}),
RemoveDuplicateServerCss &&
React.createElement(RemoveDuplicateServerCss, {
key: 'remove-duplicate-css',
Expand All @@ -2767,6 +2771,7 @@ function generateResourcesCode(

return `
import __vite_rsc_react__ from "react";
import { getServerCssLinkPropsInternal as __vite_rsc_get_server_css_link_props__ } from "${resolvePackage(`${PKG_NAME}/core/rsc`)}";

${
manager.config.command === 'serve'
Expand All @@ -2779,6 +2784,7 @@ export const Resources = (${ResourcesFn.toString()})(
${depsCode},
RemoveDuplicateServerCss,
${usePrecedence ? `"vite-rsc/importer-resources"` : `undefined`},
__vite_rsc_get_server_css_link_props__,
);
`
}
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-rsc/src/rsc/server.node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export {
createClientManifest,
createServerManifest,
loadServerAction,
setServerCssLinkProps,
type GetServerCssLinkProps,
type ServerCssLinkProps,
} from '../core/rsc'

export * from '../react/rsc/server.node'
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-rsc/src/rsc/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export {
createClientManifest,
createServerManifest,
loadServerAction,
setServerCssLinkProps,
type GetServerCssLinkProps,
type ServerCssLinkProps,
} from '../core/rsc'

export * from '../react/rsc/server'
Expand Down
Loading