From 5568bd55efb0caa01b1d09e2612ac3a0885f1647 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 1 Oct 2025 19:31:12 +0900 Subject: [PATCH 1/7] feat: expose virtual for simpler preamble setup on ssr --- packages/common/package.json | 3 ++ packages/common/refresh-utils.ts | 36 +++++++++++++++++++++++ packages/plugin-react/package.json | 5 +++- packages/plugin-react/preamble.d.ts | 1 + packages/plugin-react/src/index.ts | 5 ++++ playground/ssr-react/src/entry-client.jsx | 1 + playground/ssr-react/vite.config.js | 10 +++++-- pnpm-lock.yaml | 6 +++- 8 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 packages/plugin-react/preamble.d.ts 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/package.json b/packages/plugin-react/package.json index 8c7370bf6..b630a36c8 100644 --- a/packages/plugin-react/package.json +++ b/packages/plugin-react/package.json @@ -20,7 +20,10 @@ "dist" ], "type": "module", - "exports": "./dist/index.js", + "exports": { + ".": "./dist/index.js", + "./preamble": "./preamble.d.ts" + }, "scripts": { "dev": "tsdown --watch ./src --watch ../common", "build": "tsdown", diff --git a/packages/plugin-react/preamble.d.ts b/packages/plugin-react/preamble.d.ts new file mode 100644 index 000000000..336ce12bb --- /dev/null +++ b/packages/plugin-react/preamble.d.ts @@ -0,0 +1 @@ +export {} 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/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..5922ac765 100644 --- a/playground/ssr-react/vite.config.js +++ b/playground/ssr-react/vite.config.js @@ -41,10 +41,14 @@ 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'), + const template = fs.readFileSync( + path.resolve(_dirname, 'index.html'), + 'utf-8', ) + // const template = await server.transformIndexHtml( + // url, + // fs.readFileSync(path.resolve(_dirname, 'index.html'), 'utf-8'), + // ) const html = template.replace(``, appHtml) res.setHeader('content-type', 'text/html').end(html) } catch (e) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f4e6ae46..d65a67fe6 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.7)(jiti@2.5.1)(lightningcss@1.30.1)(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: From 5667d01c9cb8f04e5e39eae68af0dce6210eaa80 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 6 Oct 2025 10:56:01 +0900 Subject: [PATCH 2/7] chore: cleanup --- playground/ssr-react/vite.config.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/playground/ssr-react/vite.config.js b/playground/ssr-react/vite.config.js index 5922ac765..d5074ecd5 100644 --- a/playground/ssr-react/vite.config.js +++ b/playground/ssr-react/vite.config.js @@ -41,14 +41,12 @@ export default defineConfig({ '/src/entry-server.jsx', ) const appHtml = render(url) + // "@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 template = await server.transformIndexHtml( - // url, - // fs.readFileSync(path.resolve(_dirname, 'index.html'), 'utf-8'), - // ) const html = template.replace(``, appHtml) res.setHeader('content-type', 'text/html').end(html) } catch (e) { From c79a8cd766a87f0db48eae153a3c7fe0f02f6e72 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 6 Oct 2025 11:05:46 +0900 Subject: [PATCH 3/7] fix: fix preamble types --- packages/plugin-react-swc/tsdown.config.ts | 9 ++++++++- .../types}/preamble.d.ts | 0 packages/plugin-react/package.json | 3 ++- packages/plugin-react/types/preamble.d.ts | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) rename packages/{plugin-react => plugin-react-swc/types}/preamble.d.ts (100%) create mode 100644 packages/plugin-react/types/preamble.d.ts 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/preamble.d.ts b/packages/plugin-react-swc/types/preamble.d.ts similarity index 100% rename from packages/plugin-react/preamble.d.ts rename to packages/plugin-react-swc/types/preamble.d.ts diff --git a/packages/plugin-react/package.json b/packages/plugin-react/package.json index b630a36c8..41aa8d580 100644 --- a/packages/plugin-react/package.json +++ b/packages/plugin-react/package.json @@ -17,12 +17,13 @@ "Arnaud Barré" ], "files": [ + "types", "dist" ], "type": "module", "exports": { ".": "./dist/index.js", - "./preamble": "./preamble.d.ts" + "./preamble": "./types/preamble.d.ts" }, "scripts": { "dev": "tsdown --watch ./src --watch ../common", 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 {} From 0e7eb5f5b5a1dfe3b16f815d9008cca86cd43466 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 6 Oct 2025 11:21:27 +0900 Subject: [PATCH 4/7] docs --- packages/plugin-react/README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/plugin-react/README.md b/packages/plugin-react/README.md index c607891ca..cced1a87d 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: +For SSR application, which doesn't make use of [`transformIndexHtml` API](https://vite.dev/guide/api-javascript.html#vitedevserver), the package provides `@vitejs/plugin-react/preamble` to intiialize HMR runtime from client entrypoint, for example: + +```js +// [entry.client.js] +import '@vitejs/plugin-react/preamble' +``` + +Alternatively, you can manually call `transformIndexHtml` during SSR, which sets up equivalent intiialization 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 a following error: ``` Uncaught Error: @vitejs/plugin-react can't detect preamble. Something is wrong. From 37677e6054432d758c04f8187606f62504e1648a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 6 Oct 2025 11:25:00 +0900 Subject: [PATCH 5/7] docs: fix typos and improve wording in preamble section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix typos: "intiialize" → "initialize", "intiialization" → "initialization" - Fix grammar: "a following" → "the following" - Improve wording for clarity and conciseness 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- packages/plugin-react/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/plugin-react/README.md b/packages/plugin-react/README.md index cced1a87d..ca4809151 100644 --- a/packages/plugin-react/README.md +++ b/packages/plugin-react/README.md @@ -104,14 +104,14 @@ Under the hood, this simply updates the React Fash Refresh runtime URL from `/@r ## `@vitejs/plugin-react/preamble` -For SSR application, which doesn't make use of [`transformIndexHtml` API](https://vite.dev/guide/api-javascript.html#vitedevserver), the package provides `@vitejs/plugin-react/preamble` to intiialize HMR runtime from client entrypoint, for example: +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 intiialization code. Here's an example for an Express server: +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) => { @@ -128,7 +128,7 @@ app.get('/', async (req, res, next) => { }) ``` -Otherwise, you'll get a following error: +Otherwise, you'll get the following error: ``` Uncaught Error: @vitejs/plugin-react can't detect preamble. Something is wrong. From 51071acf22ef2274198c2cf8fbd8f3b147af52cd Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 6 Oct 2025 11:29:25 +0900 Subject: [PATCH 6/7] docs: add preamble documentation for plugin-react-swc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add preamble section to plugin-react-swc README - Update CHANGELOG entries for both plugin-react and plugin-react-swc - Document the new virtual module feature for SSR HMR setup 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- packages/plugin-react-swc/CHANGELOG.md | 4 ++++ packages/plugin-react-swc/README.md | 32 ++++++++++++++++++++++++++ packages/plugin-react/CHANGELOG.md | 4 ++++ 3 files changed, 40 insertions(+) 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/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)) From 3473bbb59b4959edf4cf26e0d4fe948c08cde5fc Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 6 Oct 2025 11:31:28 +0900 Subject: [PATCH 7/7] feat: add @vitejs/plugin-react-swc/preamble --- packages/plugin-react-swc/src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) 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, + }), ] }