Skip to content

Commit fffb7eb

Browse files
hi-ogawaclaude
andauthored
feat(react): expose virtual module to simplify hmr preamble setup on ssr (#890)
Co-authored-by: Claude <[email protected]>
1 parent 84f146f commit fffb7eb

File tree

15 files changed

+125
-9
lines changed

15 files changed

+125
-9
lines changed

packages/common/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
".": "./index.ts",
88
"./refresh-runtime": "./refresh-runtime.js"
99
},
10+
"dependencies": {
11+
"@rolldown/pluginutils": "1.0.0-beta.41"
12+
},
1013
"peerDependencies": {
1114
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
1215
}

packages/common/refresh-utils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import type { Plugin } from 'vite'
2+
import { exactRegex } from '@rolldown/pluginutils'
3+
14
export const runtimePublicPath = '/@react-refresh'
25

36
const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/
@@ -60,3 +63,36 @@ function $RefreshSig$() { return RefreshRuntime.createSignatureFunctionForTransf
6063

6164
return newCode
6265
}
66+
67+
export function virtualPreamblePlugin({
68+
name,
69+
isEnabled,
70+
}: {
71+
name: string
72+
isEnabled: () => boolean
73+
}): Plugin {
74+
return {
75+
name: 'vite:react-virtual-preamble',
76+
resolveId: {
77+
order: 'pre',
78+
filter: { id: exactRegex(name) },
79+
handler(source) {
80+
if (source === name) {
81+
return '\0' + source
82+
}
83+
},
84+
},
85+
load: {
86+
filter: { id: exactRegex('\0' + name) },
87+
handler(id) {
88+
if (id === '\0' + name) {
89+
if (isEnabled()) {
90+
// vite dev import analysis can rewrite base
91+
return preambleCode.replace('__BASE__', '/')
92+
}
93+
return ''
94+
}
95+
},
96+
},
97+
}
98+
}

packages/plugin-react-swc/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Add `@vitejs/plugin-react-swc/preamble` virtual module for SSR HMR ([#890](https://github.com/vitejs/vite-plugin-react/pull/890))
6+
7+
SSR applications can now initialize HMR runtime by importing `@vitejs/plugin-react-swc/preamble` at the top of their client entry instead of manually calling `transformIndexHtml`. This simplifies SSR setup for applications that don't use the `transformIndexHtml` API.
8+
59
## 4.1.0 (2025-09-17)
610

711
### Set SWC cacheRoot options

packages/plugin-react-swc/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,38 @@ If set, disables the recommendation to use `@vitejs/plugin-react-oxc` (which is
125125
react({ disableOxcRecommendation: true })
126126
```
127127

128+
## `@vitejs/plugin-react-swc/preamble`
129+
130+
The package provides `@vitejs/plugin-react-swc/preamble` to initialize HMR runtime from client entrypoint for SSR applications which don't use [`transformIndexHtml` API](https://vite.dev/guide/api-javascript.html#vitedevserver). For example:
131+
132+
```js
133+
// [entry.client.js]
134+
import '@vitejs/plugin-react-swc/preamble'
135+
```
136+
137+
Alternatively, you can manually call `transformIndexHtml` during SSR, which sets up equivalent initialization code. Here's an example for an Express server:
138+
139+
```js
140+
app.get('/', async (req, res, next) => {
141+
try {
142+
let html = fs.readFileSync(path.resolve(root, 'index.html'), 'utf-8')
143+
144+
// Transform HTML using Vite plugins.
145+
html = await viteServer.transformIndexHtml(req.url, html)
146+
147+
res.send(html)
148+
} catch (e) {
149+
return next(e)
150+
}
151+
})
152+
```
153+
154+
Otherwise, you'll get the following error:
155+
156+
```
157+
Uncaught Error: @vitejs/plugin-react-swc can't detect preamble. Something is wrong.
158+
```
159+
128160
## Consistent components exports
129161

130162
For React refresh to work correctly, your file should only export React components. The best explanation I've read is the one from the [Gatsby docs](https://www.gatsbyjs.com/docs/reference/local-development/fast-refresh/#how-it-works).

packages/plugin-react-swc/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
getPreambleCode,
1717
runtimePublicPath,
1818
silenceUseClientWarning,
19+
virtualPreamblePlugin,
1920
} from '@vitejs/react-common'
2021
import * as vite from 'vite'
2122
import { exactRegex } from '@rolldown/pluginutils'
@@ -246,6 +247,10 @@ const react = (_options?: Options): Plugin[] => {
246247
viteCacheRoot = config.cacheDir
247248
},
248249
},
250+
virtualPreamblePlugin({
251+
name: '@vitejs/plugin-react-swc/preamble',
252+
isEnabled: () => !hmrDisabled,
253+
}),
249254
]
250255
}
251256

packages/plugin-react-swc/tsdown.config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export default defineConfig({
2020
from: 'README.md',
2121
to: 'dist/README.md',
2222
},
23+
{
24+
from: 'types',
25+
to: 'dist/types',
26+
},
2327
],
2428
onSuccess() {
2529
writeFileSync(
@@ -34,7 +38,10 @@ export default defineConfig({
3438
key !== 'private',
3539
),
3640
),
37-
exports: './index.js',
41+
exports: {
42+
'.': './index.js',
43+
'./preamble': './types/preamble.d.ts',
44+
},
3845
},
3946
null,
4047
2,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {}

packages/plugin-react/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Add `@vitejs/plugin-react/preamble` virtual module for SSR HMR ([#890](https://github.com/vitejs/vite-plugin-react/pull/890))
6+
7+
SSR applications can now initialize HMR runtime by importing `@vitejs/plugin-react/preamble` at the top of their client entry instead of manually calling `transformIndexHtml`. This simplifies SSR setup for applications that don't use the `transformIndexHtml` API.
8+
59
## 5.0.4 (2025-09-27)
610

711
### Perf: use native refresh wrapper plugin in rolldown-vite ([#881](https://github.com/vitejs/vite-plugin-react/pull/881))

packages/plugin-react/README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,16 @@ react({ reactRefreshHost: 'http://localhost:3000' })
102102

103103
Under the hood, this simply updates the React Fash Refresh runtime URL from `/@react-refresh` to `http://localhost:3000/@react-refresh` to ensure there is only one Refresh runtime across the whole application. Note that if you define `base` option in the host application, you need to include it in the option, like: `http://localhost:3000/{base}`.
104104

105-
## Middleware mode
105+
## `@vitejs/plugin-react/preamble`
106106

107-
In [middleware mode](https://vite.dev/config/server-options.html#server-middlewaremode), you should make sure your entry `index.html` file is transformed by Vite. Here's an example for an Express server:
107+
The package provides `@vitejs/plugin-react/preamble` to initialize HMR runtime from client entrypoint for SSR applications which don't use [`transformIndexHtml` API](https://vite.dev/guide/api-javascript.html#vitedevserver). For example:
108+
109+
```js
110+
// [entry.client.js]
111+
import '@vitejs/plugin-react/preamble'
112+
```
113+
114+
Alternatively, you can manually call `transformIndexHtml` during SSR, which sets up equivalent initialization code. Here's an example for an Express server:
108115

109116
```js
110117
app.get('/', async (req, res, next) => {
@@ -121,7 +128,7 @@ app.get('/', async (req, res, next) => {
121128
})
122129
```
123130

124-
Otherwise, you'll probably get this error:
131+
Otherwise, you'll get the following error:
125132

126133
```
127134
Uncaught Error: @vitejs/plugin-react can't detect preamble. Something is wrong.

packages/plugin-react/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@
1717
"Arnaud Barré"
1818
],
1919
"files": [
20+
"types",
2021
"dist"
2122
],
2223
"type": "module",
23-
"exports": "./dist/index.js",
24+
"exports": {
25+
".": "./dist/index.js",
26+
"./preamble": "./types/preamble.d.ts"
27+
},
2428
"scripts": {
2529
"dev": "tsdown --watch ./src --watch ../common",
2630
"build": "tsdown",

0 commit comments

Comments
 (0)