From a0172d84e3ccea9a36b18e7419a44a53b7e41aba Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:56:29 +0900 Subject: [PATCH] fix(react):`$RefreshSig$ is not defined` with NODE_ENV=production vite dev --- packages/plugin-react/src/index.ts | 38 ++++++++++++++----- playground/node-env-production/App.jsx | 10 +++++ .../__tests__/node-env-production.spec.ts | 8 ++++ playground/node-env-production/index.html | 10 +++++ playground/node-env-production/package.json | 18 +++++++++ playground/node-env-production/vite.config.ts | 15 ++++++++ pnpm-lock.yaml | 13 +++++++ 7 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 playground/node-env-production/App.jsx create mode 100644 playground/node-env-production/__tests__/node-env-production.spec.ts create mode 100644 playground/node-env-production/index.html create mode 100644 playground/node-env-production/package.json create mode 100644 playground/node-env-production/vite.config.ts diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index 3edce150d..9a9019b31 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react/src/index.ts @@ -12,7 +12,7 @@ import { silenceUseClientWarning, virtualPreamblePlugin, } from '@vitejs/react-common' -import type { Plugin } from 'vite' +import type { Plugin, ServerOptions } from 'vite' import { reactRefreshWrapperPlugin } from 'vite/internal' import { reactCompilerPreset } from './reactCompilerPreset' @@ -66,11 +66,18 @@ export default function viteReact(opts: Options = {}): Plugin[] { const jsxImportDevRuntime = `${jsxImportSource}/jsx-dev-runtime` let runningInVite = false - let isProduction = true let skipFastRefresh = true let base: string let isBundledDev = false + function calculateSkipFastRefresh( + isProduction: boolean, + command: 'serve' | 'build', + hmr: ServerOptions['hmr'], + ) { + return isProduction || command === 'build' || hmr === false + } + const viteBabel: Plugin = { name: 'vite:react-babel', enforce: 'pre', @@ -109,11 +116,18 @@ export default function viteReact(opts: Options = {}): Plugin[] { if (config.experimental.bundledDev) { isBundledDev = true } - isProduction = config.isProduction - skipFastRefresh = - isProduction || - config.command === 'build' || - config.server.hmr === false + if ( + skipFastRefresh !== + calculateSkipFastRefresh( + config.isProduction, + config.command, + config.server?.hmr, + ) + ) { + this.warn( + `NODE_ENV (${JSON.stringify(process.env.NODE_ENV)}) or server.hmr was changed by plugins after the react plugin read the config. This may cause unexpected behavior.`, + ) + } }, options(options) { if (!runningInVite) { @@ -148,8 +162,14 @@ export default function viteReact(opts: Options = {}): Plugin[] { const viteConfigPost: Plugin = { name: 'vite:react:config-post', enforce: 'post', - config(userConfig) { - if (userConfig.server?.hmr === false) { + config(userConfig, { command }) { + skipFastRefresh = calculateSkipFastRefresh( + // same with ResolvedConfig.isProduction + process.env.NODE_ENV === 'production', + command, + userConfig.server?.hmr, + ) + if (skipFastRefresh) { return { oxc: { jsx: { diff --git a/playground/node-env-production/App.jsx b/playground/node-env-production/App.jsx new file mode 100644 index 000000000..0b6e5fcf1 --- /dev/null +++ b/playground/node-env-production/App.jsx @@ -0,0 +1,10 @@ +import { useState } from 'react' + +export default function App() { + const [count, setCount] = useState(0) + return ( + + ) +} diff --git a/playground/node-env-production/__tests__/node-env-production.spec.ts b/playground/node-env-production/__tests__/node-env-production.spec.ts new file mode 100644 index 000000000..a9a69a84a --- /dev/null +++ b/playground/node-env-production/__tests__/node-env-production.spec.ts @@ -0,0 +1,8 @@ +import { expect, test } from 'vitest' +import { page } from '~utils' + +test('app works with NODE_ENV=production', async () => { + expect(await page.textContent('button')).toMatch('count is 0') + await page.click('button') + expect(await page.textContent('button')).toMatch('count is 1') +}) diff --git a/playground/node-env-production/index.html b/playground/node-env-production/index.html new file mode 100644 index 000000000..7417c442d --- /dev/null +++ b/playground/node-env-production/index.html @@ -0,0 +1,10 @@ +
+ diff --git a/playground/node-env-production/package.json b/playground/node-env-production/package.json new file mode 100644 index 000000000..ffeb95618 --- /dev/null +++ b/playground/node-env-production/package.json @@ -0,0 +1,18 @@ +{ + "name": "@vitejs/test-react-node-env-production", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "debug": "node --inspect-brk vite", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.7", + "react-dom": "^19.2.7" + }, + "devDependencies": { + "@vitejs/plugin-react": "workspace:*" + } +} diff --git a/playground/node-env-production/vite.config.ts b/playground/node-env-production/vite.config.ts new file mode 100644 index 000000000..ff3d97d6d --- /dev/null +++ b/playground/node-env-production/vite.config.ts @@ -0,0 +1,15 @@ +import react from '@vitejs/plugin-react' +import type { UserConfig } from 'vite' + +process.env.NODE_ENV = 'production' + +const config: UserConfig = { + server: { port: 8911 /* Should be unique */ }, + plugins: [react()], + build: { + // to make tests faster + minify: false, + }, +} + +export default config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff4a4784f..332dbe2b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -992,6 +992,19 @@ importers: specifier: workspace:* version: link:../../packages/plugin-react + playground/node-env-production: + dependencies: + react: + specifier: ^19.2.7 + version: 19.2.7 + react-dom: + specifier: ^19.2.7 + version: 19.2.7(react@19.2.7) + devDependencies: + '@vitejs/plugin-react': + specifier: workspace:* + version: link:../../packages/plugin-react + playground/react: dependencies: jsx-entry: