From 6cd5f46f83324b672c2db84aa006800f59112865 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 21 Jul 2025 12:09:40 +0900 Subject: [PATCH 1/8] chore(rsc): remove `@vite/plugin-rsc/extra` API from examples --- .../basic/src/framework/entry.browser.tsx | 127 ++++++++++++++++++ .../basic/src/framework/entry.rsc.tsx | 107 +++++++++++++++ .../basic/src/framework/entry.ssr.tsx | 53 ++++++++ .../examples/basic/src/framework/react.d.ts | 3 + .../plugin-rsc/examples/basic/src/server.tsx | 6 +- .../plugin-rsc/examples/basic/vite.config.ts | 6 +- 6 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 packages/plugin-rsc/examples/basic/src/framework/entry.browser.tsx create mode 100644 packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx create mode 100644 packages/plugin-rsc/examples/basic/src/framework/entry.ssr.tsx create mode 100644 packages/plugin-rsc/examples/basic/src/framework/react.d.ts diff --git a/packages/plugin-rsc/examples/basic/src/framework/entry.browser.tsx b/packages/plugin-rsc/examples/basic/src/framework/entry.browser.tsx new file mode 100644 index 000000000..9ba4ae4ee --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/framework/entry.browser.tsx @@ -0,0 +1,127 @@ +import * as ReactClient from '@vitejs/plugin-rsc/browser' +import { getRscStreamFromHtml } from '@vitejs/plugin-rsc/rsc-html-stream/browser' +import React from 'react' +import * as ReactDOMClient from 'react-dom/client' +import type { RscPayload } from './entry.rsc' + +async function main() { + // stash `setPayload` function to trigger re-rendering + // from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr) + let setPayload: (v: RscPayload) => void + + // deserialize RSC stream back to React VDOM for CSR + const initialPayload = await ReactClient.createFromReadableStream( + // initial RSC stream is injected in SSR stream as + getRscStreamFromHtml(), + ) + + // browser root component to (re-)render RSC payload as state + function BrowserRoot() { + const [payload, setPayload_] = React.useState(initialPayload) + + React.useEffect(() => { + setPayload = (v) => React.startTransition(() => setPayload_(v)) + }, [setPayload_]) + + // re-fetch/render on client side navigation + React.useEffect(() => { + return listenNavigation(() => fetchRscPayload()) + }, []) + + return payload.root + } + + // re-fetch RSC and trigger re-rendering + async function fetchRscPayload() { + const payload = await ReactClient.createFromFetch( + fetch(window.location.href), + ) + setPayload(payload) + } + + // register a handler which will be internally called by React + // on server function request after hydration. + ReactClient.setServerCallback(async (id, args) => { + const url = new URL(window.location.href) + const temporaryReferences = ReactClient.createTemporaryReferenceSet() + const payload = await ReactClient.createFromFetch( + fetch(url, { + method: 'POST', + body: await ReactClient.encodeReply(args, { temporaryReferences }), + headers: { + 'x-rsc-action': id, + }, + }), + { temporaryReferences }, + ) + setPayload(payload) + return payload.returnValue + }) + + // hydration + const browserRoot = ( + + + + ) + ReactDOMClient.hydrateRoot(document, browserRoot, { + formState: initialPayload.formState, + }) + + // implement server HMR by trigering re-fetch/render of RSC upon server code change + if (import.meta.hot) { + import.meta.hot.on('rsc:update', () => { + fetchRscPayload() + }) + } +} + +// a little helper to setup events interception for client side navigation +function listenNavigation(onNavigation: () => void) { + window.addEventListener('popstate', onNavigation) + + const oldPushState = window.history.pushState + window.history.pushState = function (...args) { + const res = oldPushState.apply(this, args) + onNavigation() + return res + } + + const oldReplaceState = window.history.replaceState + window.history.replaceState = function (...args) { + const res = oldReplaceState.apply(this, args) + onNavigation() + return res + } + + function onClick(e: MouseEvent) { + let link = (e.target as Element).closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + e.button === 0 && // left clicks only + !e.metaKey && // open in new tab (mac) + !e.ctrlKey && // open in new tab (windows) + !e.altKey && // download + !e.shiftKey && + !e.defaultPrevented + ) { + e.preventDefault() + history.pushState(null, '', link.href) + } + } + document.addEventListener('click', onClick) + + return () => { + document.removeEventListener('click', onClick) + window.removeEventListener('popstate', onNavigation) + window.history.pushState = oldPushState + window.history.replaceState = oldReplaceState + } +} + +main() diff --git a/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx new file mode 100644 index 000000000..256d4ed67 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx @@ -0,0 +1,107 @@ +import * as ReactServer from '@vitejs/plugin-rsc/rsc' +import type { ReactFormState } from 'react-dom/client' +import type React from 'react' + +// The schema of payload which is serialized into RSC stream on rsc environment +// and deserialized on ssr/client environments. +export type RscPayload = { + // this demo renders/serializes/deserizlies entire root html element + // but this mechanism can be changed to render/fetch different parts of components + // based on your own route conventions. + root: React.ReactNode + // server action return value of non-progressive enhancement case + returnValue?: unknown + // server action form state (e.g. useActionState) of progressive enhancement case + formState?: ReactFormState +} + +// the plugin by default assumes `rsc` entry having default export of request handler. +// however, how server entries are executed can be customized by registering +// own server handler e.g. `@cloudflare/vite-plugin`. +export async function handleRequest({ + request, + getRoot, + nonce, +}: { + request: Request + getRoot: () => React.ReactNode + nonce?: string +}): Promise { + // handle server function request + const isAction = request.method === 'POST' + let returnValue: unknown | undefined + let formState: ReactFormState | undefined + let temporaryReferences: unknown | undefined + if (isAction) { + // x-rsc-action header exists when action is called via `ReactClient.setServerCallback`. + const actionId = request.headers.get('x-rsc-action') + if (actionId) { + const contentType = request.headers.get('content-type') + const body = contentType?.startsWith('multipart/form-data') + ? await request.formData() + : await request.text() + temporaryReferences = ReactServer.createTemporaryReferenceSet() + const args = await ReactServer.decodeReply(body, { temporaryReferences }) + const action = await ReactServer.loadServerAction(actionId) + returnValue = await action.apply(null, args) + } else { + // otherwise server function is called via `
` + // before hydration (e.g. when javascript is disabled). + // aka progressive enhancement. + const formData = await request.formData() + const decodedAction = await ReactServer.decodeAction(formData) + const result = await decodedAction() + formState = await ReactServer.decodeFormState(result, formData) + } + } + + const url = new URL(request.url) + const rscStream = ReactServer.renderToReadableStream({ + root: getRoot(), + returnValue, + formState, + }) + + // respond RSC stream without HTML rendering based on framework's convention. + // here we use request header `content-type`. + // additionally we allow `?__rsc` and `?__html` to easily view payload directly. + const isRscRequest = + (!request.headers.get('accept')?.includes('text/html') && + !url.searchParams.has('__html')) || + url.searchParams.has('__rsc') + + if (isRscRequest) { + return new Response(rscStream, { + headers: { + 'content-type': 'text/x-component;charset=utf-8', + vary: 'accept', + }, + }) + } + + // Delegate to SSR environment for html rendering. + // The plugin provides `loadSsrModule` helper to allow loading SSR environment entry module + // in RSC environment. however this can be customized by implementing own runtime communication + // e.g. `@cloudflare/vite-plugin`'s service binding. + const ssrEntryModule = await import.meta.viteRsc.loadModule< + typeof import('./entry.ssr.tsx') + >('ssr', 'index') + const htmlStream = await ssrEntryModule.renderHTML(rscStream, { + formState, + nonce, + // allow quick simulation of javscript disabled browser + debugNojs: url.searchParams.has('__nojs'), + }) + + // respond html + return new Response(htmlStream, { + headers: { + 'Content-type': 'text/html', + vary: 'accept', + }, + }) +} + +if (import.meta.hot) { + import.meta.hot.accept() +} diff --git a/packages/plugin-rsc/examples/basic/src/framework/entry.ssr.tsx b/packages/plugin-rsc/examples/basic/src/framework/entry.ssr.tsx new file mode 100644 index 000000000..129dbadf1 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/framework/entry.ssr.tsx @@ -0,0 +1,53 @@ +import { injectRscStreamToHtml } from '@vitejs/plugin-rsc/rsc-html-stream/ssr' // helper API +import * as ReactClient from '@vitejs/plugin-rsc/ssr' // RSC API +import React from 'react' +import type { ReactFormState } from 'react-dom/client' +import * as ReactDOMServer from 'react-dom/server.edge' +import type { RscPayload } from './entry.rsc' + +export async function renderHTML( + rscStream: ReadableStream, + options: { + formState?: ReactFormState + nonce?: string + debugNojs?: boolean + }, +) { + // duplicate one RSC stream into two. + // - one for SSR (ReactClient.createFromReadableStream below) + // - another for browser hydration payload by injecting . + const [rscStream1, rscStream2] = rscStream.tee() + + // deserialize RSC stream back to React VDOM + let payload: Promise + function SsrRoot() { + // deserialization needs to be kicked off inside ReactDOMServer context + // for ReactDomServer preinit/preloading to work + payload ??= ReactClient.createFromReadableStream(rscStream1) + return React.use(payload).root + } + + // render html (traditional SSR) + const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent('index') + const htmlStream = await ReactDOMServer.renderToReadableStream(, { + bootstrapScriptContent: options?.debugNojs + ? undefined + : bootstrapScriptContent, + nonce: options?.nonce, + // no types + ...{ formState: options?.formState }, + }) + + let responseStream: ReadableStream = htmlStream + if (!options?.debugNojs) { + // initial RSC stream is injected in HTML stream as + responseStream = responseStream.pipeThrough( + injectRscStreamToHtml(rscStream2, { + nonce: options?.nonce, + }), + ) + } + + return responseStream +} diff --git a/packages/plugin-rsc/examples/basic/src/framework/react.d.ts b/packages/plugin-rsc/examples/basic/src/framework/react.d.ts new file mode 100644 index 000000000..d92ea675b --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/framework/react.d.ts @@ -0,0 +1,3 @@ +declare module 'react-dom/server.edge' { + export * from 'react-dom/server' +} diff --git a/packages/plugin-rsc/examples/basic/src/server.tsx b/packages/plugin-rsc/examples/basic/src/server.tsx index 86b9234d3..4cd20064f 100644 --- a/packages/plugin-rsc/examples/basic/src/server.tsx +++ b/packages/plugin-rsc/examples/basic/src/server.tsx @@ -1,5 +1,5 @@ +import { handleRequest } from './framework/entry.rsc.tsx' import './styles.css' -import { renderRequest } from '@vitejs/plugin-rsc/extra/rsc' export default async function handler(request: Request): Promise { const url = new URL(request.url) @@ -11,8 +11,8 @@ export default async function handler(request: Request): Promise { ) const nonce = !process.env.NO_CSP ? crypto.randomUUID() : undefined - const response = await renderRequest(request, root, { nonce }) - if (nonce) { + const response = await handleRequest({ request, getRoot: () => root }) + if (nonce && response.headers.get('content-type')?.includes('text/html')) { response.headers.set( 'content-security-policy', `default-src 'self'; ` + diff --git a/packages/plugin-rsc/examples/basic/vite.config.ts b/packages/plugin-rsc/examples/basic/vite.config.ts index 35e31d2ab..9a3b534c1 100644 --- a/packages/plugin-rsc/examples/basic/vite.config.ts +++ b/packages/plugin-rsc/examples/basic/vite.config.ts @@ -33,9 +33,9 @@ export default defineConfig({ vitePluginUseCache(), rsc({ entries: { - client: './src/client.tsx', - ssr: './src/server.ssr.tsx', - rsc: './src/server.tsx', + client: './src/framework/entry.browser.tsx', + ssr: './src/framework/entry.ssr.tsx', + rsc: './src/framework/entry.rsc.tsx', }, // disable auto css injection to manually test `loadCss` feature. rscCssTransform: false, From 27efb004da3e6ed7aaf3fa01c1523c905d615b4f Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 21 Jul 2025 12:12:09 +0900 Subject: [PATCH 2/8] chore: cleanup --- packages/plugin-rsc/README.md | 1 - packages/plugin-rsc/examples/basic/src/client.tsx | 3 --- packages/plugin-rsc/examples/basic/src/server.ssr.tsx | 1 - packages/plugin-rsc/examples/basic/vite.config.ts | 2 +- 4 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 packages/plugin-rsc/examples/basic/src/client.tsx delete mode 100644 packages/plugin-rsc/examples/basic/src/server.ssr.tsx diff --git a/packages/plugin-rsc/README.md b/packages/plugin-rsc/README.md index 440d6f516..8aadcb337 100644 --- a/packages/plugin-rsc/README.md +++ b/packages/plugin-rsc/README.md @@ -25,7 +25,6 @@ npx degit vitejs/vite-plugin-react/packages/plugin-rsc/examples/starter my-app - This demonstrates how to integrate [experimental React Router RSC API](https://remix.run/blog/rsc-preview). React Router now provides [official RSC support](https://reactrouter.com/how-to/react-server-components), so it's recommended to follow React Router's official documentation for the latest integration. - [`./examples/basic`](./examples/basic) - This is mainly used for e2e testing and include various advanced RSC usages (e.g. `"use cache"` example). - It also uses a high level `@vitejs/plugin-rsc/extra/{rsc,ssr,browser}` API for quick setup. - [`./examples/ssg`](./examples/ssg) - Static site generation (SSG) example with MDX and client components for interactivity. diff --git a/packages/plugin-rsc/examples/basic/src/client.tsx b/packages/plugin-rsc/examples/basic/src/client.tsx deleted file mode 100644 index ebbd43356..000000000 --- a/packages/plugin-rsc/examples/basic/src/client.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { hydrate } from '@vitejs/plugin-rsc/extra/browser' - -hydrate() diff --git a/packages/plugin-rsc/examples/basic/src/server.ssr.tsx b/packages/plugin-rsc/examples/basic/src/server.ssr.tsx deleted file mode 100644 index 651769496..000000000 --- a/packages/plugin-rsc/examples/basic/src/server.ssr.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from '@vitejs/plugin-rsc/extra/ssr' diff --git a/packages/plugin-rsc/examples/basic/vite.config.ts b/packages/plugin-rsc/examples/basic/vite.config.ts index 9a3b534c1..a3fec2fdf 100644 --- a/packages/plugin-rsc/examples/basic/vite.config.ts +++ b/packages/plugin-rsc/examples/basic/vite.config.ts @@ -35,7 +35,7 @@ export default defineConfig({ entries: { client: './src/framework/entry.browser.tsx', ssr: './src/framework/entry.ssr.tsx', - rsc: './src/framework/entry.rsc.tsx', + rsc: './src/server.tsx', }, // disable auto css injection to manually test `loadCss` feature. rscCssTransform: false, From db012325dfcf6a5c9b90853f8283517f3f4e8d3e Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 21 Jul 2025 12:16:03 +0900 Subject: [PATCH 3/8] chore: move use-cache-runtime --- .../examples/basic/src/{ => framework}/use-cache-runtime.tsx | 0 packages/plugin-rsc/examples/basic/vite.config.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/plugin-rsc/examples/basic/src/{ => framework}/use-cache-runtime.tsx (100%) diff --git a/packages/plugin-rsc/examples/basic/src/use-cache-runtime.tsx b/packages/plugin-rsc/examples/basic/src/framework/use-cache-runtime.tsx similarity index 100% rename from packages/plugin-rsc/examples/basic/src/use-cache-runtime.tsx rename to packages/plugin-rsc/examples/basic/src/framework/use-cache-runtime.tsx diff --git a/packages/plugin-rsc/examples/basic/vite.config.ts b/packages/plugin-rsc/examples/basic/vite.config.ts index a3fec2fdf..8b6cceecb 100644 --- a/packages/plugin-rsc/examples/basic/vite.config.ts +++ b/packages/plugin-rsc/examples/basic/vite.config.ts @@ -175,7 +175,7 @@ function vitePluginUseCache(): Plugin[] { }) if (!result.output.hasChanged()) return result.output.prepend( - `import __vite_rsc_cache from "/src/use-cache-runtime";`, + `import __vite_rsc_cache from "/src/framework/use-cache-runtime";`, ) return { code: result.output.toString(), From 08d462fd2f516de6cfcd9e93ba4a06c8a24024c1 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 21 Jul 2025 12:18:14 +0900 Subject: [PATCH 4/8] chore: fix nonce --- .../examples/basic/src/routes/use-cache/server.tsx | 2 +- packages/plugin-rsc/examples/basic/src/server.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/examples/basic/src/routes/use-cache/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/use-cache/server.tsx index d90f7496a..9c5e09f4c 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/use-cache/server.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/use-cache/server.tsx @@ -1,4 +1,4 @@ -import { revalidateCache } from '../../use-cache-runtime' +import { revalidateCache } from '../../framework/use-cache-runtime' export function TestUseCache() { return ( diff --git a/packages/plugin-rsc/examples/basic/src/server.tsx b/packages/plugin-rsc/examples/basic/src/server.tsx index 4cd20064f..e5ecdc5b7 100644 --- a/packages/plugin-rsc/examples/basic/src/server.tsx +++ b/packages/plugin-rsc/examples/basic/src/server.tsx @@ -11,7 +11,11 @@ export default async function handler(request: Request): Promise { ) const nonce = !process.env.NO_CSP ? crypto.randomUUID() : undefined - const response = await handleRequest({ request, getRoot: () => root }) + const response = await handleRequest({ + request, + getRoot: () => root, + nonce, + }) if (nonce && response.headers.get('content-type')?.includes('text/html')) { response.headers.set( 'content-security-policy', From b2813d80d9c7b11fb6c548bae3d10771a3b89a1b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 21 Jul 2025 12:43:56 +0900 Subject: [PATCH 5/8] chore: tweak build test --- packages/plugin-rsc/examples/basic/vite.config.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/examples/basic/vite.config.ts b/packages/plugin-rsc/examples/basic/vite.config.ts index 8b6cceecb..051bb728b 100644 --- a/packages/plugin-rsc/examples/basic/vite.config.ts +++ b/packages/plugin-rsc/examples/basic/vite.config.ts @@ -79,11 +79,15 @@ export default defineConfig({ assert(typeof viteManifest.source === 'string') if (this.environment.name === 'rsc') { assert(viteManifest.source.includes('src/server.tsx')) - assert(!viteManifest.source.includes('src/client.tsx')) + assert( + !viteManifest.source.includes('src/framework/entry.browser.tsx'), + ) } if (this.environment.name === 'client') { assert(!viteManifest.source.includes('src/server.tsx')) - assert(viteManifest.source.includes('src/client.tsx')) + assert( + viteManifest.source.includes('src/framework/entry.browser.tsx'), + ) } }, }, From 0d74403e8214e9bd8359444f66a0cdcbf19f7794 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 21 Jul 2025 12:57:59 +0900 Subject: [PATCH 6/8] fix: fix temporary references --- .../examples/basic/src/framework/entry.rsc.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx index 256d4ed67..e4827bf69 100644 --- a/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx +++ b/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx @@ -56,11 +56,12 @@ export async function handleRequest({ } const url = new URL(request.url) - const rscStream = ReactServer.renderToReadableStream({ - root: getRoot(), - returnValue, - formState, - }) + const rscPayload: RscPayload = { root: getRoot(), formState, returnValue } + const rscOptions = { temporaryReferences } + const rscStream = ReactServer.renderToReadableStream( + rscPayload, + rscOptions, + ) // respond RSC stream without HTML rendering based on framework's convention. // here we use request header `content-type`. From 5c40f84493b2d85a86d9b5a07357f80ccdfae99e Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 21 Jul 2025 13:11:36 +0900 Subject: [PATCH 7/8] fix: fix vite dev style nonce --- packages/plugin-rsc/examples/basic/src/server.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/plugin-rsc/examples/basic/src/server.tsx b/packages/plugin-rsc/examples/basic/src/server.tsx index e5ecdc5b7..49b39a442 100644 --- a/packages/plugin-rsc/examples/basic/src/server.tsx +++ b/packages/plugin-rsc/examples/basic/src/server.tsx @@ -4,13 +4,17 @@ import './styles.css' export default async function handler(request: Request): Promise { const url = new URL(request.url) const { Root } = await import('./routes/root.tsx') + const nonce = !process.env.NO_CSP ? crypto.randomUUID() : undefined + // https://vite.dev/guide/features.html#content-security-policy-csp + // this isn't needed if `style-src: 'unsafe-inline'` (dev) and `script-src: 'self'` + const nonceMeta = nonce && const root = ( <> {import.meta.viteRsc.loadCss()} + {nonceMeta} ) - const nonce = !process.env.NO_CSP ? crypto.randomUUID() : undefined const response = await handleRequest({ request, getRoot: () => root, From e53b2fa79081f001153a27c5a279f5f012b05174 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 21 Jul 2025 13:14:12 +0900 Subject: [PATCH 8/8] fix: fix charset --- packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx | 2 +- packages/plugin-rsc/examples/basic/src/routes/root.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx index e4827bf69..ff7c2068d 100644 --- a/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx +++ b/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx @@ -97,7 +97,7 @@ export async function handleRequest({ // respond html return new Response(htmlStream, { headers: { - 'Content-type': 'text/html', + 'content-type': 'text/html;charset=utf-8', vary: 'accept', }, }) diff --git a/packages/plugin-rsc/examples/basic/src/routes/root.tsx b/packages/plugin-rsc/examples/basic/src/routes/root.tsx index 5a3421ed4..52ce38dee 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/root.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/root.tsx @@ -34,6 +34,7 @@ export function Root(props: { url: URL }) { return ( + vite-rsc {import.meta.viteRsc.loadCss('/src/routes/root.tsx')}