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 ); +} );