diff --git a/.gitignore b/.gitignore
index 901a775c5af23..330a92ca02c7b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,7 @@ wp-tests-config.php
/src/wp-includes/class-wp-block-parser-frame.php
/src/wp-includes/theme.json
/packagehash.txt
+/.gutenberg-hash
/artifacts
/setup.log
/coverage
diff --git a/Gruntfile.js b/Gruntfile.js
index 457ddccab2c19..a6635fc3428e5 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1458,10 +1458,16 @@ module.exports = function(grunt) {
} );
} );
- grunt.registerTask( 'gutenberg-integrate', 'Complete Gutenberg integration workflow.', [
- 'gutenberg-build',
- 'gutenberg-copy'
- ] );
+ grunt.registerTask( 'gutenberg-sync', 'Syncs Gutenberg checkout and build if ref has changed.', function() {
+ const done = this.async();
+ grunt.util.spawn( {
+ cmd: 'node',
+ args: [ 'tools/gutenberg/sync-gutenberg.js' ],
+ opts: { stdio: 'inherit' }
+ }, function( error ) {
+ done( ! error );
+ } );
+ } );
grunt.registerTask( 'copy-vendor-scripts', 'Copies vendor scripts from node_modules to wp-includes/js/dist/vendor/.', function() {
const done = this.async();
@@ -1896,7 +1902,8 @@ module.exports = function(grunt) {
grunt.task.run( [
'build:js',
'build:css',
- 'gutenberg-integrate',
+ 'gutenberg-sync',
+ 'gutenberg-copy',
'copy-vendor-scripts',
'build:certificates'
] );
@@ -1906,7 +1913,8 @@ module.exports = function(grunt) {
'build:files',
'build:js',
'build:css',
- 'gutenberg-integrate',
+ 'gutenberg-sync',
+ 'gutenberg-copy',
'copy-vendor-scripts',
'replace:source-maps',
'verify:build'
diff --git a/package.json b/package.json
index efd866faccdea..a5d54d773efb6 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"url": "https://develop.svn.wordpress.org/trunk"
},
"gutenberg": {
- "ref": "892bfad51d2261f44f3a21f934b1c72bd29a2449"
+ "ref": "7bf80ea84eb8b62eceb1bb3fe82e42163673ca79"
},
"engines": {
"node": ">=20.10.0",
@@ -99,7 +99,7 @@
"wicg-inert": "3.1.3"
},
"scripts": {
- "postinstall": "npm run gutenberg:checkout",
+ "postinstall": "npm run gutenberg:sync && npm run gutenberg:copy -- --dev",
"build": "grunt build",
"build:dev": "grunt build --dev",
"dev": "grunt watch --dev",
@@ -126,7 +126,7 @@
"gutenberg:checkout": "node tools/gutenberg/checkout-gutenberg.js",
"gutenberg:build": "node tools/gutenberg/build-gutenberg.js",
"gutenberg:copy": "node tools/gutenberg/copy-gutenberg-build.js",
- "gutenberg:integrate": "npm run gutenberg:checkout && npm run gutenberg:build && npm run gutenberg:copy",
+ "gutenberg:sync": "node tools/gutenberg/sync-gutenberg.js",
"vendor:copy": "node tools/vendors/copy-vendors.js",
"sync-gutenberg-packages": "grunt sync-gutenberg-packages",
"postsync-gutenberg-packages": "grunt wp-packages:sync-stable-blocks && grunt build --dev && grunt build"
diff --git a/src/index.php b/src/index.php
index 91c0517857339..544acab805b09 100644
--- a/src/index.php
+++ b/src/index.php
@@ -15,7 +15,7 @@
* Load the actual index.php file if the assets were already built.
* Note: WPINC is not defined yet, it is defined later in wp-settings.php.
*/
-if ( file_exists( ABSPATH . 'wp-includes/js/dist/edit-post.js' ) ) {
+if ( file_exists( ABSPATH . 'wp-includes/js/jquery/jquery.js' ) && is_dir( ABSPATH . 'wp-includes/build' ) ) {
require_once ABSPATH . '_index.php';
return;
}
diff --git a/src/wp-admin/font-library.php b/src/wp-admin/font-library.php
index 5c9fe21264997..abc2ea4f4da70 100644
--- a/src/wp-admin/font-library.php
+++ b/src/wp-admin/font-library.php
@@ -19,10 +19,10 @@
}
// Check if Gutenberg build files are available
-if ( ! function_exists( 'font_library_wp_admin_render_page' ) ) {
+if ( ! function_exists( 'wp_font_library_wp_admin_render_page' ) ) {
wp_die(
'
' . __( 'Font Library is not available.' ) . '
' .
- '' . __( 'The Font Library requires Gutenberg integration. Please run npm run gutenberg:integrate to build the necessary files.' ) . '
',
+ '' . __( 'The Font Library requires Gutenberg build files. Please run npm install to build the necessary files.' ) . '
',
503
);
}
@@ -33,6 +33,6 @@
require_once ABSPATH . 'wp-admin/admin-header.php';
// Render the Font Library page
-font_library_wp_admin_render_page();
+wp_font_library_wp_admin_render_page();
require_once ABSPATH . 'wp-admin/admin-footer.php';
diff --git a/src/wp-admin/index.php b/src/wp-admin/index.php
index 628096844c08b..7c549b6d8d4b7 100644
--- a/src/wp-admin/index.php
+++ b/src/wp-admin/index.php
@@ -6,7 +6,7 @@
* please refer to wp-admin/_index.php.
*/
-if ( file_exists( __DIR__ . '/../wp-includes/js/dist/edit-post.js' ) ) {
+if ( file_exists( __DIR__ . '/../wp-includes/js/jquery/jquery.js' ) && is_dir( __DIR__ . '/../wp-includes/build' ) ) {
require_once __DIR__ . '/_index.php';
return;
}
diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php
index 2a9968608106a..e2c594d7ecfc6 100644
--- a/src/wp-includes/blocks.php
+++ b/src/wp-includes/blocks.php
@@ -2421,6 +2421,10 @@ function parse_blocks( $content ) {
*/
$parser_class = apply_filters( 'block_parser_class', 'WP_Block_Parser' );
+ if ( ! class_exists( $parser_class ) ) {
+ return array();
+ }
+
$parser = new $parser_class();
return $parser->parse( $content );
}
diff --git a/src/wp-includes/blocks/index.php b/src/wp-includes/blocks/index.php
index 98615ea1ba766..65d2cb5ad67a3 100644
--- a/src/wp-includes/blocks/index.php
+++ b/src/wp-includes/blocks/index.php
@@ -13,9 +13,15 @@
define( 'BLOCKS_PATH', ABSPATH . WPINC . '/blocks/' );
// Include files required for core blocks registration.
-require BLOCKS_PATH . 'legacy-widget.php';
-require BLOCKS_PATH . 'widget-group.php';
-require BLOCKS_PATH . 'require-dynamic-blocks.php';
+if ( file_exists( BLOCKS_PATH . 'legacy-widget.php' ) ) {
+ require BLOCKS_PATH . 'legacy-widget.php';
+}
+if ( file_exists( BLOCKS_PATH . 'widget-group.php' ) ) {
+ require BLOCKS_PATH . 'widget-group.php';
+}
+if ( file_exists( BLOCKS_PATH . 'require-dynamic-blocks.php' ) ) {
+ require BLOCKS_PATH . 'require-dynamic-blocks.php';
+}
/**
* Registers core block style handles.
@@ -43,6 +49,9 @@ function register_core_block_style_handles() {
static $core_blocks_meta;
if ( ! $core_blocks_meta ) {
+ if ( ! file_exists( BLOCKS_PATH . 'blocks-json.php' ) ) {
+ return;
+ }
$core_blocks_meta = require BLOCKS_PATH . 'blocks-json.php';
}
@@ -150,6 +159,9 @@ static function ( $file ) use ( $normalized_blocks_path ) {
* @since 5.5.0
*/
function register_core_block_types_from_metadata() {
+ if ( ! file_exists( BLOCKS_PATH . 'require-static-blocks.php' ) ) {
+ return;
+ }
$block_folders = require BLOCKS_PATH . 'require-static-blocks.php';
foreach ( $block_folders as $block_folder ) {
register_block_type_from_metadata(
@@ -169,6 +181,9 @@ function register_core_block_types_from_metadata() {
* @since 6.7.0
*/
function wp_register_core_block_metadata_collection() {
+ if ( ! file_exists( BLOCKS_PATH . 'blocks-json.php' ) ) {
+ return;
+ }
wp_register_block_metadata_collection(
BLOCKS_PATH,
BLOCKS_PATH . 'blocks-json.php'
diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php
index f59f877775b77..b3a5a1ca135b4 100644
--- a/src/wp-includes/formatting.php
+++ b/src/wp-includes/formatting.php
@@ -5227,6 +5227,11 @@ function wp_pre_kses_less_than_callback( $matches ) {
* @return string Filtered text to run through KSES.
*/
function wp_pre_kses_block_attributes( $content, $allowed_html, $allowed_protocols ) {
+ // If the block parser isn't available, skip block attribute filtering.
+ if ( ! class_exists( 'WP_Block_Parser' ) ) {
+ return $content;
+ }
+
/*
* `filter_block_content` is expected to call `wp_kses`. Temporarily remove
* the filter to avoid recursion.
diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php
index 388940e33e1ea..f39fa49ab4b32 100644
--- a/src/wp-includes/script-loader.php
+++ b/src/wp-includes/script-loader.php
@@ -281,7 +281,8 @@ function wp_default_packages_scripts( $scripts ) {
* 'annotations.js' => array('dependencies' => array(...), 'version' => '...'),
* 'api-fetch.js' => array(...
*/
- $assets = include ABSPATH . WPINC . "/assets/script-loader-packages{$suffix}.php";
+ $assets_file = ABSPATH . WPINC . "/assets/script-loader-packages{$suffix}.php";
+ $assets = file_exists( $assets_file ) ? include $assets_file : array();
foreach ( $assets as $file_name => $package_data ) {
$basename = str_replace( $suffix . '.js', '', basename( $file_name ) );
diff --git a/src/wp-includes/script-modules.php b/src/wp-includes/script-modules.php
index 85cc4accf2e52..f851d41bf21f2 100644
--- a/src/wp-includes/script-modules.php
+++ b/src/wp-includes/script-modules.php
@@ -153,7 +153,8 @@ function wp_default_script_modules() {
* 'interactivity-router/index.min.js' => array('dependencies' => array(…), 'version' => '…'),
* 'block-library/navigation/view.min.js' => …
*/
- $assets = include ABSPATH . WPINC . "/assets/script-modules-packages{$suffix}.php";
+ $assets_file = ABSPATH . WPINC . "/assets/script-modules-packages{$suffix}.php";
+ $assets = file_exists( $assets_file ) ? include $assets_file : array();
foreach ( $assets as $file_name => $script_module_data ) {
/*
diff --git a/src/wp-settings.php b/src/wp-settings.php
index adaa0b161c3f6..14749eff51041 100644
--- a/src/wp-settings.php
+++ b/src/wp-settings.php
@@ -242,8 +242,12 @@
require ABSPATH . WPINC . '/cron.php';
require ABSPATH . WPINC . '/deprecated.php';
require ABSPATH . WPINC . '/script-loader.php';
-require ABSPATH . WPINC . '/build/routes.php';
-require ABSPATH . WPINC . '/build/pages.php';
+if ( file_exists( ABSPATH . WPINC . '/build/routes.php' ) ) {
+ require ABSPATH . WPINC . '/build/routes.php';
+}
+if ( file_exists( ABSPATH . WPINC . '/build/pages.php' ) ) {
+ require ABSPATH . WPINC . '/build/pages.php';
+}
require ABSPATH . WPINC . '/taxonomy.php';
require ABSPATH . WPINC . '/class-wp-taxonomy.php';
require ABSPATH . WPINC . '/class-wp-term.php';
@@ -373,9 +377,15 @@
require ABSPATH . WPINC . '/class-wp-block.php';
require ABSPATH . WPINC . '/class-wp-block-list.php';
require ABSPATH . WPINC . '/class-wp-block-metadata-registry.php';
-require ABSPATH . WPINC . '/class-wp-block-parser-block.php';
-require ABSPATH . WPINC . '/class-wp-block-parser-frame.php';
-require ABSPATH . WPINC . '/class-wp-block-parser.php';
+if ( file_exists( ABSPATH . WPINC . '/class-wp-block-parser-block.php' ) ) {
+ require ABSPATH . WPINC . '/class-wp-block-parser-block.php';
+}
+if ( file_exists( ABSPATH . WPINC . '/class-wp-block-parser-frame.php' ) ) {
+ require ABSPATH . WPINC . '/class-wp-block-parser-frame.php';
+}
+if ( file_exists( ABSPATH . WPINC . '/class-wp-block-parser.php' ) ) {
+ require ABSPATH . WPINC . '/class-wp-block-parser.php';
+}
require ABSPATH . WPINC . '/class-wp-classic-to-block-menu-converter.php';
require ABSPATH . WPINC . '/class-wp-navigation-fallback.php';
require ABSPATH . WPINC . '/block-bindings.php';
diff --git a/tools/gutenberg/build-gutenberg.js b/tools/gutenberg/build-gutenberg.js
index 0b989a13495ec..cf6dd973953bc 100644
--- a/tools/gutenberg/build-gutenberg.js
+++ b/tools/gutenberg/build-gutenberg.js
@@ -156,7 +156,25 @@ async function main() {
console.log( `✅ Build completed in ${ duration }s` );
} catch ( error ) {
console.error( '❌ Build failed:', error.message );
- process.exit( 1 );
+ throw error;
+ } finally {
+ // Restore Gutenberg's package.json regardless of success or failure
+ await restorePackageJson();
+ }
+}
+
+/**
+ * Restore Gutenberg's package.json to its original state.
+ */
+async function restorePackageJson() {
+ console.log( '\n🔄 Restoring Gutenberg package.json...' );
+ try {
+ await exec( 'git', [ 'checkout', '--', 'package.json' ], {
+ cwd: gutenbergDir,
+ } );
+ console.log( '✅ package.json restored' );
+ } catch ( error ) {
+ console.warn( '⚠️ Could not restore package.json:', error.message );
}
}
diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js
index 7257e5f3b1d8d..a66ca113e0cc2 100644
--- a/tools/gutenberg/copy-gutenberg-build.js
+++ b/tools/gutenberg/copy-gutenberg-build.js
@@ -48,7 +48,7 @@ const COPY_CONFIG = {
source: 'scripts',
destination: 'js/dist',
copyDirectories: true, // Copy subdirectories
- patterns: [ '*.js', '*.js.map' ],
+ patterns: [ '*.js' ],
// Rename vendors/ to vendor/ when copying
directoryRenames: {
vendors: 'vendor',
@@ -916,25 +916,21 @@ async function main() {
// Only copy react-jsx-runtime files, skip react and react-dom
const vendorFiles = fs.readdirSync( src );
let copiedCount = 0;
+ fs.mkdirSync( dest, { recursive: true } );
for ( const file of vendorFiles ) {
- if ( file.startsWith( 'react-jsx-runtime' ) ) {
+ if (
+ file.startsWith( 'react-jsx-runtime' ) &&
+ file.endsWith( '.js' )
+ ) {
const srcFile = path.join( src, file );
const destFile = path.join( dest, file );
- fs.mkdirSync( dest, { recursive: true } );
-
- if (
- file.endsWith( '.js' ) &&
- ! file.endsWith( '.js.map' )
- ) {
- let content = fs.readFileSync(
- srcFile,
- 'utf8'
- );
- content = removeSourceMaps( content );
- fs.writeFileSync( destFile, content );
- } else {
- fs.copyFileSync( srcFile, destFile );
- }
+
+ let content = fs.readFileSync(
+ srcFile,
+ 'utf8'
+ );
+ content = removeSourceMaps( content );
+ fs.writeFileSync( destFile, content );
copiedCount++;
}
}
@@ -955,9 +951,7 @@ async function main() {
for ( const file of packageFiles ) {
if (
- /^index\.(js|js\.map|min\.js|min\.js\.map|min\.asset\.php)$/.test(
- file
- )
+ /^index\.(js|min\.js|min\.asset\.php)$/.test( file )
) {
const srcFile = path.join( src, file );
// Replace 'index.' with 'package-name.'
@@ -972,10 +966,7 @@ async function main() {
} );
// Apply source map removal for .js files
- if (
- file.endsWith( '.js' ) &&
- ! file.endsWith( '.js.map' )
- ) {
+ if ( file.endsWith( '.js' ) ) {
let content = fs.readFileSync(
srcFile,
'utf8'
@@ -983,7 +974,7 @@ async function main() {
content = removeSourceMaps( content );
fs.writeFileSync( destPath, content );
} else {
- // Copy other files as-is
+ // Copy other files as-is (.min.asset.php)
fs.copyFileSync( srcFile, destPath );
}
}
@@ -991,22 +982,15 @@ async function main() {
}
} else if (
entry.isFile() &&
- /\.(js|js\.map)$/.test( entry.name )
+ entry.name.endsWith( '.js' )
) {
// Copy root-level JS files
const dest = path.join( scriptsDest, entry.name );
fs.mkdirSync( path.dirname( dest ), { recursive: true } );
- if (
- entry.name.endsWith( '.js' ) &&
- ! entry.name.endsWith( '.js.map' )
- ) {
- let content = fs.readFileSync( src, 'utf8' );
- content = removeSourceMaps( content );
- fs.writeFileSync( dest, content );
- } else {
- fs.copyFileSync( src, dest );
- }
+ let content = fs.readFileSync( src, 'utf8' );
+ content = removeSourceMaps( content );
+ fs.writeFileSync( dest, content );
}
}
diff --git a/tools/gutenberg/sync-gutenberg.js b/tools/gutenberg/sync-gutenberg.js
new file mode 100644
index 0000000000000..814188d920cfa
--- /dev/null
+++ b/tools/gutenberg/sync-gutenberg.js
@@ -0,0 +1,149 @@
+#!/usr/bin/env node
+
+/**
+ * Sync Gutenberg Script
+ *
+ * This script ensures Gutenberg is checked out and built for the correct ref.
+ * It follows the same pattern as install-changed:
+ * - Stores the built ref in .gutenberg-hash
+ * - Compares current package.json ref with stored hash
+ * - Only runs checkout + build when they differ
+ *
+ * @package WordPress
+ */
+
+const { spawn } = require( 'child_process' );
+const fs = require( 'fs' );
+const path = require( 'path' );
+
+// Paths
+const rootDir = path.resolve( __dirname, '../..' );
+const gutenbergDir = path.join( rootDir, 'gutenberg' );
+const gutenbergBuildDir = path.join( gutenbergDir, 'build' );
+const packageJsonPath = path.join( rootDir, 'package.json' );
+const hashFilePath = path.join( rootDir, '.gutenberg-hash' );
+
+/**
+ * Execute a command and return a promise.
+ *
+ * @param {string} command - Command to execute.
+ * @param {string[]} args - Command arguments.
+ * @param {Object} options - Spawn options.
+ * @return {Promise} Promise that resolves when command completes.
+ */
+function exec( command, args, options = {} ) {
+ return new Promise( ( resolve, reject ) => {
+ const child = spawn( command, args, {
+ cwd: options.cwd || rootDir,
+ stdio: 'inherit',
+ shell: process.platform === 'win32',
+ ...options,
+ } );
+
+ child.on( 'close', ( code ) => {
+ if ( code !== 0 ) {
+ reject(
+ new Error(
+ `${ command } ${ args.join( ' ' ) } failed with code ${ code }`
+ )
+ );
+ } else {
+ resolve();
+ }
+ } );
+
+ child.on( 'error', reject );
+ } );
+}
+
+/**
+ * Read the expected Gutenberg ref from package.json.
+ *
+ * @return {string} The Gutenberg ref.
+ */
+function getExpectedRef() {
+ const packageJson = JSON.parse( fs.readFileSync( packageJsonPath, 'utf8' ) );
+ const ref = packageJson.gutenberg?.ref;
+
+ if ( ! ref ) {
+ throw new Error( 'Missing "gutenberg.ref" in package.json' );
+ }
+
+ return ref;
+}
+
+/**
+ * Read the stored hash from .gutenberg-hash file.
+ *
+ * @return {string|null} The stored ref, or null if file doesn't exist.
+ */
+function getStoredHash() {
+ try {
+ return fs.readFileSync( hashFilePath, 'utf8' ).trim();
+ } catch ( error ) {
+ return null;
+ }
+}
+
+/**
+ * Write the ref to .gutenberg-hash file.
+ *
+ * @param {string} ref - The ref to store.
+ */
+function writeHash( ref ) {
+ fs.writeFileSync( hashFilePath, ref + '\n' );
+}
+
+/**
+ * Check if Gutenberg build exists.
+ *
+ * @return {boolean} True if build directory exists.
+ */
+function hasBuild() {
+ return fs.existsSync( gutenbergBuildDir );
+}
+
+/**
+ * Main execution function.
+ */
+async function main() {
+ console.log( '🔍 Checking Gutenberg sync status...' );
+
+ const expectedRef = getExpectedRef();
+ const storedHash = getStoredHash();
+
+ console.log( ` Expected ref: ${ expectedRef }` );
+ console.log( ` Stored hash: ${ storedHash || '(none)' }` );
+
+ // Check if we need to rebuild
+ if ( storedHash === expectedRef && hasBuild() ) {
+ console.log( '✅ Gutenberg is already synced and built' );
+ return;
+ }
+
+ if ( storedHash !== expectedRef ) {
+ console.log( '\n📦 Gutenberg ref has changed, rebuilding...' );
+ } else {
+ console.log( '\n📦 Gutenberg build not found, building...' );
+ }
+
+ // Run checkout
+ console.log( '\n🔄 Running gutenberg:checkout...' );
+ await exec( 'node', [ 'tools/gutenberg/checkout-gutenberg.js' ] );
+
+ // Run build
+ console.log( '\n🔄 Running gutenberg:build...' );
+ await exec( 'node', [ 'tools/gutenberg/build-gutenberg.js' ] );
+
+ // Write the hash after successful build
+ writeHash( expectedRef );
+ console.log( `\n✅ Updated .gutenberg-hash to ${ expectedRef }` );
+
+ console.log( '\n✅ Gutenberg sync complete!' );
+}
+
+// Run main function
+main().catch( ( error ) => {
+ console.error( '❌ Sync failed:', error.message );
+ process.exit( 1 );
+} );