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-runtime.js b/packages/common/refresh-runtime.js index e798d000e..ffee3bbab 100644 --- a/packages/common/refresh-runtime.js +++ b/packages/common/refresh-runtime.js @@ -314,6 +314,9 @@ function collectCustomHooksForSignature(type) { } export function injectIntoGlobalHook(globalObject) { + if (globalObject.__vite_plugin_react_injectIntoGlobalHook) return + globalObject.__vite_plugin_react_injectIntoGlobalHook = true + // For React Native, the global hook will be set up by require('react-devtools-core'). // That code will run before us. So we need to monkeypatch functions on existing hook. diff --git a/packages/common/refresh-utils.ts b/packages/common/refresh-utils.ts index db0e587e4..da9c9233c 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,55 @@ function $RefreshSig$() { return RefreshRuntime.createSignatureFunctionForTransf return newCode } + +export function virtualPreamblePlugin(opts: { + isEnabled: () => boolean + reactRefreshHost?: string +}): Plugin { + const VIRTUAL_NAME = 'virtual:@vitejs/plugin-react/preamble' + let importSource = VIRTUAL_NAME + if (opts.reactRefreshHost) { + importSource = opts.reactRefreshHost + '/@id/__x00__' + VIRTUAL_NAME + } + return { + name: 'vite:react-virtual-preamble', + apply: 'serve', + resolveId: { + order: 'pre', + filter: { id: exactRegex(VIRTUAL_NAME) }, + handler(source) { + if (source === VIRTUAL_NAME) { + return '\0' + source + } + }, + }, + load: { + filter: { id: exactRegex('\0' + VIRTUAL_NAME) }, + handler(id) { + if (id === '\0' + VIRTUAL_NAME) { + if (opts.isEnabled()) { + // vite dev import analysis can rewrite base + return preambleCode.replace('__BASE__', '/') + } + return '' + } + }, + }, + transform: { + filter: { code: /__REACT_DEVTOOLS_GLOBAL_HOOK__/ }, + handler(code, id, options) { + if (options?.ssr) return + if (id === runtimePublicPath) return + + // this is expected to match `react`, `react-dom`, and `react-dom/client`. + // they are all optimized to be esm during dev. + if ( + opts.isEnabled() && + code.includes('__REACT_DEVTOOLS_GLOBAL_HOOK__') + ) { + return `import ${JSON.stringify(importSource)};` + code + } + }, + }, + } +} diff --git a/packages/plugin-react-oxc/src/index.ts b/packages/plugin-react-oxc/src/index.ts index e42b7356a..0702c0ddc 100644 --- a/packages/plugin-react-oxc/src/index.ts +++ b/packages/plugin-react-oxc/src/index.ts @@ -4,9 +4,9 @@ import { readFileSync } from 'node:fs' import type { BuildOptions, Plugin } from 'vite' import { addRefreshWrapper, - getPreambleCode, runtimePublicPath, silenceUseClientWarning, + virtualPreamblePlugin, } from '@vitejs/react-common' import { exactRegex } from '@rolldown/pluginutils' @@ -143,17 +143,15 @@ export default function viteReact(opts: Options = {}): Plugin[] { return newCode ? { code: newCode, map: null } : undefined }, }, - transformIndexHtml(_, config) { - if (!skipFastRefresh) - return [ - { - tag: 'script', - attrs: { type: 'module' }, - children: getPreambleCode(config.server!.config.base), - }, - ] - }, } - return [viteConfig, viteConfigPost, viteRefreshRuntime, viteRefreshWrapper] + return [ + viteConfig, + viteConfigPost, + viteRefreshRuntime, + viteRefreshWrapper, + virtualPreamblePlugin({ + isEnabled: () => !skipFastRefresh, + }) as any, // rolldown-vite type mismatch + ] } diff --git a/packages/plugin-react-swc/src/index.ts b/packages/plugin-react-swc/src/index.ts index 425bac15c..c65108cc9 100644 --- a/packages/plugin-react-swc/src/index.ts +++ b/packages/plugin-react-swc/src/index.ts @@ -13,9 +13,9 @@ import { import type { Plugin } from 'vite' import { addRefreshWrapper, - getPreambleCode, runtimePublicPath, silenceUseClientWarning, + virtualPreamblePlugin, } from '@vitejs/react-common' import * as vite from 'vite' import { exactRegex } from '@rolldown/pluginutils' @@ -165,17 +165,6 @@ const react = (_options?: Options): Plugin[] => { ) } }, - transformIndexHtml: (_, config) => { - if (!hmrDisabled) { - return [ - { - tag: 'script', - attrs: { type: 'module' }, - children: getPreambleCode(config.server!.config.base), - }, - ] - } - }, async transform(code, _id, transformOptions) { const id = _id.split('?')[0] const refresh = !transformOptions?.ssr && !hmrDisabled @@ -205,6 +194,10 @@ const react = (_options?: Options): Plugin[] => { return { code: newCode ?? result.code, map: result.map } }, }, + virtualPreamblePlugin({ + isEnabled: () => !hmrDisabled, + reactRefreshHost: options.reactRefreshHost, + }), options.plugins ? { name: 'vite:react-swc', diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index a01f693e5..c4ba1c374 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, @@ -506,16 +507,6 @@ export default function viteReact(opts: Options = {}): Plugin[] { } }, }, - transformIndexHtml() { - if (!skipFastRefresh && !isFullBundle) - return [ - { - tag: 'script', - attrs: { type: 'module' }, - children: getPreambleCode(base), - }, - ] - }, } return [ @@ -524,6 +515,10 @@ export default function viteReact(opts: Options = {}): Plugin[] { ? [viteRefreshWrapper, viteConfigPost, viteReactRefreshFullBundleMode] : []), viteReactRefresh, + virtualPreamblePlugin({ + isEnabled: () => !skipFastRefresh && !isFullBundle, + reactRefreshHost: opts.reactRefreshHost, + }), ] } diff --git a/playground/react/vite.config.ts b/playground/react/vite.config.ts index d257635de..192210e3a 100644 --- a/playground/react/vite.config.ts +++ b/playground/react/vite.config.ts @@ -4,7 +4,11 @@ import type { UserConfig } from 'vite' const config: UserConfig = { server: { port: 8902 /* Should be unique */ }, mode: 'development', - plugins: [react()], + plugins: [ + react({ + reactRefreshHost: 'http://localhost:8902', + }), + ], build: { // to make tests faster minify: false, diff --git a/playground/ssr-react/vite.config.js b/playground/ssr-react/vite.config.js index f9258a1d0..6a658a0fe 100644 --- a/playground/ssr-react/vite.config.js +++ b/playground/ssr-react/vite.config.js @@ -41,9 +41,9 @@ 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 html = template.replace(``, appHtml) res.setHeader('content-type', 'text/html').end(html) 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: