From 00a8f929d4d523c46c3b5b866fda8d13263c944a Mon Sep 17 00:00:00 2001 From: Douglas Date: Wed, 25 Feb 2026 17:36:15 -0300 Subject: [PATCH 1/4] wp-build: Stop bundling Core packages, generate prerequisites asset instead wp-build previously bundled boot, route, theme, and private-apis from node_modules and registered them as WordPress scripts/modules. This caused the plugin to overwrite Core's or Gutenberg's versions at runtime, which is particularly dangerous for private-apis (Symbol/WeakMap mismatch causes silent failures). Replace the bundling block with a new generatePagePrerequisitesAsset() function that statically analyzes @wordpress/boot's package.json to produce a build/page-prerequisites.asset.php file listing the classic script handles WordPress needs to load. The 4 packages are now treated as external dependencies provided by Core (WP 7.0+) or Gutenberg. Co-Authored-By: Claude Opus 4.6 --- packages/wp-build/lib/build.mjs | 140 +++++++++++++----- .../lib/wordpress-externals-plugin.mjs | 40 ++--- .../templates/page-wp-admin.php.template | 2 +- packages/wp-build/templates/page.php.template | 2 +- 4 files changed, 128 insertions(+), 56 deletions(-) diff --git a/packages/wp-build/lib/build.mjs b/packages/wp-build/lib/build.mjs index c5ad3cbea9038b..84b12370c26ba5 100755 --- a/packages/wp-build/lib/build.mjs +++ b/packages/wp-build/lib/build.mjs @@ -55,7 +55,10 @@ import { renderTemplateToString, } from './php-generator.mjs'; import { getPackageInfo, getPackageInfoFromFile } from './package-utils.mjs'; -import { createWordpressExternalsPlugin } from './wordpress-externals-plugin.mjs'; +import { + createWordpressExternalsPlugin, + vendorExternals, +} from './wordpress-externals-plugin.mjs'; import { getAllRoutes, getRouteFiles, @@ -878,6 +881,102 @@ async function inferStyleDependencies( scriptDependencies, packageName ) { return styleDeps; } +/** + * Generate a page-prerequisites.asset.php file by statically analyzing + * the `@wordpress/boot` package.json dependencies. + * + * This replaces the old approach of bundling boot/route/theme/private-apis + * from node_modules. Instead, we read boot's declared dependencies and + * produce an asset file listing the classic script handles that WordPress + * needs to load before the page's boot module can initialize. + * + * WordPress's script dependency system handles transitive dependencies + * automatically, so we only need to capture direct dependencies. + */ +async function generatePagePrerequisitesAsset() { + const { createRequire } = await import( 'module' ); + const require = createRequire( import.meta.url ); + + // Resolve @wordpress/boot's package.json from node_modules + const bootPackageJsonPath = require.resolve( + '@wordpress/boot/package.json', + { paths: [ ROOT_DIR ] } + ); + + const bootPackageJson = JSON.parse( + await readFile( bootPackageJsonPath, 'utf8' ) + ); + + const dependencies = new Set(); + + // Collect all direct dependencies and peerDependencies + const allDeps = { + ...( bootPackageJson.dependencies || {} ), + ...( bootPackageJson.peerDependencies || {} ), + }; + + for ( const depName of Object.keys( allDeps ) ) { + // Handle @wordpress/* packages + if ( depName.startsWith( '@wordpress/' ) ) { + const shortName = depName.split( '/' )[ 1 ]; + let depPackageJson; + + try { + const depPath = require.resolve( `${ depName }/package.json`, { + paths: [ ROOT_DIR ], + } ); + + depPackageJson = JSON.parse( + await readFile( depPath, 'utf8' ) + ); + } catch { + continue; + } + + // Only add packages that register as classic scripts (wpScript) + if ( depPackageJson.wpScript ) { + dependencies.add( `wp-${ shortName }` ); + } + // Packages with only wpScriptModuleExports are handled by the + // module system and don't need to be listed here. + continue; + } + + // Handle vendor dependencies using the shared vendorExternals map + if ( vendorExternals[ depName ] ) { + dependencies.add( vendorExternals[ depName ].handle ); + + // React's JSX transform compiles JSX to calls into react-jsx-runtime, + // which WordPress registers as a separate script handle. + if ( depName === 'react' ) { + dependencies.add( 'react-jsx-runtime' ); + } + + continue; + } + } + + // Generate content-based version hash from boot's package.json + const hash = createHash( 'sha256' ); + hash.update( await readFile( bootPackageJsonPath ) ); + const version = hash.digest( 'hex' ).slice( 0, 20 ); + + const dependenciesString = Array.from( dependencies ) + .sort() + .map( ( dep ) => `'${ dep }'` ) + .join( ', ' ); + + const assetContent = ` array(${ dependenciesString }), 'version' => '${ version }');`; + + const assetFilePath = path.join( + BUILD_DIR, + 'page-prerequisites.asset.php' + ); + + await mkdir( path.dirname( assetFilePath ), { recursive: true } ); + await writeFile( assetFilePath, assetContent ); +} + /** * Generate PHP files for script module registration. * @@ -1821,46 +1920,13 @@ async function buildAll( baseUrlExpression ) { }; } ); - // Bundle boot, route, and theme packages from node_modules when pages exist + // Generate page prerequisites asset file when pages exist if ( pageData.length > 0 ) { try { - const { createRequire } = await import( 'module' ); - const require = createRequire( import.meta.url ); - - // Resolve the @wordpress packages directory from node_modules - const bootPackageJson = require.resolve( - '@wordpress/boot/package.json', - { paths: [ ROOT_DIR ] } - ); - const wordpressPackagesDir = path.dirname( - path.dirname( bootPackageJson ) - ); - - // Bundle boot, route, theme, and private-apis packages - const externalPackages = [ - 'boot', - 'route', - 'theme', - 'private-apis', - ]; - for ( const pkgName of externalPackages ) { - const result = await bundlePackage( pkgName, { - sourceDir: wordpressPackagesDir, - handlePrefix: 'wp', - scriptGlobal: 'wp', - packageNamespace: 'wordpress', - } ); - - if ( result && result.modules ) { - modules.push( ...result.modules ); - } - if ( result && result.scripts ) { - scripts.push( ...result.scripts ); - } - } + await generatePagePrerequisitesAsset(); } catch ( error ) { console.warn( - '\n⚠️ Warning: Could not bundle WordPress packages for pages:', + '\n⚠️ Warning: Could not generate page prerequisites asset:', error.message ); } diff --git a/packages/wp-build/lib/wordpress-externals-plugin.mjs b/packages/wp-build/lib/wordpress-externals-plugin.mjs index 281749e185b3ec..f56d82cfb8a794 100644 --- a/packages/wp-build/lib/wordpress-externals-plugin.mjs +++ b/packages/wp-build/lib/wordpress-externals-plugin.mjs @@ -11,6 +11,27 @@ import { createHash } from 'crypto'; */ import { getPackageInfo } from './package-utils.mjs'; +/** + * Map of vendor packages to their global variables and WordPress script handles. + * Shared between the externals plugin and the page prerequisites asset generator. + */ +export const vendorExternals = { + react: { global: 'React', handle: 'react' }, + 'react-dom': { global: 'ReactDOM', handle: 'react-dom' }, + 'react/jsx-runtime': { + global: 'ReactJSXRuntime', + handle: 'react-jsx-runtime', + }, + 'react/jsx-dev-runtime': { + global: 'ReactJSXRuntime', + handle: 'react-jsx-runtime', + }, + moment: { global: 'moment', handle: 'moment' }, + lodash: { global: 'lodash', handle: 'lodash' }, + 'lodash-es': { global: 'lodash', handle: 'lodash' }, + jquery: { global: 'jQuery', handle: 'jquery' }, +}; + /** * Generate a content hash from file contents. * Uses SHA256 algorithm for broad compatibility across Node.js versions. @@ -121,23 +142,8 @@ export function createWordpressExternalsPlugin( return false; } - // Map of vendor packages to their global variables and handles - const vendorExternals = { - react: { global: 'React', handle: 'react' }, - 'react-dom': { global: 'ReactDOM', handle: 'react-dom' }, - 'react/jsx-runtime': { - global: 'ReactJSXRuntime', - handle: 'react-jsx-runtime', - }, - 'react/jsx-dev-runtime': { - global: 'ReactJSXRuntime', - handle: 'react-jsx-runtime', - }, - moment: { global: 'moment', handle: 'moment' }, - lodash: { global: 'lodash', handle: 'lodash' }, - 'lodash-es': { global: 'lodash', handle: 'lodash' }, - jquery: { global: 'jQuery', handle: 'jquery' }, - }; + // Vendor externals map is defined at the module level and exported + // for sharing with the page prerequisites asset generator. // Build list of package namespace configurations const packageExternals = [ diff --git a/packages/wp-build/templates/page-wp-admin.php.template b/packages/wp-build/templates/page-wp-admin.php.template index 19444c4ad94af4..f7085598b2aefb 100644 --- a/packages/wp-build/templates/page-wp-admin.php.template +++ b/packages/wp-build/templates/page-wp-admin.php.template @@ -144,7 +144,7 @@ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts( $hook_suf $routes = {{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes(); // Get boot module asset file for dependencies - $asset_file = __DIR__ . '/../../modules/boot/index.min.asset.php'; + $asset_file = __DIR__ . '/../../page-prerequisites.asset.php'; if ( file_exists( $asset_file ) ) { $asset = require $asset_file; diff --git a/packages/wp-build/templates/page.php.template b/packages/wp-build/templates/page.php.template index e29c677314eba2..a1e566229597d1 100644 --- a/packages/wp-build/templates/page.php.template +++ b/packages/wp-build/templates/page.php.template @@ -150,7 +150,7 @@ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_render_page() { $routes = {{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_routes(); // Get boot module asset file for dependencies - $asset_file = __DIR__ . '/../../modules/boot/index.min.asset.php'; + $asset_file = __DIR__ . '/../../page-prerequisites.asset.php'; if ( file_exists( $asset_file ) ) { $asset = require $asset_file; From febc204ba86456e610cb96e5a79652e7b4b618db Mon Sep 17 00:00:00 2001 From: Douglas Date: Fri, 6 Mar 2026 16:21:59 -0300 Subject: [PATCH 2/4] wp-build: Use boot's module asset file instead of page-prerequisites Instead of statically analyzing boot's package.json to generate a separate page-prerequisites.asset.php, use the asset file that esbuild naturally produces when building boot as a module. For Gutenberg, boot is already built in Phase 2. For external plugins, build boot from node_modules solely for the asset file, then clean up the unused JS output. Co-Authored-By: Claude Opus 4.6 --- packages/wp-build/lib/build.mjs | 158 ++++++------------ .../lib/wordpress-externals-plugin.mjs | 40 ++--- .../templates/page-wp-admin.php.template | 2 +- packages/wp-build/templates/page.php.template | 2 +- 4 files changed, 70 insertions(+), 132 deletions(-) diff --git a/packages/wp-build/lib/build.mjs b/packages/wp-build/lib/build.mjs index 84b12370c26ba5..1ec004f9390afd 100755 --- a/packages/wp-build/lib/build.mjs +++ b/packages/wp-build/lib/build.mjs @@ -55,10 +55,7 @@ import { renderTemplateToString, } from './php-generator.mjs'; import { getPackageInfo, getPackageInfoFromFile } from './package-utils.mjs'; -import { - createWordpressExternalsPlugin, - vendorExternals, -} from './wordpress-externals-plugin.mjs'; +import { createWordpressExternalsPlugin } from './wordpress-externals-plugin.mjs'; import { getAllRoutes, getRouteFiles, @@ -881,102 +878,6 @@ async function inferStyleDependencies( scriptDependencies, packageName ) { return styleDeps; } -/** - * Generate a page-prerequisites.asset.php file by statically analyzing - * the `@wordpress/boot` package.json dependencies. - * - * This replaces the old approach of bundling boot/route/theme/private-apis - * from node_modules. Instead, we read boot's declared dependencies and - * produce an asset file listing the classic script handles that WordPress - * needs to load before the page's boot module can initialize. - * - * WordPress's script dependency system handles transitive dependencies - * automatically, so we only need to capture direct dependencies. - */ -async function generatePagePrerequisitesAsset() { - const { createRequire } = await import( 'module' ); - const require = createRequire( import.meta.url ); - - // Resolve @wordpress/boot's package.json from node_modules - const bootPackageJsonPath = require.resolve( - '@wordpress/boot/package.json', - { paths: [ ROOT_DIR ] } - ); - - const bootPackageJson = JSON.parse( - await readFile( bootPackageJsonPath, 'utf8' ) - ); - - const dependencies = new Set(); - - // Collect all direct dependencies and peerDependencies - const allDeps = { - ...( bootPackageJson.dependencies || {} ), - ...( bootPackageJson.peerDependencies || {} ), - }; - - for ( const depName of Object.keys( allDeps ) ) { - // Handle @wordpress/* packages - if ( depName.startsWith( '@wordpress/' ) ) { - const shortName = depName.split( '/' )[ 1 ]; - let depPackageJson; - - try { - const depPath = require.resolve( `${ depName }/package.json`, { - paths: [ ROOT_DIR ], - } ); - - depPackageJson = JSON.parse( - await readFile( depPath, 'utf8' ) - ); - } catch { - continue; - } - - // Only add packages that register as classic scripts (wpScript) - if ( depPackageJson.wpScript ) { - dependencies.add( `wp-${ shortName }` ); - } - // Packages with only wpScriptModuleExports are handled by the - // module system and don't need to be listed here. - continue; - } - - // Handle vendor dependencies using the shared vendorExternals map - if ( vendorExternals[ depName ] ) { - dependencies.add( vendorExternals[ depName ].handle ); - - // React's JSX transform compiles JSX to calls into react-jsx-runtime, - // which WordPress registers as a separate script handle. - if ( depName === 'react' ) { - dependencies.add( 'react-jsx-runtime' ); - } - - continue; - } - } - - // Generate content-based version hash from boot's package.json - const hash = createHash( 'sha256' ); - hash.update( await readFile( bootPackageJsonPath ) ); - const version = hash.digest( 'hex' ).slice( 0, 20 ); - - const dependenciesString = Array.from( dependencies ) - .sort() - .map( ( dep ) => `'${ dep }'` ) - .join( ', ' ); - - const assetContent = ` array(${ dependenciesString }), 'version' => '${ version }');`; - - const assetFilePath = path.join( - BUILD_DIR, - 'page-prerequisites.asset.php' - ); - - await mkdir( path.dirname( assetFilePath ), { recursive: true } ); - await writeFile( assetFilePath, assetContent ); -} - /** * Generate PHP files for script module registration. * @@ -1920,15 +1821,58 @@ async function buildAll( baseUrlExpression ) { }; } ); - // Generate page prerequisites asset file when pages exist + // Build boot module for page prerequisites when pages exist if ( pageData.length > 0 ) { + const bootAssetPath = path.join( + BUILD_DIR, + 'modules', + 'boot', + 'index.min.asset.php' + ); + + // If boot wasn't built as a project package (external plugin case), + // build it from node_modules to generate the asset file. + // The JS output isn't used at runtime — Core/Gutenberg provides boot. try { - await generatePagePrerequisitesAsset(); - } catch ( error ) { - console.warn( - '\n⚠️ Warning: Could not generate page prerequisites asset:', - error.message - ); + await readFile( bootAssetPath ); + } catch { + try { + const { createRequire } = await import( 'module' ); + const require = createRequire( import.meta.url ); + + const bootPkgPath = require.resolve( + '@wordpress/boot/package.json', + { paths: [ ROOT_DIR ] } + ); + + const wordpressPackagesDir = path.dirname( + path.dirname( bootPkgPath ) + ); + + await bundlePackage( 'boot', { + sourceDir: wordpressPackagesDir, + handlePrefix: 'wp', + scriptGlobal: 'wp', + packageNamespace: 'wordpress', + } ); + // Clean up JS output — only the asset file is needed. + const bootDir = path.join( BUILD_DIR, 'modules', 'boot' ); + for ( const file of [ + 'index.js', + 'index.js.map', + 'index.min.js', + 'index.min.js.map', + ] ) { + await unlink( path.join( bootDir, file ) ).catch( + () => {} + ); + } + } catch ( error ) { + console.warn( + '\n⚠️ Warning: Could not build boot module for page prerequisites:', + error.message + ); + } } } diff --git a/packages/wp-build/lib/wordpress-externals-plugin.mjs b/packages/wp-build/lib/wordpress-externals-plugin.mjs index f56d82cfb8a794..281749e185b3ec 100644 --- a/packages/wp-build/lib/wordpress-externals-plugin.mjs +++ b/packages/wp-build/lib/wordpress-externals-plugin.mjs @@ -11,27 +11,6 @@ import { createHash } from 'crypto'; */ import { getPackageInfo } from './package-utils.mjs'; -/** - * Map of vendor packages to their global variables and WordPress script handles. - * Shared between the externals plugin and the page prerequisites asset generator. - */ -export const vendorExternals = { - react: { global: 'React', handle: 'react' }, - 'react-dom': { global: 'ReactDOM', handle: 'react-dom' }, - 'react/jsx-runtime': { - global: 'ReactJSXRuntime', - handle: 'react-jsx-runtime', - }, - 'react/jsx-dev-runtime': { - global: 'ReactJSXRuntime', - handle: 'react-jsx-runtime', - }, - moment: { global: 'moment', handle: 'moment' }, - lodash: { global: 'lodash', handle: 'lodash' }, - 'lodash-es': { global: 'lodash', handle: 'lodash' }, - jquery: { global: 'jQuery', handle: 'jquery' }, -}; - /** * Generate a content hash from file contents. * Uses SHA256 algorithm for broad compatibility across Node.js versions. @@ -142,8 +121,23 @@ export function createWordpressExternalsPlugin( return false; } - // Vendor externals map is defined at the module level and exported - // for sharing with the page prerequisites asset generator. + // Map of vendor packages to their global variables and handles + const vendorExternals = { + react: { global: 'React', handle: 'react' }, + 'react-dom': { global: 'ReactDOM', handle: 'react-dom' }, + 'react/jsx-runtime': { + global: 'ReactJSXRuntime', + handle: 'react-jsx-runtime', + }, + 'react/jsx-dev-runtime': { + global: 'ReactJSXRuntime', + handle: 'react-jsx-runtime', + }, + moment: { global: 'moment', handle: 'moment' }, + lodash: { global: 'lodash', handle: 'lodash' }, + 'lodash-es': { global: 'lodash', handle: 'lodash' }, + jquery: { global: 'jQuery', handle: 'jquery' }, + }; // Build list of package namespace configurations const packageExternals = [ diff --git a/packages/wp-build/templates/page-wp-admin.php.template b/packages/wp-build/templates/page-wp-admin.php.template index f7085598b2aefb..19444c4ad94af4 100644 --- a/packages/wp-build/templates/page-wp-admin.php.template +++ b/packages/wp-build/templates/page-wp-admin.php.template @@ -144,7 +144,7 @@ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_enqueue_scripts( $hook_suf $routes = {{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_wp_admin_routes(); // Get boot module asset file for dependencies - $asset_file = __DIR__ . '/../../page-prerequisites.asset.php'; + $asset_file = __DIR__ . '/../../modules/boot/index.min.asset.php'; if ( file_exists( $asset_file ) ) { $asset = require $asset_file; diff --git a/packages/wp-build/templates/page.php.template b/packages/wp-build/templates/page.php.template index a1e566229597d1..e29c677314eba2 100644 --- a/packages/wp-build/templates/page.php.template +++ b/packages/wp-build/templates/page.php.template @@ -150,7 +150,7 @@ function {{PREFIX}}_{{PAGE_SLUG_UNDERSCORE}}_render_page() { $routes = {{PREFIX}}_get_{{PAGE_SLUG_UNDERSCORE}}_routes(); // Get boot module asset file for dependencies - $asset_file = __DIR__ . '/../../page-prerequisites.asset.php'; + $asset_file = __DIR__ . '/../../modules/boot/index.min.asset.php'; if ( file_exists( $asset_file ) ) { $asset = require $asset_file; From 21e9e9f8f1037cacd6b944bfc8040b4572701ece Mon Sep 17 00:00:00 2001 From: Douglas Date: Mon, 9 Mar 2026 19:07:45 -0300 Subject: [PATCH 3/4] remove assets file generation for plugins --- packages/wp-build/lib/build.mjs | 55 --------------------------------- 1 file changed, 55 deletions(-) diff --git a/packages/wp-build/lib/build.mjs b/packages/wp-build/lib/build.mjs index 1ec004f9390afd..9de4efadfe0469 100755 --- a/packages/wp-build/lib/build.mjs +++ b/packages/wp-build/lib/build.mjs @@ -1821,61 +1821,6 @@ async function buildAll( baseUrlExpression ) { }; } ); - // Build boot module for page prerequisites when pages exist - if ( pageData.length > 0 ) { - const bootAssetPath = path.join( - BUILD_DIR, - 'modules', - 'boot', - 'index.min.asset.php' - ); - - // If boot wasn't built as a project package (external plugin case), - // build it from node_modules to generate the asset file. - // The JS output isn't used at runtime — Core/Gutenberg provides boot. - try { - await readFile( bootAssetPath ); - } catch { - try { - const { createRequire } = await import( 'module' ); - const require = createRequire( import.meta.url ); - - const bootPkgPath = require.resolve( - '@wordpress/boot/package.json', - { paths: [ ROOT_DIR ] } - ); - - const wordpressPackagesDir = path.dirname( - path.dirname( bootPkgPath ) - ); - - await bundlePackage( 'boot', { - sourceDir: wordpressPackagesDir, - handlePrefix: 'wp', - scriptGlobal: 'wp', - packageNamespace: 'wordpress', - } ); - // Clean up JS output — only the asset file is needed. - const bootDir = path.join( BUILD_DIR, 'modules', 'boot' ); - for ( const file of [ - 'index.js', - 'index.js.map', - 'index.min.js', - 'index.min.js.map', - ] ) { - await unlink( path.join( bootDir, file ) ).catch( - () => {} - ); - } - } catch ( error ) { - console.warn( - '\n⚠️ Warning: Could not build boot module for page prerequisites:', - error.message - ); - } - } - } - console.log( '\n📄 Generating PHP registration files...\n' ); const phpReplacements = await getPhpReplacements( ROOT_DIR, From ad6866cb753c25e3407438580187d68d607136c3 Mon Sep 17 00:00:00 2001 From: Douglas Date: Tue, 10 Mar 2026 15:02:59 -0300 Subject: [PATCH 4/4] wp-build: Add breaking change changelog entry for WP 7.0 requirement Co-Authored-By: Claude Opus 4.6 --- packages/wp-build/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/wp-build/CHANGELOG.md b/packages/wp-build/CHANGELOG.md index 8a8f3f6eb44b8f..b2cc4f583a1e20 100644 --- a/packages/wp-build/CHANGELOG.md +++ b/packages/wp-build/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Changes + +- `@wordpress/boot`, `@wordpress/route`, `@wordpress/theme`, and `@wordpress/private-apis` are no longer bundled. They are now expected to be provided by WordPress Core (7.0+) or the Gutenberg plugin. + ### Enhancements - Avoid unexpected results when typecasting `IS_GUTENBERG_PLUGIN` and `IS_WORDPRESS_CORE` values to Booleans ([#75844](https://github.com/WordPress/gutenberg/pull/75844)).