Skip to content
Open
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
3 changes: 3 additions & 0 deletions packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
".": "./index.ts",
"./refresh-runtime": "./refresh-runtime.js"
},
"dependencies": {
"@rolldown/pluginutils": "1.0.0-beta.41"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be added to SWC deps too (and maybe rsc) because the common package in bundled into react plugins. I'm not a power user of pnpm, but is catalog the best way to unsure it's the same version everywhere?

Also should we use a strict version?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this might look weird but I just noticed @rolldown/pluginutils is already in dependencies of all three react packages (and rsc doesn't use it) and they are all pinned with same version. So this should be fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes ok with dependabot this should be done in sync 👍

},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
}
Expand Down
36 changes: 36 additions & 0 deletions packages/common/refresh-utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { Plugin } from 'vite'
import { exactRegex } from '@rolldown/pluginutils'

export const runtimePublicPath = '/@react-refresh'

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

return newCode
}

export function virtualPreamblePlugin({
name,
isEnabled,
}: {
name: string
isEnabled: () => boolean
}): Plugin {
return {
name: 'vite:react-virtual-preamble',
resolveId: {
order: 'pre',
filter: { id: exactRegex(name) },
handler(source) {
if (source === name) {
return '\0' + source
}
},
},
load: {
filter: { id: exactRegex('\0' + name) },
handler(id) {
if (id === '\0' + name) {
if (isEnabled()) {
// vite dev import analysis can rewrite base
return preambleCode.replace('__BASE__', '/')
}
return ''
}
},
},
}
}
4 changes: 4 additions & 0 deletions packages/plugin-react-swc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Add `@vitejs/plugin-react-swc/preamble` virtual module for SSR HMR ([#890](https://github.com/vitejs/vite-plugin-react/pull/890))

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.

## 4.1.0 (2025-09-17)

### Set SWC cacheRoot options
Expand Down
32 changes: 32 additions & 0 deletions packages/plugin-react-swc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,38 @@ If set, disables the recommendation to use `@vitejs/plugin-react-oxc` (which is
react({ disableOxcRecommendation: true })
```

## `@vitejs/plugin-react-swc/preamble`

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:

```js
// [entry.client.js]
import '@vitejs/plugin-react-swc/preamble'
```

Alternatively, you can manually call `transformIndexHtml` during SSR, which sets up equivalent initialization code. Here's an example for an Express server:

```js
app.get('/', async (req, res, next) => {
try {
let html = fs.readFileSync(path.resolve(root, 'index.html'), 'utf-8')

// Transform HTML using Vite plugins.
html = await viteServer.transformIndexHtml(req.url, html)

res.send(html)
} catch (e) {
return next(e)
}
})
```

Otherwise, you'll get the following error:

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

## Consistent components exports

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).
Expand Down
5 changes: 5 additions & 0 deletions packages/plugin-react-swc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getPreambleCode,
runtimePublicPath,
silenceUseClientWarning,
virtualPreamblePlugin,
} from '@vitejs/react-common'
import * as vite from 'vite'
import { exactRegex } from '@rolldown/pluginutils'
Expand Down Expand Up @@ -246,6 +247,10 @@ const react = (_options?: Options): Plugin[] => {
viteCacheRoot = config.cacheDir
},
},
virtualPreamblePlugin({
name: '@vitejs/plugin-react-swc/preamble',
isEnabled: () => !hmrDisabled,
}),
]
}

Expand Down
9 changes: 8 additions & 1 deletion packages/plugin-react-swc/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export default defineConfig({
from: 'README.md',
to: 'dist/README.md',
},
{
from: 'types',
to: 'dist/types',
},
],
onSuccess() {
writeFileSync(
Expand All @@ -34,7 +38,10 @@ export default defineConfig({
key !== 'private',
),
),
exports: './index.js',
exports: {
'.': './index.js',
'./preamble': './types/preamble.d.ts',
},
},
null,
2,
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-react-swc/types/preamble.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {}
4 changes: 4 additions & 0 deletions packages/plugin-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Add `@vitejs/plugin-react/preamble` virtual module for SSR HMR ([#890](https://github.com/vitejs/vite-plugin-react/pull/890))

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.

## 5.0.4 (2025-09-27)

### Perf: use native refresh wrapper plugin in rolldown-vite ([#881](https://github.com/vitejs/vite-plugin-react/pull/881))
Expand Down
13 changes: 10 additions & 3 deletions packages/plugin-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,16 @@ react({ reactRefreshHost: 'http://localhost:3000' })

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}`.

## Middleware mode
## `@vitejs/plugin-react/preamble`

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:
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:

```js
// [entry.client.js]
import '@vitejs/plugin-react/preamble'
```

Alternatively, you can manually call `transformIndexHtml` during SSR, which sets up equivalent initialization code. Here's an example for an Express server:

```js
app.get('/', async (req, res, next) => {
Expand All @@ -121,7 +128,7 @@ app.get('/', async (req, res, next) => {
})
```

Otherwise, you'll probably get this error:
Otherwise, you'll get the following error:

```
Uncaught Error: @vitejs/plugin-react can't detect preamble. Something is wrong.
Expand Down
6 changes: 5 additions & 1 deletion packages/plugin-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@
"Arnaud Barré"
],
"files": [
"types",
"dist"
],
"type": "module",
"exports": "./dist/index.js",
"exports": {
".": "./dist/index.js",
"./preamble": "./types/preamble.d.ts"
},
"scripts": {
"dev": "tsdown --watch ./src --watch ../common",
"build": "tsdown",
Expand Down
5 changes: 5 additions & 0 deletions packages/plugin-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
preambleCode,
runtimePublicPath,
silenceUseClientWarning,
virtualPreamblePlugin,
} from '@vitejs/react-common'
import {
exactRegex,
Expand Down Expand Up @@ -524,6 +525,10 @@ export default function viteReact(opts: Options = {}): Plugin[] {
? [viteRefreshWrapper, viteConfigPost, viteReactRefreshFullBundleMode]
: []),
viteReactRefresh,
virtualPreamblePlugin({
name: '@vitejs/plugin-react/preamble',
isEnabled: () => !skipFastRefresh && !isFullBundle,
}),
]
}

Expand Down
1 change: 1 addition & 0 deletions packages/plugin-react/types/preamble.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {}
1 change: 1 addition & 0 deletions playground/ssr-react/src/entry-client.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import '@vitejs/plugin-react/preamble'
import ReactDOM from 'react-dom/client'
import { App } from './App'

Expand Down
8 changes: 5 additions & 3 deletions playground/ssr-react/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ export default defineConfig({
'/src/entry-server.jsx',
)
const appHtml = render(url)
const template = await server.transformIndexHtml(
url,
fs.readFileSync(path.resolve(_dirname, 'index.html'), 'utf-8'),
// "@vitejs/plugin-react/preamble" is used instead of transformIndexHtml
// to setup react hmr globals.
const template = fs.readFileSync(
path.resolve(_dirname, 'index.html'),
'utf-8',
)
const html = template.replace(`<!--app-html-->`, appHtml)
res.setHeader('content-type', 'text/html').end(html)
Expand Down
6 changes: 5 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading