diff --git a/packages/common/package.json b/packages/common/package.json index 1bb806f8c..97c1eb7c2 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -7,6 +7,9 @@ ".": "./index.ts", "./refresh-runtime": "./refresh-runtime.js" }, + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.41" + }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } diff --git a/packages/common/refresh-utils.ts b/packages/common/refresh-utils.ts index db0e587e4..ef9dc00f6 100644 --- a/packages/common/refresh-utils.ts +++ b/packages/common/refresh-utils.ts @@ -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/ @@ -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 '' + } + }, + }, + } +} diff --git a/packages/plugin-react-swc/CHANGELOG.md b/packages/plugin-react-swc/CHANGELOG.md index 532ef9ed2..bf3196030 100644 --- a/packages/plugin-react-swc/CHANGELOG.md +++ b/packages/plugin-react-swc/CHANGELOG.md @@ -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 diff --git a/packages/plugin-react-swc/README.md b/packages/plugin-react-swc/README.md index c74f11d72..e20554e44 100644 --- a/packages/plugin-react-swc/README.md +++ b/packages/plugin-react-swc/README.md @@ -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). diff --git a/packages/plugin-react-swc/src/index.ts b/packages/plugin-react-swc/src/index.ts index 425bac15c..8beeda5d6 100644 --- a/packages/plugin-react-swc/src/index.ts +++ b/packages/plugin-react-swc/src/index.ts @@ -16,6 +16,7 @@ import { getPreambleCode, runtimePublicPath, silenceUseClientWarning, + virtualPreamblePlugin, } from '@vitejs/react-common' import * as vite from 'vite' import { exactRegex } from '@rolldown/pluginutils' @@ -246,6 +247,10 @@ const react = (_options?: Options): Plugin[] => { viteCacheRoot = config.cacheDir }, }, + virtualPreamblePlugin({ + name: '@vitejs/plugin-react-swc/preamble', + isEnabled: () => !hmrDisabled, + }), ] } diff --git a/packages/plugin-react-swc/tsdown.config.ts b/packages/plugin-react-swc/tsdown.config.ts index 354d5f2e6..4c2080beb 100644 --- a/packages/plugin-react-swc/tsdown.config.ts +++ b/packages/plugin-react-swc/tsdown.config.ts @@ -20,6 +20,10 @@ export default defineConfig({ from: 'README.md', to: 'dist/README.md', }, + { + from: 'types', + to: 'dist/types', + }, ], onSuccess() { writeFileSync( @@ -34,7 +38,10 @@ export default defineConfig({ key !== 'private', ), ), - exports: './index.js', + exports: { + '.': './index.js', + './preamble': './types/preamble.d.ts', + }, }, null, 2, diff --git a/packages/plugin-react-swc/types/preamble.d.ts b/packages/plugin-react-swc/types/preamble.d.ts new file mode 100644 index 000000000..336ce12bb --- /dev/null +++ b/packages/plugin-react-swc/types/preamble.d.ts @@ -0,0 +1 @@ +export {} diff --git a/packages/plugin-react/CHANGELOG.md b/packages/plugin-react/CHANGELOG.md index 6ef1b7750..fe770eee1 100644 --- a/packages/plugin-react/CHANGELOG.md +++ b/packages/plugin-react/CHANGELOG.md @@ -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)) diff --git a/packages/plugin-react/README.md b/packages/plugin-react/README.md index c607891ca..ca4809151 100644 --- a/packages/plugin-react/README.md +++ b/packages/plugin-react/README.md @@ -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) => { @@ -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. diff --git a/packages/plugin-react/package.json b/packages/plugin-react/package.json index 1417828c5..d265c6110 100644 --- a/packages/plugin-react/package.json +++ b/packages/plugin-react/package.json @@ -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", diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index a01f693e5..41993f281 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react/src/index.ts @@ -12,6 +12,7 @@ import { preambleCode, runtimePublicPath, silenceUseClientWarning, + virtualPreamblePlugin, } from '@vitejs/react-common' import { exactRegex, @@ -524,6 +525,10 @@ export default function viteReact(opts: Options = {}): Plugin[] { ? [viteRefreshWrapper, viteConfigPost, viteReactRefreshFullBundleMode] : []), viteReactRefresh, + virtualPreamblePlugin({ + name: '@vitejs/plugin-react/preamble', + isEnabled: () => !skipFastRefresh && !isFullBundle, + }), ] } diff --git a/packages/plugin-react/types/preamble.d.ts b/packages/plugin-react/types/preamble.d.ts new file mode 100644 index 000000000..336ce12bb --- /dev/null +++ b/packages/plugin-react/types/preamble.d.ts @@ -0,0 +1 @@ +export {} diff --git a/playground/ssr-react/src/entry-client.jsx b/playground/ssr-react/src/entry-client.jsx index bb2769717..62b8418e0 100644 --- a/playground/ssr-react/src/entry-client.jsx +++ b/playground/ssr-react/src/entry-client.jsx @@ -1,3 +1,4 @@ +import '@vitejs/plugin-react/preamble' import ReactDOM from 'react-dom/client' import { App } from './App' diff --git a/playground/ssr-react/vite.config.js b/playground/ssr-react/vite.config.js index f9258a1d0..d5074ecd5 100644 --- a/playground/ssr-react/vite.config.js +++ b/playground/ssr-react/vite.config.js @@ -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(``, appHtml) res.setHeader('content-type', 'text/html').end(html) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 191b360e8..8bd2612a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,7 +78,11 @@ importers: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) - packages/common: {} + packages/common: + dependencies: + '@rolldown/pluginutils': + specifier: 1.0.0-beta.41 + version: 1.0.0-beta.41 packages/plugin-react: dependencies: