From fd7c81b5cb5f63eb875ebd7a3a3e1bbda7fa26be Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 19 Jan 2026 15:15:40 +0100 Subject: [PATCH] Build/Test Tools: Fix React Refresh hot reloading for block plugins. The react-refresh-entry.js script was bundling its own copy of react-refresh/runtime instead of using the window.ReactRefreshRuntime global. This created two separate runtime instances: one in the entry script where hooks were set up, and one as the window global that plugins use for performReactRefresh(). Since they were different instances, the refresh never triggered. This fix splits the webpack development config into two separate configs: - Runtime config: bundles react-refresh and exposes it as window.ReactRefreshRuntime (no externals) - Entry config: uses window.ReactRefreshRuntime as an external, ensuring hooks are set up on the same runtime instance Follow-up to [60055]. Props youknowriad. Co-Authored-By: Claude Opus 4.5 --- tools/webpack/development.js | 75 +++++++++++++++++++++++------------- webpack.config.js | 3 +- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/tools/webpack/development.js b/tools/webpack/development.js index f7fd1f653b81e..4d2df930bd1d2 100644 --- a/tools/webpack/development.js +++ b/tools/webpack/development.js @@ -14,45 +14,25 @@ const { baseDir } = require( './shared' ); * These scripts enable hot module replacement for plugins * using `@wordpress/scripts` with the `--hot` flag. * + * Returns two separate configs: + * 1. Runtime config - bundles react-refresh/runtime and exposes it as window.ReactRefreshRuntime + * 2. Entry config - uses the window global as an external to ensure both scripts share the same runtime instance + * * @param {Object} env Environment options. * @param {string} env.buildTarget Build target directory. * @param {boolean} env.watch Whether to watch for changes. - * @return {Object} Webpack configuration object. + * @return {Object[]} Array of webpack configuration objects. */ module.exports = function( env = { buildTarget: 'src/', watch: false } ) { const buildTarget = env.buildTarget || 'src/'; - const entry = { - // React Refresh runtime - exposes ReactRefreshRuntime global. - [ buildTarget + 'wp-includes/js/dist/development/react-refresh-runtime.js' ]: { - import: 'react-refresh/runtime', - library: { - name: 'ReactRefreshRuntime', - type: 'window', - }, - }, - [ buildTarget + 'wp-includes/js/dist/development/react-refresh-runtime.min.js' ]: { - import: 'react-refresh/runtime', - library: { - name: 'ReactRefreshRuntime', - type: 'window', - }, - }, - // React Refresh entry - injects runtime into global hook before React loads. - [ buildTarget + 'wp-includes/js/dist/development/react-refresh-entry.js' ]: - '@pmmmwh/react-refresh-webpack-plugin/client/ReactRefreshEntry.js', - [ buildTarget + 'wp-includes/js/dist/development/react-refresh-entry.min.js' ]: - '@pmmmwh/react-refresh-webpack-plugin/client/ReactRefreshEntry.js', - }; - - return { + const baseConfig = { target: 'browserslist', // Must use development mode to preserve process.env.NODE_ENV checks // in the source files. These scripts are only used during development. mode: 'development', devtool: false, cache: true, - entry, output: { path: baseDir, filename: '[name]', @@ -69,4 +49,47 @@ module.exports = function( env = { buildTarget: 'src/', watch: false } ) { }, watch: env.watch, }; + + // Config for react-refresh-runtime.js - bundles the runtime and exposes + // it as window.ReactRefreshRuntime. No externals - this creates the global. + const runtimeConfig = { + ...baseConfig, + name: 'runtime', + entry: { + [ buildTarget + 'wp-includes/js/dist/development/react-refresh-runtime.js' ]: { + import: 'react-refresh/runtime', + library: { + name: 'ReactRefreshRuntime', + type: 'window', + }, + }, + [ buildTarget + 'wp-includes/js/dist/development/react-refresh-runtime.min.js' ]: { + import: 'react-refresh/runtime', + library: { + name: 'ReactRefreshRuntime', + type: 'window', + }, + }, + }, + }; + + // Config for react-refresh-entry.js - uses window.ReactRefreshRuntime as an + // external instead of bundling its own copy. This ensures the hooks set up + // by the entry are on the same runtime instance that plugins use for + // performReactRefresh(). + const entryConfig = { + ...baseConfig, + name: 'entry', + entry: { + [ buildTarget + 'wp-includes/js/dist/development/react-refresh-entry.js' ]: + '@pmmmwh/react-refresh-webpack-plugin/client/ReactRefreshEntry.js', + [ buildTarget + 'wp-includes/js/dist/development/react-refresh-entry.min.js' ]: + '@pmmmwh/react-refresh-webpack-plugin/client/ReactRefreshEntry.js', + }, + externals: { + 'react-refresh/runtime': 'ReactRefreshRuntime', + }, + }; + + return [ runtimeConfig, entryConfig ]; }; diff --git a/webpack.config.js b/webpack.config.js index e04bd8d46e497..29ebbd696b875 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,9 +15,10 @@ module.exports = function ( // Only building Core-specific media files and development scripts. // Blocks, packages, script modules, and vendors are now sourced from // the Gutenberg build (see tools/gutenberg/copy-gutenberg-build.js). + // Note: developmentConfig returns an array of configs, so we spread it. const config = [ mediaConfig( env ), - developmentConfig( env ), + ...developmentConfig( env ), ]; return config;