diff --git a/package-lock.json b/package-lock.json index bd27510a8bd56..01b1c86e5afd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "devDependencies": { "@lodder/grunt-postcss": "^3.1.1", "@playwright/test": "1.56.1", + "@pmmmwh/react-refresh-webpack-plugin": "0.6.1", "@wordpress/e2e-test-utils-playwright": "1.33.2", "@wordpress/prettier-config": "4.33.1", "@wordpress/scripts": "30.26.2", @@ -70,6 +71,7 @@ "postcss": "8.5.6", "prettier": "npm:wp-prettier@3.0.3", "qunit": "~2.24.2", + "react-refresh": "0.14.0", "sass": "1.94.0", "sinon": "16.1.3", "sinon-test": "~3.1.6", @@ -4008,6 +4010,121 @@ "node": ">=18" } }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.1.tgz", + "integrity": "sha512-95DXXJxNkpYu+sqmpDp7vbw9JCyiNpHuCsvuMuOgVFrKQlwEIn9Y1+NNIQJq+zFL+eWyxw6htthB5CtdwJupNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "anser": "^2.1.1", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "@types/webpack": "5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <5.0.0", + "webpack": "^5.0.0", + "webpack-dev-server": "^4.8.0 || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.24", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", @@ -7449,6 +7566,13 @@ "ajv": "^6.9.1" } }, + "node_modules/anser": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz", + "integrity": "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==", + "dev": true, + "license": "MIT" + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", diff --git a/package.json b/package.json index 7ae0193bfe6c3..efd866faccdea 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ ], "devDependencies": { "@lodder/grunt-postcss": "^3.1.1", + "@pmmmwh/react-refresh-webpack-plugin": "0.6.1", "@playwright/test": "1.56.1", "@wordpress/e2e-test-utils-playwright": "1.33.2", "@wordpress/prettier-config": "4.33.1", @@ -61,6 +62,7 @@ "postcss": "8.5.6", "prettier": "npm:wp-prettier@3.0.3", "qunit": "~2.24.2", + "react-refresh": "0.14.0", "sass": "1.94.0", "sinon": "16.1.3", "sinon-test": "~3.1.6", diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php index c6dba96da74b8..19ccaf7e6dbd4 100644 --- a/src/wp-includes/deprecated.php +++ b/src/wp-includes/deprecated.php @@ -6479,17 +6479,3 @@ function wp_print_auto_sizes_contain_css_fix() { registered['react'] ) + || defined( 'WP_RUN_CORE_TESTS' ) + ) { + return; + } + + // React Refresh runtime - exposes ReactRefreshRuntime global. + // No dependencies. + $scripts->add( + 'wp-react-refresh-runtime', + '/wp-includes/js/dist/development/react-refresh-runtime.js', + array(), + '0.14.0' + ); + + // React Refresh entry - injects runtime into global hook. + // Must load before React to set up hooks. + $scripts->add( + 'wp-react-refresh-entry', + '/wp-includes/js/dist/development/react-refresh-entry.js', + array( 'wp-react-refresh-runtime' ), + '0.14.0' + ); + + // Add entry as a dependency of React so it loads first. + // See https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/TROUBLESHOOTING.md#externalising-react. + $scripts->registered['react']->deps[] = 'wp-react-refresh-entry'; +} + /** * Returns contents of an inline script used in appending polyfill scripts for * browsers which fail the provided tests. The provided array is a mapping from @@ -618,6 +662,7 @@ function wp_tinymce_inline_scripts() { */ function wp_default_packages( $scripts ) { wp_default_packages_vendor( $scripts ); + wp_register_development_scripts( $scripts ); wp_register_tinymce_scripts( $scripts ); wp_default_packages_scripts( $scripts ); diff --git a/tools/webpack/development.js b/tools/webpack/development.js new file mode 100644 index 0000000000000..10102c69865ac --- /dev/null +++ b/tools/webpack/development.js @@ -0,0 +1,72 @@ +/** + * External dependencies + */ +const TerserPlugin = require( 'terser-webpack-plugin' ); + +/** + * Internal dependencies + */ +const { baseDir } = require( './shared' ); + +/** + * Webpack configuration for development scripts (React Refresh). + * + * These scripts enable hot module replacement for block development + * when using `@wordpress/scripts` with the `--hot` flag. + * + * @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. + */ +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 { + 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]', + }, + optimization: { + minimize: true, + moduleIds: 'deterministic', + minimizer: [ + new TerserPlugin( { + include: /\.min\.js$/, + extractComments: false, + } ), + ], + }, + watch: env.watch, + }; +}; diff --git a/webpack.config.js b/webpack.config.js index 089c2d67dabec..e04bd8d46e497 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,5 @@ const mediaConfig = require( './tools/webpack/media' ); +const developmentConfig = require( './tools/webpack/development' ); module.exports = function ( env = { environment: 'production', watch: false, buildTarget: false } @@ -11,10 +12,13 @@ module.exports = function ( env.buildTarget = env.mode === 'production' ? 'build/' : 'src/'; } - // Only building Core-specific media files. + // 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). - const config = [ mediaConfig( env ) ]; + const config = [ + mediaConfig( env ), + developmentConfig( env ), + ]; return config; };