From 71b6ef3f8918c2d30ed776280a9be686cdfbcda9 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Wed, 25 Mar 2026 14:25:08 +0100 Subject: [PATCH 1/3] Build: detect version and generate asset.php for vendor scripts --- bin/packages/build-vendors.mjs | 97 ++++++++++++++++++++++++++-------- lib/client-assets.php | 52 +++++++++--------- 2 files changed, 103 insertions(+), 46 deletions(-) diff --git a/bin/packages/build-vendors.mjs b/bin/packages/build-vendors.mjs index e8efe0ab45fd27..cb80bcfd9d9290 100755 --- a/bin/packages/build-vendors.mjs +++ b/bin/packages/build-vendors.mjs @@ -2,6 +2,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; +import { readFile, writeFile, mkdir } from 'fs/promises'; import esbuild from 'esbuild'; const __dirname = path.dirname( fileURLToPath( import.meta.url ) ); @@ -14,27 +15,75 @@ const VENDOR_SCRIPTS = [ name: 'react', global: 'React', handle: 'react', + dependencies: [ 'wp-polyfill' ], }, { name: 'react-dom', global: 'ReactDOM', handle: 'react-dom', + dependencies: [ 'react' ], }, { name: 'react/jsx-runtime', global: 'ReactJSXRuntime', handle: 'react-jsx-runtime', + dependencies: [ 'react' ], }, ]; +/** + * Read the version from a package's package.json in node_modules. + * + * @param {string} packageName npm package name (e.g., 'react', 'react-dom'). + * @return {Promise} The package version string. + */ +async function getPackageVersion( packageName ) { + const packageJsonPath = path.join( + ROOT_DIR, + 'node_modules', + packageName, + 'package.json' + ); + const packageJson = JSON.parse( + await readFile( packageJsonPath, 'utf-8' ) + ); + return packageJson.version; +} + +/** + * Generate a .asset.php file for a vendor script. + * + * @param {Object} config Vendor script configuration. + * @param {string} config.handle WordPress script handle. + * @param {string} config.name Package name (e.g., 'react', 'react/jsx-runtime'). + * @param {string[]} config.dependencies WordPress script dependencies. + */ +async function generateAssetFile( config ) { + const { handle, name, dependencies } = config; + + // The npm package name is the first segment of the name (e.g., 'react/jsx-runtime' -> 'react'). + const packageName = name.split( '/' )[ 0 ]; + const version = await getPackageVersion( packageName ); + + const dependenciesString = dependencies + .map( ( dep ) => `'${ dep }'` ) + .join( ', ' ); + const assetContent = ` array(${ dependenciesString }), 'version' => '${ version }');`; + + const assetFilePath = path.join( VENDORS_DIR, `${ handle }.min.asset.php` ); + await mkdir( path.dirname( assetFilePath ), { recursive: true } ); + await writeFile( assetFilePath, assetContent ); +} + /** * Bundle a vendor script from node_modules into an IIFE script. * This is used to build packages like React that don't ship UMD builds. * - * @param {Object} config Vendor script configuration. - * @param {string} config.name Package name (e.g., 'react', 'react-dom', 'react/jsx-runtime'). - * @param {string} config.global Global variable name (e.g., 'React', 'ReactDOM'). - * @param {string} config.handle WordPress script handle (e.g., 'react', 'react-dom'). + * @param {Object} config Vendor script configuration. + * @param {string} config.name Package name (e.g., 'react', 'react-dom', 'react/jsx-runtime'). + * @param {string} config.global Global variable name (e.g., 'React', 'ReactDOM'). + * @param {string} config.handle WordPress script handle (e.g., 'react', 'react-dom'). + * @param {string[]} config.dependencies WordPress script dependencies. * @return {Promise} Promise that resolves when all builds are finished. */ async function bundleVendorScript( config ) { @@ -64,23 +113,29 @@ async function bundleVendorScript( config ) { }, }; - // Build both minified and non-minified versions - await Promise.all( - [ false, true ].map( ( production ) => { - const outputFile = handle + ( production ? '.min.js' : '.js' ); - return esbuild.build( { - entryPoints: [ name ], - outfile: path.join( VENDORS_DIR, outputFile ), - bundle: true, - format: 'iife', - globalName: global, - minify: production, - target: 'esnext', // Don't transpile, just bundle. - platform: 'browser', - plugins: [ reactExternalPlugin ], - } ); - } ) - ); + const esbuildOptions = { + entryPoints: [ name ], + bundle: true, + format: 'iife', + globalName: global, + target: 'esnext', + platform: 'browser', + plugins: [ reactExternalPlugin ], + }; + + await Promise.all( [ + esbuild.build( { + ...esbuildOptions, + outfile: path.join( VENDORS_DIR, handle + '.js' ), + minify: false, + } ), + esbuild.build( { + ...esbuildOptions, + outfile: path.join( VENDORS_DIR, handle + '.min.js' ), + minify: true, + } ), + generateAssetFile( config ), + ] ); } /** diff --git a/lib/client-assets.php b/lib/client-assets.php index 62689eb9772190..64ad7aead2745e 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -367,32 +367,34 @@ function gutenberg_enqueue_stored_styles( $options = array() ) { * @param WP_Scripts $scripts WP_Scripts instance. */ function gutenberg_register_vendor_scripts( $scripts ) { - $extension = SCRIPT_DEBUG ? '.js' : '.min.js'; - - gutenberg_override_script( - $scripts, - 'react', - gutenberg_url( 'build/scripts/vendors/react' . $extension ), - // WordPress Core in `wp_register_development_scripts` sets `wp-react-refresh-entry` as a dependency to `react` when `SCRIPT_DEBUG` is true. - // We need to preserve that here. - SCRIPT_DEBUG ? array( 'wp-react-refresh-entry', 'wp-polyfill' ) : array( 'wp-polyfill' ), - '18' - ); - gutenberg_override_script( - $scripts, - 'react-dom', - gutenberg_url( 'build/scripts/vendors/react-dom' . $extension ), - array( 'react' ), - '18' - ); + $extension = SCRIPT_DEBUG ? '.js' : '.min.js'; + $vendors_dir = gutenberg_dir_path() . 'build/scripts/vendors/'; + + $vendor_handles = array( 'react', 'react-dom', 'react-jsx-runtime' ); + + foreach ( $vendor_handles as $handle ) { + $asset_file = $vendors_dir . $handle . '.min.asset.php'; + $asset = file_exists( $asset_file ) ? require $asset_file : array(); + $dependencies = $asset['dependencies'] ?? array(); + $version = $asset['version'] ?? '0'; + + gutenberg_override_script( + $scripts, + $handle, + gutenberg_url( 'build/scripts/vendors/' . $handle . $extension ), + $dependencies, + $version + ); + } - gutenberg_override_script( - $scripts, - 'react-jsx-runtime', - gutenberg_url( 'build/scripts/vendors/react-jsx-runtime' . $extension ), - array( 'react' ), - '18' - ); + // WordPress Core in `wp_register_development_scripts` sets `wp-react-refresh-entry` + // as a dependency to `react` when `SCRIPT_DEBUG` is true. Preserve that here. + if ( SCRIPT_DEBUG ) { + $react = $scripts->query( 'react', 'registered' ); + if ( $react && ! in_array( 'wp-react-refresh-entry', $react->deps, true ) ) { + $react->deps[] = 'wp-react-refresh-entry'; + } + } } add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts' ); From 7aa0ccaf53ff45d3823f09951010781f69a88f2b Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Thu, 26 Mar 2026 13:05:18 +0100 Subject: [PATCH 2/3] Add backport changelog entry --- backport-changelog/7.0/11359.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 backport-changelog/7.0/11359.md diff --git a/backport-changelog/7.0/11359.md b/backport-changelog/7.0/11359.md new file mode 100644 index 00000000000000..ec11dbb22791cf --- /dev/null +++ b/backport-changelog/7.0/11359.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/11359 + +* https://github.com/WordPress/gutenberg/pull/76811 From 399e9ac5531726fb769480a9fbc36419c7816653 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Thu, 26 Mar 2026 18:26:45 +0100 Subject: [PATCH 3/3] Move changelog entry from 7.0 to 7.1 --- backport-changelog/{7.0 => 7.1}/11359.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backport-changelog/{7.0 => 7.1}/11359.md (100%) diff --git a/backport-changelog/7.0/11359.md b/backport-changelog/7.1/11359.md similarity index 100% rename from backport-changelog/7.0/11359.md rename to backport-changelog/7.1/11359.md