Skip to content

Commit 677ca1d

Browse files
Merge remote-tracking branch 'upstream/main' into chore-rsc-nightly
2 parents 1e477bd + 74ec0e0 commit 677ca1d

File tree

10 files changed

+179
-65
lines changed

10 files changed

+179
-65
lines changed

packages/plugin-rsc/README.md

Lines changed: 56 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ This package provides [React Server Components](https://react.dev/reference/rsc/
44

55
## Features
66

7-
- **Framework-less RSC experience**: The plugin implements [RSC conventions](https://react.dev/reference/rsc/server-components) and provides low level `react-server-dom` runtime API without framework-specific abstractions.
8-
- **CSS support**: CSS is automatically code-split both at client and server components and they are injected upon rendering.
7+
- **Framework-agnostic**: The plugin implements [RSC bundler features](https://react.dev/reference/rsc/server-components) and provides low level RSC runtime (`react-server-dom`) API without framework-specific abstractions.
8+
- **Runtime-agnostic**: Built on [Vite environment API](https://vite.dev/guide/api-environment.html) and works with other runtimes (e.g., [`@cloudflare/vite-plugin`](https://github.com/cloudflare/workers-sdk/tree/main/packages/vite-plugin-cloudflare)).
99
- **HMR support**: Enables editing both client and server components without full page reloads.
10-
- **Runtime agnostic**: Built on [Vite environment API](https://vite.dev/guide/api-environment.html) and works with other runtimes (e.g., [`@cloudflare/vite-plugin`](https://github.com/cloudflare/workers-sdk/tree/main/packages/vite-plugin-cloudflare)).
10+
- **CSS support**: CSS is automatically code-split both at client and server components and they are injected upon rendering.
1111

1212
## Getting Started
1313

@@ -19,14 +19,17 @@ npx degit vitejs/vite-plugin-react/packages/plugin-rsc/examples/starter my-app
1919

2020
## Examples
2121

22-
- [`./examples/starter`](./examples/starter)
23-
- This example provides an in-depth overview of API with inline comments to explain how they function within RSC-powered React application.
24-
- [`./examples/react-router`](./examples/react-router)
25-
- 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.
26-
- [`./examples/basic`](./examples/basic)
27-
- This is mainly used for e2e testing and include various advanced RSC usages (e.g. `"use cache"` example).
28-
- [`./examples/ssg`](./examples/ssg)
29-
- Static site generation (SSG) example with MDX and client components for interactivity.
22+
**Start here:** [`./examples/starter`](./examples/starter) - Recommended for understanding the plugin
23+
24+
- Provides an in-depth overview of API with inline comments to explain how they function within RSC-powered React application.
25+
26+
**Integration examples:**
27+
28+
- [`./examples/basic`](./examples/basic) - Advanced RSC features and testing
29+
- This is mainly used for e2e testing and includes various advanced RSC usages (e.g. `"use cache"` example).
30+
- [`./examples/ssg`](./examples/ssg) - Static site generation with MDX and client components for interactivity.
31+
- [`./examples/react-router`](./examples/react-router) - React Router RSC integration
32+
- 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.
3033

3134
## Basic Concepts
3235

@@ -132,7 +135,7 @@ import * as ReactServer from '@vitejs/plugin-rsc/rsc' // re-export of react-serv
132135
133136
// the plugin assumes `rsc` entry having default export of request handler
134137
export default async function handler(request: Request): Promise<Response> {
135-
// serialization React VDOM to RSC stream
138+
// serialize React VDOM to RSC stream
136139
const root = (
137140
<html>
138141
<body>
@@ -193,51 +196,26 @@ export async function handleSsr(rscStream: ReadableStream) {
193196
- [`entry.browser.tsx`](./examples/starter/src/framework/entry.browser.tsx)
194197

195198
```tsx
196-
import * as ReactClient from "@vitejs/plugin-rsc/browser"; // re-export of react-server-dom/client.browser
197-
import * as ReactDOMClient from "react-dom/client";
199+
import * as ReactClient from '@vitejs/plugin-rsc/browser' // re-export of react-server-dom/client.browser
200+
import * as ReactDOMClient from 'react-dom/client'
198201

199202
async function main() {
200203
// fetch and deserialize RSC stream back to React VDOM
201-
const rscResponse = await fetch(window.location.href + ".rsc);
202-
const root = await ReactClient.createFromReadableStream(rscResponse.body);
204+
const rscResponse = await fetch(window.location.href + '.rsc')
205+
const root = await ReactClient.createFromReadableStream(rscResponse.body)
203206

204207
// hydration (traditional CSR)
205-
ReactDOMClient.hydrateRoot(document, root);
208+
ReactDOMClient.hydrateRoot(document, root)
206209
}
207210

208-
main();
211+
main()
209212
```
210213

211-
## `react-server-dom` API
212-
213-
### `@vitejs/plugin-rsc/rsc`
214-
215-
This module re-exports RSC runtime API provided by `react-server-dom/server.edge` and `react-server-dom/client.edge` such as:
216-
217-
- `renderToReadableStream`: RSC serialization (React VDOM -> RSC stream)
218-
- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM). This is also available on rsc environment itself. For example, it allows saving serailized RSC and deserializing it for later use.
219-
- `decodeAction/decodeReply/decodeFormState/loadServerAction/createTemporaryReferenceSet`
220-
- `encodeReply/createClientTemporaryReferenceSet`
221-
222-
### `@vitejs/plugin-rsc/ssr`
223-
224-
This module re-exports RSC runtime API provided by `react-server-dom/client.edge`
225-
226-
- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM)
227-
228-
### `@vitejs/plugin-rsc/browser`
229-
230-
This module re-exports RSC runtime API provided by `react-server-dom/client.browser`
231-
232-
- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM)
233-
- `createFromFetch`: a robust way of `createFromReadableStream((await fetch("...")).body)`
234-
- `encodeReply/setServerCallback`: server function related...
235-
236214
## Environment helper API
237215

238216
The plugin provides an additional helper for multi environment interaction.
239217

240-
### available on `rsc` or `ssr` environment
218+
### Available on `rsc` or `ssr` environment
241219

242220
#### `import.meta.viteRsc.loadModule`
243221

@@ -260,7 +238,7 @@ ssrModule.renderHTML(...);
260238
export function renderHTML(...) {}
261239
```
262240

263-
### available on `rsc` environment
241+
### Available on `rsc` environment
264242

265243
#### `import.meta.viteRsc.loadCss`
266244

@@ -287,7 +265,7 @@ export function ServerPage() {
287265
}
288266
```
289267

290-
Where specifying `loadCss(<id>)`, it will collect css through the server module resolved by `<id>`.
268+
When specifying `loadCss(<id>)`, it will collect css through the server module resolved by `<id>`.
291269

292270
```tsx
293271
// virtual:my-framework-helper
@@ -324,7 +302,7 @@ export function Page(props) {
324302
return <div>...</div>
325303
}
326304

327-
// my-route.css?vite-rsc-css-export=Page
305+
// my-route.tsx?vite-rsc-css-export=Page
328306
function Page(props) {
329307
return <div>...</div>
330308
}
@@ -341,14 +319,13 @@ function __Page(props) {
341319
export { __Page as Page }
342320
```
343321

344-
### available on `ssr` environment
322+
### Available on `ssr` environment
345323

346324
#### `import.meta.viteRsc.loadBootstrapScriptContent("index")`
347325

348326
This provides a raw js code to execute a browser entry file specified by `environments.client.build.rollupOptions.input.index`. This is intended to be used with React DOM SSR API, such as [`renderToReadableStream`](https://react.dev/reference/react-dom/server/renderToReadableStream)
349327

350328
```js
351-
import bootstrapScriptContent from 'virtual:vite-rsc/bootstrap-script-content'
352329
import { renderToReadableStream } from 'react-dom/server.edge'
353330

354331
const bootstrapScriptContent =
@@ -358,7 +335,7 @@ const htmlStream = await renderToReadableStream(reactNode, {
358335
})
359336
```
360337

361-
### available on `client` environment
338+
### Available on `client` environment
362339

363340
#### `rsc:update` event
364341

@@ -420,6 +397,31 @@ export default defineConfig({
420397
})
421398
```
422399

400+
## RSC runtime (react-server-dom) API
401+
402+
### `@vitejs/plugin-rsc/rsc`
403+
404+
This module re-exports RSC runtime API provided by `react-server-dom/server.edge` and `react-server-dom/client.edge` such as:
405+
406+
- `renderToReadableStream`: RSC serialization (React VDOM -> RSC stream)
407+
- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM). This is also available on rsc environment itself. For example, it allows saving serialized RSC and deserializing it for later use.
408+
- `decodeAction/decodeReply/decodeFormState/loadServerAction/createTemporaryReferenceSet`
409+
- `encodeReply/createClientTemporaryReferenceSet`
410+
411+
### `@vitejs/plugin-rsc/ssr`
412+
413+
This module re-exports RSC runtime API provided by `react-server-dom/client.edge`
414+
415+
- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM)
416+
417+
### `@vitejs/plugin-rsc/browser`
418+
419+
This module re-exports RSC runtime API provided by `react-server-dom/client.browser`
420+
421+
- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM)
422+
- `createFromFetch`: a robust way of `createFromReadableStream((await fetch("...")).body)`
423+
- `encodeReply/setServerCallback`: server function related...
424+
423425
## High level API
424426

425427
> [!NOTE]
@@ -473,6 +475,10 @@ export function Page() {
473475
}
474476
```
475477

478+
## Canary and Experimental channel releases
479+
480+
See https://github.com/vitejs/vite-plugin-react/pull/524 for how to install the package for React [canary](https://react.dev/community/versioning-policy#canary-channel) and [experimental](https://react.dev/community/versioning-policy#all-release-channels) usages.
481+
476482
## Credits
477483

478484
This project builds on fundamental techniques and insights from pioneering Vite RSC implementations.

packages/plugin-rsc/examples/basic/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,9 @@
3333
"vite": "^7.1.1",
3434
"vite-plugin-inspect": "^11.3.2",
3535
"wrangler": "^4.28.1"
36+
},
37+
"stackblitz": {
38+
"installDependencies": false,
39+
"startCommand": "pnpm i && pnpm dev"
3640
}
3741
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Dep() {
2+
return <>test-import-meta-glob</>
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export async function TestImportMetaGlob() {
2+
const mod: any = await Object.values(import.meta.glob('./dep.tsx'))[0]()
3+
return <mod.default />
4+
}

packages/plugin-rsc/examples/basic/src/routes/react-cache/server.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ import React from 'react'
66

77
export async function TestReactCache(props: { url: URL }) {
88
if (props.url.searchParams.has('test-react-cache')) {
9-
await Promise.all([
10-
testCacheFn('test1'),
11-
testCacheFn('test2'),
12-
testCacheFn('test1'),
13-
testNonCacheFn('test1'),
14-
testNonCacheFn('test2'),
15-
testNonCacheFn('test1'),
16-
])
9+
await testCacheFn('test1')
10+
await testCacheFn('test2')
11+
await testCacheFn('test1')
12+
await testNonCacheFn('test1')
13+
await testNonCacheFn('test2')
14+
await testNonCacheFn('test1')
1715
} else {
1816
cacheFnCount = 0
1917
nonCacheFnCount = 0

packages/plugin-rsc/examples/basic/src/routes/root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { TestHmrSharedServer } from './hmr-shared/server'
3636
import { TestHmrSharedClient } from './hmr-shared/client'
3737
import { TestHmrSharedAtomic } from './hmr-shared/atomic/server'
3838
import { TestCssQueries } from './css-queries/server'
39+
import { TestImportMetaGlob } from './import-meta-glob/server'
3940

4041
export function Root(props: { url: URL }) {
4142
return (
@@ -85,6 +86,7 @@ export function Root(props: { url: URL }) {
8586
<TestUseCache />
8687
<TestReactCache url={props.url} />
8788
<TestCssQueries />
89+
<TestImportMetaGlob />
8890
</body>
8991
</html>
9092
)

packages/plugin-rsc/examples/basic/vite.config.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export default { fetch: handler };
131131
}
132132
},
133133
},
134+
testScanPlugin(),
134135
],
135136
build: {
136137
minify: false,
@@ -158,6 +159,38 @@ export default { fetch: handler };
158159
},
159160
}) as any
160161

162+
function testScanPlugin(): Plugin[] {
163+
const moduleIds: { name: string; ids: string[] }[] = []
164+
return [
165+
{
166+
name: 'test-scan',
167+
apply: 'build',
168+
buildEnd() {
169+
moduleIds.push({
170+
name: this.environment.name,
171+
ids: [...this.getModuleIds()],
172+
})
173+
},
174+
buildApp: {
175+
order: 'post',
176+
async handler() {
177+
// client scan build discovers additional modules for server references.
178+
const [m1, m2] = moduleIds.filter((m) => m.name === 'rsc')
179+
const diff = m2.ids.filter((id) => !m1.ids.includes(id))
180+
assert(diff.length > 0)
181+
182+
// but make sure it's not due to import.meta.glob
183+
// https://github.com/vitejs/rolldown-vite/issues/373
184+
assert.equal(
185+
diff.find((id) => id.includes('import-meta-glob/dep.tsx')),
186+
undefined,
187+
)
188+
},
189+
},
190+
},
191+
]
192+
}
193+
161194
function vitePluginUseCache(): Plugin[] {
162195
return [
163196
{

packages/plugin-rsc/src/plugin.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
import { cjsModuleRunnerPlugin } from './plugins/cjs'
4141
import { evalValue, parseIdQuery } from './plugins/utils'
4242
import { createDebug } from '@hiogawa/utils'
43+
import { transformScanBuildStrip } from './plugins/scan'
4344

4445
// state for build orchestration
4546
let serverReferences: Record<string, string> = {}
@@ -901,19 +902,16 @@ globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage;
901902
]
902903
}
903904

905+
// During scan build, we strip all code but imports to
906+
// traverse module graph faster and just discover client/server references.
904907
function scanBuildStripPlugin(): Plugin {
905908
return {
906909
name: 'rsc:scan-strip',
907910
apply: 'build',
908911
enforce: 'post',
909-
transform(code, _id, _options) {
912+
async transform(code, _id, _options) {
910913
if (!isScanBuild) return
911-
// During server scan, we strip all code but imports to only discover client/server references.
912-
const [imports] = esModuleLexer.parse(code)
913-
const output = imports
914-
.map((e) => e.n && `import ${JSON.stringify(e.n)};\n`)
915-
.filter(Boolean)
916-
.join('')
914+
const output = await transformScanBuildStrip(code)
917915
return { code: output, map: { mappings: '' } }
918916
},
919917
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { transformScanBuildStrip } from './scan'
3+
4+
describe(transformScanBuildStrip, () => {
5+
it('basic', async () => {
6+
const input = `\
7+
import { a } from "a";
8+
import "b";
9+
import(String("c"))
10+
import.meta.glob("d", {
11+
query: "?e",
12+
})
13+
import.meta.globee("d", { query: "?e" })
14+
export default "foo";
15+
`
16+
expect(await transformScanBuildStrip(input)).toMatchInlineSnapshot(`
17+
"import "a";
18+
import "b";
19+
console.log(import.meta.glob("d", {
20+
query: "?e",
21+
}));
22+
"
23+
`)
24+
})
25+
})

0 commit comments

Comments
 (0)