Skip to content

Commit 3506cfa

Browse files
Copilothi-ogawa
andauthored
feat(rsc): use locally installed react-server-dom-webpack when available (#915)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: hi-ogawa <[email protected]> Co-authored-by: Hiroshi Ogawa <[email protected]>
1 parent 91bcc08 commit 3506cfa

File tree

4 files changed

+125
-5
lines changed

4 files changed

+125
-5
lines changed

packages/plugin-rsc/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,10 @@ Note that while there are official npm packages [`server-only`](https://www.npmj
561561
562562
This build-time validation is enabled by default and can be disabled by setting `validateImports: false` in the plugin options.
563563
564+
### `react-server-dom-webpack`
565+
566+
Currently `@vitejs/plugin-rsc` includes a vendored version of `react-server-dom-webpack`. However, when `react-server-dom-webpack` is installed in user project's dependencies, the plugin will automatically use it instead. This allows you to stay up-to-date with the latest React Server Components runtime without waiting for plugin updates.
567+
564568
## Credits
565569
566570
This project builds on fundamental techniques and insights from pioneering Vite RSC implementations.

packages/plugin-rsc/e2e/isolated.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { test } from '@playwright/test'
1+
import { expect, test, type Page } from '@playwright/test'
22
import { setupIsolatedFixture, useFixture } from './fixture'
33
import { defineStarterTest } from './starter'
44
import path from 'node:path'
55
import os from 'node:os'
66
import * as vite from 'vite'
77
import { waitForHydration } from './helper'
8+
import { x } from 'tinyexec'
89

910
test.describe(() => {
1011
// use RUNNER_TEMP on Github Actions
@@ -20,6 +21,10 @@ test.describe(() => {
2021
test.describe('dev-isolated', () => {
2122
const f = useFixture({ root: tmpRoot, mode: 'dev' })
2223
defineStarterTest(f)
24+
25+
test('verify react-server-dom-webpack', async ({ page }) => {
26+
await testReactServerDom(page, f.url(), true)
27+
})
2328
})
2429

2530
test.describe('build-isolated', () => {
@@ -86,3 +91,77 @@ test.describe('examples/ssg', () => {
8691
})
8792
})
8893
})
94+
95+
test.describe('react-server-dom-webpack', () => {
96+
const tmpRoot = path.join(
97+
process.env['RUNNER_TEMP'] || os.tmpdir(),
98+
'test-vite-rsc-webpack',
99+
)
100+
test.beforeAll(async () => {
101+
await setupIsolatedFixture({
102+
src: 'examples/starter',
103+
dest: tmpRoot,
104+
})
105+
const {
106+
default: { version },
107+
} = await import('react-server-dom-webpack/package.json', {
108+
with: { type: 'json' },
109+
})
110+
await x('pnpm', ['i', `react-server-dom-webpack@${version}`], {
111+
throwOnError: true,
112+
nodeOptions: {
113+
cwd: tmpRoot,
114+
},
115+
})
116+
})
117+
118+
test.describe('dev', () => {
119+
const f = useFixture({ root: tmpRoot, mode: 'dev' })
120+
defineStarterTest(f)
121+
122+
test('verify react-server-dom-webpack', async ({ page }) => {
123+
await testReactServerDom(page, f.url(), false)
124+
})
125+
})
126+
127+
test.describe('build', () => {
128+
const f = useFixture({ root: tmpRoot, mode: 'build' })
129+
defineStarterTest(f)
130+
})
131+
})
132+
133+
async function testReactServerDom(
134+
page: Page,
135+
url: string,
136+
expectVendor: boolean,
137+
) {
138+
let hasVendor = false
139+
let hasNonVendor = false
140+
page.on('request', async (request) => {
141+
if (
142+
request
143+
.url()
144+
.includes('.vite/deps/react-server-dom-webpack_client__browser.js')
145+
) {
146+
hasNonVendor = true
147+
}
148+
if (
149+
request
150+
.url()
151+
.includes(
152+
'.vite/deps/@vitejs_plugin-rsc_vendor_react-server-dom_client__browser.js',
153+
)
154+
) {
155+
hasVendor = true
156+
}
157+
})
158+
await page.goto(url)
159+
await waitForHydration(page)
160+
if (expectVendor) {
161+
expect(hasVendor).toBe(true)
162+
expect(hasNonVendor).toBe(false)
163+
} else {
164+
expect(hasVendor).toBe(false)
165+
expect(hasNonVendor).toBe(true)
166+
}
167+
}

packages/plugin-rsc/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@
6767
"peerDependencies": {
6868
"react": "*",
6969
"react-dom": "*",
70+
"react-server-dom-webpack": "*",
7071
"vite": "*"
72+
},
73+
"peerDependenciesMeta": {
74+
"react-server-dom-webpack": {
75+
"optional": true
76+
}
7177
}
7278
}

packages/plugin-rsc/src/plugin.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ export default function vitePluginRsc(
325325
}
326326
}
327327

328+
let hasReactServerDomWebpack = false
329+
328330
return [
329331
{
330332
name: 'rsc',
@@ -358,6 +360,12 @@ export default function vitePluginRsc(
358360
PKG_NAME,
359361
...result.ssr.noExternal.sort(),
360362
]
363+
hasReactServerDomWebpack = result.ssr.noExternal.includes(
364+
'react-server-dom-webpack',
365+
)
366+
const reactServerDomPackageName = hasReactServerDomWebpack
367+
? 'react-server-dom-webpack'
368+
: REACT_SERVER_DOM_NAME
361369

362370
return {
363371
appType: config.appType ?? 'custom',
@@ -380,7 +388,7 @@ export default function vitePluginRsc(
380388
optimizeDeps: {
381389
include: [
382390
'react-dom/client',
383-
`${REACT_SERVER_DOM_NAME}/client.browser`,
391+
`${reactServerDomPackageName}/client.browser`,
384392
],
385393
exclude: [PKG_NAME],
386394
},
@@ -406,7 +414,7 @@ export default function vitePluginRsc(
406414
'react/jsx-dev-runtime',
407415
'react-dom/server.edge',
408416
'react-dom/static.edge',
409-
`${REACT_SERVER_DOM_NAME}/client.edge`,
417+
`${reactServerDomPackageName}/client.edge`,
410418
],
411419
exclude: [PKG_NAME],
412420
},
@@ -432,8 +440,8 @@ export default function vitePluginRsc(
432440
'react-dom',
433441
'react/jsx-runtime',
434442
'react/jsx-dev-runtime',
435-
`${REACT_SERVER_DOM_NAME}/server.edge`,
436-
`${REACT_SERVER_DOM_NAME}/client.edge`,
443+
`${reactServerDomPackageName}/server.edge`,
444+
`${reactServerDomPackageName}/client.edge`,
437445
],
438446
exclude: [PKG_NAME],
439447
},
@@ -670,6 +678,29 @@ export default function vitePluginRsc(
670678
}
671679
},
672680
},
681+
{
682+
// Alias plugin to redirect vendored react-server-dom imports to user's package when available
683+
name: 'rsc:react-server-dom-webpack-alias',
684+
resolveId: {
685+
order: 'pre',
686+
async handler(source, importer, options) {
687+
if (
688+
hasReactServerDomWebpack &&
689+
source.startsWith(`${PKG_NAME}/vendor/react-server-dom/`)
690+
) {
691+
const newSource = source.replace(
692+
`${PKG_NAME}/vendor/react-server-dom`,
693+
'react-server-dom-webpack',
694+
)
695+
const resolved = await this.resolve(newSource, importer, {
696+
...options,
697+
skipSelf: true,
698+
})
699+
return resolved
700+
}
701+
},
702+
},
703+
},
673704
{
674705
// backward compat: `loadSsrModule(name)` implemented as `loadModule("ssr", name)`
675706
name: 'rsc:load-ssr-module',

0 commit comments

Comments
 (0)