From f8a7238651b9594dbe94331571b7f86d18230b95 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:06:01 -0500 Subject: [PATCH 01/30] Remove the need to clone the Gutenberg repository. This switches to downloading a prebuilt zip file uploaded to the GitHub Container Registry for a given commit instead. This eliminates the need to run any Gutenberg build scripts within `wordpress-develop`. --- Gruntfile.js | 22 --- package.json | 1 - tools/gutenberg/build-gutenberg.js | 192 ----------------------- tools/gutenberg/checkout-gutenberg.js | 218 ++++++++++---------------- tools/gutenberg/sync-gutenberg.js | 149 ------------------ 5 files changed, 80 insertions(+), 502 deletions(-) delete mode 100644 tools/gutenberg/build-gutenberg.js delete mode 100644 tools/gutenberg/sync-gutenberg.js diff --git a/Gruntfile.js b/Gruntfile.js index b5c69553f3f0d..2c3b2481b88c7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1486,17 +1486,6 @@ module.exports = function(grunt) { } ); } ); - grunt.registerTask( 'gutenberg-build', 'Builds the Gutenberg repository.', function() { - const done = this.async(); - grunt.util.spawn( { - cmd: 'node', - args: [ 'tools/gutenberg/build-gutenberg.js' ], - opts: { stdio: 'inherit' } - }, function( error ) { - done( ! error ); - } ); - } ); - grunt.registerTask( 'gutenberg-copy', 'Copies Gutenberg build output to WordPress Core.', function() { const done = this.async(); const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; @@ -1509,17 +1498,6 @@ module.exports = function(grunt) { } ); } ); - 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(); const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; diff --git a/package.json b/package.json index 4d3f3823f0d99..6e40afdf9f989 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,6 @@ "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js", "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", "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:sync": "node tools/gutenberg/sync-gutenberg.js", "vendor:copy": "node tools/vendors/copy-vendors.js", diff --git a/tools/gutenberg/build-gutenberg.js b/tools/gutenberg/build-gutenberg.js deleted file mode 100644 index 01cf4489de1fa..0000000000000 --- a/tools/gutenberg/build-gutenberg.js +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env node - -/** - * Build Gutenberg Script - * - * This script builds the Gutenberg repository using its build command - * as specified in the root package.json's "gutenberg" configuration. - * - * @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' ); - -/** - * Execute a command and return a promise. - * Captures output and only displays it on failure for cleaner logs. - * - * @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 ) => { - let stdout = ''; - let stderr = ''; - - const child = spawn( command, args, { - cwd: options.cwd || rootDir, - stdio: [ 'ignore', 'pipe', 'pipe' ], - shell: process.platform === 'win32', // Use shell on Windows to find .cmd files - ...options, - } ); - - // Capture output - if ( child.stdout ) { - child.stdout.on( 'data', ( data ) => { - stdout += data.toString(); - } ); - } - - if ( child.stderr ) { - child.stderr.on( 'data', ( data ) => { - stderr += data.toString(); - } ); - } - - child.on( 'close', ( code ) => { - if ( code !== 0 ) { - // Show output only on failure - if ( stdout ) { - console.error( '\nCommand output:' ); - console.error( stdout ); - } - if ( stderr ) { - console.error( '\nCommand errors:' ); - console.error( stderr ); - } - reject( - new Error( - `${ command } ${ args.join( - ' ' - ) } failed with code ${ code }` - ) - ); - } else { - resolve(); - } - } ); - - child.on( 'error', reject ); - } ); -} - -/** - * Main execution function. - */ -async function main() { - console.log( 'šŸ” Checking Gutenberg setup...' ); - - // Verify Gutenberg directory exists - if ( ! fs.existsSync( gutenbergDir ) ) { - console.error( 'āŒ Gutenberg directory not found at:', gutenbergDir ); - console.error( ' Run: node tools/gutenberg/checkout-gutenberg.js' ); - process.exit( 1 ); - } - - // Verify node_modules exists - const nodeModulesPath = path.join( gutenbergDir, 'node_modules' ); - if ( ! fs.existsSync( nodeModulesPath ) ) { - console.error( 'āŒ Gutenberg dependencies not installed' ); - console.error( ' Run: node tools/gutenberg/checkout-gutenberg.js' ); - process.exit( 1 ); - } - - console.log( 'āœ… Gutenberg directory found' ); - - // Modify Gutenberg's package.json for Core build - console.log( '\nāš™ļø Configuring build for WordPress Core...' ); - const gutenbergPackageJsonPath = path.join( gutenbergDir, 'package.json' ); - - try { - const content = fs.readFileSync( gutenbergPackageJsonPath, 'utf8' ); - const gutenbergPackageJson = JSON.parse( content ); - - // Set Core environment variables - gutenbergPackageJson.config = gutenbergPackageJson.config || {}; - gutenbergPackageJson.config.IS_GUTENBERG_PLUGIN = false; - gutenbergPackageJson.config.IS_WORDPRESS_CORE = true; - - // Set wpPlugin.name for Core naming convention - gutenbergPackageJson.wpPlugin = gutenbergPackageJson.wpPlugin || {}; - gutenbergPackageJson.wpPlugin.name = 'wp'; - - fs.writeFileSync( - gutenbergPackageJsonPath, - JSON.stringify( gutenbergPackageJson, null, '\t' ) + '\n' - ); - - console.log( ' āœ… IS_GUTENBERG_PLUGIN = false' ); - console.log( ' āœ… IS_WORDPRESS_CORE = true' ); - console.log( ' āœ… wpPlugin.name = wp' ); - } catch ( error ) { - console.error( - 'āŒ Error modifying Gutenberg package.json:', - error.message - ); - process.exit( 1 ); - } - - // Build Gutenberg - console.log( '\nšŸ”Ø Building Gutenberg for WordPress Core...' ); - console.log( ' (This may take a few minutes)' ); - - const startTime = Date.now(); - - try { - // Invoke the build script directly with node instead of going through - // `npm run build --` to avoid shell argument mangling of the base-url - // value (which contains spaces, parentheses, and single quotes). - // The PATH is extended with node_modules/.bin so that bin commands - // like `wp-build` are found, matching what npm would normally provide. - const binPath = path.join( gutenbergDir, 'node_modules', '.bin' ); - await exec( 'node', [ - 'bin/build.mjs', - '--skip-types', - "--base-url=includes_url( 'build/' )", - ], { - cwd: gutenbergDir, - env: { - ...process.env, - PATH: binPath + path.delimiter + process.env.PATH, - }, - } ); - - const duration = Math.round( ( Date.now() - startTime ) / 1000 ); - console.log( `āœ… Build completed in ${ duration }s` ); - } catch ( error ) { - console.error( 'āŒ Build failed:', error.message ); - 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 ); - } -} - -// Run main function -main().catch( ( error ) => { - console.error( 'āŒ Unexpected error:', error ); - process.exit( 1 ); -} ); diff --git a/tools/gutenberg/checkout-gutenberg.js b/tools/gutenberg/checkout-gutenberg.js index 42e35a1967b78..93b5a1dbd7f89 100644 --- a/tools/gutenberg/checkout-gutenberg.js +++ b/tools/gutenberg/checkout-gutenberg.js @@ -3,14 +3,12 @@ /** * Checkout Gutenberg Repository Script * - * This script checks out the Gutenberg repository at a specific commit/branch/tag - * as specified in the root package.json's "gutenberg" configuration. + * This script downloads a pre-built Gutenberg zip artifact from the GitHub + * Container Registry and extracts it into the ./gutenberg directory. * - * It handles: - * - Initial clone if directory doesn't exist - * - Updating existing checkout to correct ref - * - Installing dependencies with npm ci - * - Idempotent operation (safe to run multiple times) + * The artifact is identified by the "gutenberg.ref" SHA in the root + * package.json, which is used as the OCI image tag for the gutenberg-build + * package on GHCR. * * @package WordPress */ @@ -19,109 +17,46 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const path = require( 'path' ); -// Constants -const GUTENBERG_REPO = 'https://github.com/WordPress/gutenberg.git'; - // Paths const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); const packageJsonPath = path.join( rootDir, 'package.json' ); +// GHCR configuration +const GHCR_REPO = 'desrosj/gutenberg/gutenberg-build'; + /** - * Execute a command and return a promise. - * Captures output and only displays it on failure for cleaner logs. + * Execute a command, streaming stdio directly so progress is visible. * * @param {string} command - Command to execute. * @param {string[]} args - Command arguments. * @param {Object} options - Spawn options. - * @return {Promise} Promise that resolves when command completes. + * @return {Promise} Promise that resolves with stdout when command completes successfully. */ function exec( command, args, options = {} ) { return new Promise( ( resolve, reject ) => { let stdout = ''; - let stderr = ''; const child = spawn( command, args, { cwd: options.cwd || rootDir, - stdio: [ 'ignore', 'pipe', 'pipe' ], - shell: process.platform === 'win32', // Use shell on Windows to find .cmd files + stdio: options.captureOutput ? [ 'ignore', 'pipe', 'inherit' ] : 'inherit', + shell: process.platform === 'win32', ...options, } ); - // Capture output - if ( child.stdout ) { + if ( options.captureOutput && child.stdout ) { child.stdout.on( 'data', ( data ) => { stdout += data.toString(); } ); } - if ( child.stderr ) { - child.stderr.on( 'data', ( data ) => { - stderr += data.toString(); - } ); - } - child.on( 'close', ( code ) => { if ( code !== 0 ) { - // Show output only on failure - if ( stdout ) { - console.error( '\nCommand output:' ); - console.error( stdout ); - } - if ( stderr ) { - console.error( '\nCommand errors:' ); - console.error( stderr ); - } reject( new Error( - `${ command } ${ args.join( - ' ' - ) } failed with code ${ code }` + `${ command } ${ args.join( ' ' ) } failed with code ${ code }` ) ); - } else { - resolve(); - } - } ); - - child.on( 'error', reject ); - } ); -} - -/** - * Execute a command and capture its output. - * - * @param {string} command - Command to execute. - * @param {string[]} args - Command arguments. - * @param {Object} options - Spawn options. - * @return {Promise} Promise that resolves with command output. - */ -function execOutput( command, args, options = {} ) { - return new Promise( ( resolve, reject ) => { - const child = spawn( command, args, { - cwd: options.cwd || rootDir, - shell: process.platform === 'win32', // Use shell on Windows to find .cmd files - ...options, - } ); - - let stdout = ''; - let stderr = ''; - - if ( child.stdout ) { - child.stdout.on( 'data', ( data ) => { - stdout += data.toString(); - } ); - } - - if ( child.stderr ) { - child.stderr.on( 'data', ( data ) => { - stderr += data.toString(); - } ); - } - - child.on( 'close', ( code ) => { - if ( code !== 0 ) { - reject( new Error( `${ command } failed: ${ stderr }` ) ); } else { resolve( stdout.trim() ); } @@ -137,7 +72,7 @@ function execOutput( command, args, options = {} ) { async function main() { console.log( 'šŸ” Checking Gutenberg configuration...' ); - // Read Gutenberg ref from package.json + // Read Gutenberg ref from package.json. let ref; try { const packageJson = JSON.parse( @@ -149,87 +84,94 @@ async function main() { throw new Error( 'Missing "gutenberg.ref" in package.json' ); } - console.log( ` Repository: ${ GUTENBERG_REPO }` ); console.log( ` Reference: ${ ref }` ); } catch ( error ) { console.error( 'āŒ Error reading package.json:', error.message ); process.exit( 1 ); } - // Check if Gutenberg directory exists - const gutenbergExists = fs.existsSync( gutenbergDir ); - - if ( ! gutenbergExists ) { - console.log( '\nšŸ“„ Cloning Gutenberg repository (shallow clone)...' ); - try { - // Generic shallow clone approach that works for both branches and commit hashes - // 1. Clone with no checkout and shallow depth - await exec( 'git', [ - 'clone', - '--depth', - '1', - '--no-checkout', - GUTENBERG_REPO, - 'gutenberg', - ] ); - - // 2. Fetch the specific ref with depth 1 (works for branches, tags, and commits) - await exec( 'git', [ 'fetch', '--depth', '1', 'origin', ref ], { - cwd: gutenbergDir, - } ); + const zipName = `gutenberg-${ ref }.zip`; + const zipPath = path.join( rootDir, zipName ); - // 3. Checkout FETCH_HEAD - await exec( 'git', [ 'checkout', 'FETCH_HEAD' ], { - cwd: gutenbergDir, - } ); - - console.log( 'āœ… Cloned successfully' ); - } catch ( error ) { - console.error( 'āŒ Clone failed:', error.message ); - process.exit( 1 ); + // Step 1: Get an anonymous GHCR token for pulling. + console.log( '\nšŸ”‘ Fetching GHCR token...' ); + let token; + try { + const tokenJson = await exec( 'curl', [ + '--silent', + '--fail', + `https://ghcr.io/token?scope=repository:${ GHCR_REPO }:pull&service=ghcr.io`, + ], { captureOutput: true } ); + token = JSON.parse( tokenJson ).token; + if ( ! token ) { + throw new Error( 'No token in response' ); } - } else { - console.log( '\nāœ… Gutenberg directory already exists' ); + console.log( 'āœ… Token acquired' ); + } catch ( error ) { + console.error( 'āŒ Failed to fetch token:', error.message ); + process.exit( 1 ); } - // Fetch and checkout target ref - console.log( `\nšŸ“” Fetching and checking out: ${ ref }` ); + // Step 2: Get the manifest to find the blob digest. + console.log( `\nšŸ“‹ Fetching manifest for ${ ref }...` ); + let digest; try { - // Fetch the specific ref (works for branches, tags, and commit hashes) - await exec( 'git', [ 'fetch', '--depth', '1', 'origin', ref ], { - cwd: gutenbergDir, - } ); - - // Checkout what was just fetched - await exec( 'git', [ 'checkout', 'FETCH_HEAD' ], { - cwd: gutenbergDir, - } ); - - console.log( 'āœ… Checked out successfully' ); + const manifestJson = await exec( 'curl', [ + '--silent', + '--fail', + '--header', `Authorization: Bearer ${ token }`, + '--header', 'Accept: application/vnd.oci.image.manifest.v1+json', + `https://ghcr.io/v2/${ GHCR_REPO }/manifests/${ ref }`, + ], { captureOutput: true } ); + const manifest = JSON.parse( manifestJson ); + digest = manifest?.layers?.[ 0 ]?.digest; + if ( ! digest ) { + throw new Error( 'No layer digest found in manifest' ); + } + console.log( `āœ… Blob digest: ${ digest }` ); } catch ( error ) { - console.error( 'āŒ Fetch/checkout failed:', error.message ); + console.error( 'āŒ Failed to fetch manifest:', error.message ); process.exit( 1 ); } - // Install dependencies - console.log( '\nšŸ“¦ Installing dependencies...' ); - const nodeModulesExists = fs.existsSync( - path.join( gutenbergDir, 'node_modules' ) - ); + // Step 3: Download the blob (the zip file). + console.log( `\nšŸ“„ Downloading ${ zipName }...` ); + try { + await exec( 'curl', [ + '--fail', + '--location', + '--header', `Authorization: Bearer ${ token }`, + '--output', zipPath, + `https://ghcr.io/v2/${ GHCR_REPO }/blobs/${ digest }`, + ] ); + console.log( 'āœ… Download complete' ); + } catch ( error ) { + console.error( 'āŒ Download failed:', error.message ); + process.exit( 1 ); + } - if ( ! nodeModulesExists ) { - console.log( ' (This may take a few minutes on first run)' ); + // Remove existing gutenberg directory so the unzip is clean. + if ( fs.existsSync( gutenbergDir ) ) { + console.log( '\nšŸ—‘ļø Removing existing gutenberg directory...' ); + fs.rmSync( gutenbergDir, { recursive: true, force: true } ); } + fs.mkdirSync( gutenbergDir, { recursive: true } ); + + // Extract the zip into ./gutenberg. + console.log( `\nšŸ“¦ Extracting ${ zipName } into ./gutenberg...` ); try { - await exec( 'npm', [ 'ci' ], { cwd: gutenbergDir } ); - console.log( 'āœ… Dependencies installed' ); + await exec( 'unzip', [ '-q', zipPath, '-d', gutenbergDir ] ); + console.log( 'āœ… Extraction complete' ); } catch ( error ) { - console.error( 'āŒ npm ci failed:', error.message ); + console.error( 'āŒ Extraction failed:', error.message ); process.exit( 1 ); } - console.log( '\nāœ… Gutenberg checkout complete!' ); + // Clean up the zip file. + fs.rmSync( zipPath ); + + console.log( '\nāœ… Gutenberg download complete!' ); } // Run main function diff --git a/tools/gutenberg/sync-gutenberg.js b/tools/gutenberg/sync-gutenberg.js deleted file mode 100644 index 814188d920cfa..0000000000000 --- a/tools/gutenberg/sync-gutenberg.js +++ /dev/null @@ -1,149 +0,0 @@ -#!/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 ); -} ); From 29a617f692a83a6d2c7944f38fb945010fa1134c Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:50:00 -0500 Subject: [PATCH 02/30] Rename `checkout` related scripts to `download`. --- Gruntfile.js | 8 ++++---- package.json | 9 ++++----- .../{checkout-gutenberg.js => download-gutenberg.js} | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) rename tools/gutenberg/{checkout-gutenberg.js => download-gutenberg.js} (98%) diff --git a/Gruntfile.js b/Gruntfile.js index 2c3b2481b88c7..3b6deb1e7f05e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1475,11 +1475,11 @@ module.exports = function(grunt) { } ); // Gutenberg integration tasks. - grunt.registerTask( 'gutenberg-checkout', 'Checks out the Gutenberg repository.', function() { + grunt.registerTask( 'gutenberg-download', 'Downloads the Gutenberg build artifact.', function() { const done = this.async(); grunt.util.spawn( { cmd: 'node', - args: [ 'tools/gutenberg/checkout-gutenberg.js' ], + args: [ 'tools/gutenberg/download-gutenberg.js' ], opts: { stdio: 'inherit' } }, function( error ) { done( ! error ); @@ -1940,7 +1940,7 @@ module.exports = function(grunt) { 'build:js', 'build:css', 'build:codemirror', - 'gutenberg-sync', + 'gutenberg-download', 'gutenberg-copy', 'copy-vendor-scripts', 'build:certificates' @@ -1952,7 +1952,7 @@ module.exports = function(grunt) { 'build:js', 'build:css', 'build:codemirror', - 'gutenberg-sync', + 'gutenberg-download', 'gutenberg-copy', 'copy-vendor-scripts', 'replace:source-maps', diff --git a/package.json b/package.json index 6e40afdf9f989..606fb52a00db7 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "f4d8a5803aa2fbe26e7d9af4d17e80a622b7bab8" + "ref": "e0c5fc81de25a4f837c063ca2c2db32d74698a49" }, "engines": { "node": ">=20.10.0", @@ -111,7 +111,7 @@ "wicg-inert": "3.1.3" }, "scripts": { - "postinstall": "npm run gutenberg:sync && npm run gutenberg:copy -- --dev", + "postinstall": "npm run gutenberg:download && npm run gutenberg:copy -- --dev", "build": "grunt build", "build:dev": "grunt build --dev", "dev": "grunt watch --dev", @@ -137,11 +137,10 @@ "test:e2e": "wp-scripts test-playwright --config tests/e2e/playwright.config.js", "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js", "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", - "gutenberg:checkout": "node tools/gutenberg/checkout-gutenberg.js", + "gutenberg:download": "node tools/gutenberg/download-gutenberg.js", "gutenberg:copy": "node tools/gutenberg/copy-gutenberg-build.js", - "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" } -} \ No newline at end of file +} diff --git a/tools/gutenberg/checkout-gutenberg.js b/tools/gutenberg/download-gutenberg.js similarity index 98% rename from tools/gutenberg/checkout-gutenberg.js rename to tools/gutenberg/download-gutenberg.js index 93b5a1dbd7f89..49a4fd974b5e8 100644 --- a/tools/gutenberg/checkout-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -6,7 +6,7 @@ * This script downloads a pre-built Gutenberg zip artifact from the GitHub * Container Registry and extracts it into the ./gutenberg directory. * - * The artifact is identified by the "gutenberg.ref" SHA in the root + * The artifact is identified by the "gutenberg.ref" value in the root * package.json, which is used as the OCI image tag for the gutenberg-build * package on GHCR. * From cd08d6073f7fc1b60fb248ca3746001b98544134 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:51:53 -0500 Subject: [PATCH 03/30] Rename `ref` to `sha`. A REF is typically a human-readable path to a branch where as a SHA is a hash representing an individual commit. Since the latter is what's being used here, this aims to avoid confusion. --- package.json | 2 +- tools/gutenberg/download-gutenberg.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 606fb52a00db7..45feb2de48ef3 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "e0c5fc81de25a4f837c063ca2c2db32d74698a49" + "sha": "e0c5fc81de25a4f837c063ca2c2db32d74698a49" }, "engines": { "node": ">=20.10.0", diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index 49a4fd974b5e8..a9ed00e7644ac 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -6,7 +6,7 @@ * This script downloads a pre-built Gutenberg zip artifact from the GitHub * Container Registry and extracts it into the ./gutenberg directory. * - * The artifact is identified by the "gutenberg.ref" value in the root + * The artifact is identified by the "gutenberg.sha" value in the root * package.json, which is used as the OCI image tag for the gutenberg-build * package on GHCR. * @@ -72,25 +72,25 @@ function exec( command, args, options = {} ) { async function main() { console.log( 'šŸ” Checking Gutenberg configuration...' ); - // Read Gutenberg ref from package.json. - let ref; + // Read Gutenberg SHA from package.json. + let sha; try { const packageJson = JSON.parse( fs.readFileSync( packageJsonPath, 'utf8' ) ); - ref = packageJson.gutenberg?.ref; + sha = packageJson.gutenberg?.sha; - if ( ! ref ) { - throw new Error( 'Missing "gutenberg.ref" in package.json' ); + if ( ! sha ) { + throw new Error( 'Missing "gutenberg.sha" in package.json' ); } - console.log( ` Reference: ${ ref }` ); + console.log( ` SHA: ${ sha }` ); } catch ( error ) { console.error( 'āŒ Error reading package.json:', error.message ); process.exit( 1 ); } - const zipName = `gutenberg-${ ref }.zip`; + const zipName = `gutenberg-${ sha }.zip`; const zipPath = path.join( rootDir, zipName ); // Step 1: Get an anonymous GHCR token for pulling. @@ -113,7 +113,7 @@ async function main() { } // Step 2: Get the manifest to find the blob digest. - console.log( `\nšŸ“‹ Fetching manifest for ${ ref }...` ); + console.log( `\nšŸ“‹ Fetching manifest for ${ sha }...` ); let digest; try { const manifestJson = await exec( 'curl', [ @@ -121,7 +121,7 @@ async function main() { '--fail', '--header', `Authorization: Bearer ${ token }`, '--header', 'Accept: application/vnd.oci.image.manifest.v1+json', - `https://ghcr.io/v2/${ GHCR_REPO }/manifests/${ ref }`, + `https://ghcr.io/v2/${ GHCR_REPO }/manifests/${ sha }`, ], { captureOutput: true } ); const manifest = JSON.parse( manifestJson ); digest = manifest?.layers?.[ 0 ]?.digest; From 8729f3700f5ab0fdd0fa853e372dc7c5705a1dad Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:02:51 -0500 Subject: [PATCH 04/30] Move the container registry reference out of code. --- Gruntfile.js | 8 ++++++-- package.json | 3 ++- tools/gutenberg/download-gutenberg.js | 25 +++++++++++++++---------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 3b6deb1e7f05e..874b5ee3a6efa 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1475,11 +1475,15 @@ module.exports = function(grunt) { } ); // Gutenberg integration tasks. - grunt.registerTask( 'gutenberg-download', 'Downloads the Gutenberg build artifact.', function() { + grunt.registerTask( 'gutenberg-download', 'Downloads the built Gutenberg artifact.', function() { const done = this.async(); + const args = [ 'tools/gutenberg/download-gutenberg.js' ]; + if ( grunt.option( 'force' ) ) { + args.push( '--force' ); + } grunt.util.spawn( { cmd: 'node', - args: [ 'tools/gutenberg/download-gutenberg.js' ], + args, opts: { stdio: 'inherit' } }, function( error ) { done( ! error ); diff --git a/package.json b/package.json index 45feb2de48ef3..4f0810782b236 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "e0c5fc81de25a4f837c063ca2c2db32d74698a49" + "sha": "e0c5fc81de25a4f837c063ca2c2db32d74698a49", + "ghcrRepo": "Gdesrosj/gutenberg/gutenberg-build" }, "engines": { "node": ">=20.10.0", diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index a9ed00e7644ac..3a0d158884d54 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -8,7 +8,7 @@ * * The artifact is identified by the "gutenberg.sha" value in the root * package.json, which is used as the OCI image tag for the gutenberg-build - * package on GHCR. + * package on GitHub Container Registry. * * @package WordPress */ @@ -22,9 +22,6 @@ const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); const packageJsonPath = path.join( rootDir, 'package.json' ); -// GHCR configuration -const GHCR_REPO = 'desrosj/gutenberg/gutenberg-build'; - /** * Execute a command, streaming stdio directly so progress is visible. * @@ -68,23 +65,31 @@ function exec( command, args, options = {} ) { /** * Main execution function. + * + * @param {boolean} force - Whether to force a fresh download even if the gutenberg directory exists. */ -async function main() { +async function main( force ) { console.log( 'šŸ” Checking Gutenberg configuration...' ); - // Read Gutenberg SHA from package.json. - let sha; + // Read Gutenberg configuration from package.json. + let sha, ghcrRepo; try { const packageJson = JSON.parse( fs.readFileSync( packageJsonPath, 'utf8' ) ); sha = packageJson.gutenberg?.sha; + ghcrRepo = packageJson.gutenberg?.ghcrRepo; if ( ! sha ) { throw new Error( 'Missing "gutenberg.sha" in package.json' ); } + if ( ! ghcrRepo ) { + throw new Error( 'Missing "gutenberg.ghcrRepo" in package.json' ); + } + console.log( ` SHA: ${ sha }` ); + console.log( ` GHCR repository: ${ ghcrRepo }` ); } catch ( error ) { console.error( 'āŒ Error reading package.json:', error.message ); process.exit( 1 ); @@ -100,7 +105,7 @@ async function main() { const tokenJson = await exec( 'curl', [ '--silent', '--fail', - `https://ghcr.io/token?scope=repository:${ GHCR_REPO }:pull&service=ghcr.io`, + `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io`, ], { captureOutput: true } ); token = JSON.parse( tokenJson ).token; if ( ! token ) { @@ -121,7 +126,7 @@ async function main() { '--fail', '--header', `Authorization: Bearer ${ token }`, '--header', 'Accept: application/vnd.oci.image.manifest.v1+json', - `https://ghcr.io/v2/${ GHCR_REPO }/manifests/${ sha }`, + `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ sha }`, ], { captureOutput: true } ); const manifest = JSON.parse( manifestJson ); digest = manifest?.layers?.[ 0 ]?.digest; @@ -142,7 +147,7 @@ async function main() { '--location', '--header', `Authorization: Bearer ${ token }`, '--output', zipPath, - `https://ghcr.io/v2/${ GHCR_REPO }/blobs/${ digest }`, + `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, ] ); console.log( 'āœ… Download complete' ); } catch ( error ) { From 800fb0626d9d0cdb3b043ef78e8ba1526a2c3131 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:06:39 -0500 Subject: [PATCH 05/30] Don't redownload without `--force` --- tools/gutenberg/download-gutenberg.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index 3a0d158884d54..eb38701e6fdb7 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -95,6 +95,12 @@ async function main( force ) { process.exit( 1 ); } + // Skip download if the gutenberg directory already exists and --force is not set. + if ( ! force && fs.existsSync( gutenbergDir ) ) { + console.log( '\nāœ… The gutenberg directory already exists. Use --force to re-download.' ); + return; + } + const zipName = `gutenberg-${ sha }.zip`; const zipPath = path.join( rootDir, zipName ); @@ -180,7 +186,8 @@ async function main( force ) { } // Run main function -main().catch( ( error ) => { +const force = process.argv.includes( '--force' ); +main( force ).catch( ( error ) => { console.error( 'āŒ Unexpected error:', error ); process.exit( 1 ); } ); From 7dfec4f9e7a31ab004cce19c3ecd30d9b32b90c5 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:14:53 -0500 Subject: [PATCH 06/30] Switch to using the GHCR package from `gutenberg`. The Gutenberg repository has been updated to include the icon assets in the plugin build. See github.com/WordPress/gutenberg/pull/75866. This also updates the hash value. --- package.json | 4 +- tools/gutenberg/download-gutenberg.js | 167 +++++++++++++++----------- 2 files changed, 97 insertions(+), 74 deletions(-) diff --git a/package.json b/package.json index 4f0810782b236..70463389ede88 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "e0c5fc81de25a4f837c063ca2c2db32d74698a49", - "ghcrRepo": "Gdesrosj/gutenberg/gutenberg-build" + "sha": "03b95683ab264e18908bdc4b789bf104d69cb2d3", + "ghcrRepo": "WordPress/gutenberg/gutenberg-build" }, "engines": { "node": ">=20.10.0", diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index eb38701e6fdb7..f3ac989132256 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -96,93 +96,116 @@ async function main( force ) { } // Skip download if the gutenberg directory already exists and --force is not set. + let downloaded = false; if ( ! force && fs.existsSync( gutenbergDir ) ) { - console.log( '\nāœ… The gutenberg directory already exists. Use --force to re-download.' ); - return; - } + console.log( '\nā„¹ļø The `gutenberg` directory already exists. Use `npm run grunt gutenberg-download -- --force` to download a fresh copy.' ); + } else { + downloaded = true; + const zipName = `gutenberg-${ sha }.zip`; + const zipPath = path.join( rootDir, zipName ); + + // Step 1: Get an anonymous GHCR token for pulling. + console.log( '\nšŸ”‘ Fetching GHCR token...' ); + let token; + try { + const tokenJson = await exec( 'curl', [ + '--silent', + '--fail', + `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io`, + ], { captureOutput: true } ); + token = JSON.parse( tokenJson ).token; + if ( ! token ) { + throw new Error( 'No token in response' ); + } + console.log( 'āœ… Token acquired' ); + } catch ( error ) { + console.error( 'āŒ Failed to fetch token:', error.message ); + process.exit( 1 ); + } - const zipName = `gutenberg-${ sha }.zip`; - const zipPath = path.join( rootDir, zipName ); + // Step 2: Get the manifest to find the blob digest. + console.log( `\nšŸ“‹ Fetching manifest for ${ sha }...` ); + let digest; + try { + const manifestJson = await exec( 'curl', [ + '--silent', + '--fail', + '--header', `Authorization: Bearer ${ token }`, + '--header', 'Accept: application/vnd.oci.image.manifest.v1+json', + `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ sha }`, + ], { captureOutput: true } ); + const manifest = JSON.parse( manifestJson ); + digest = manifest?.layers?.[ 0 ]?.digest; + if ( ! digest ) { + throw new Error( 'No layer digest found in manifest' ); + } + console.log( `āœ… Blob digest: ${ digest }` ); + } catch ( error ) { + console.error( 'āŒ Failed to fetch manifest:', error.message ); + process.exit( 1 ); + } - // Step 1: Get an anonymous GHCR token for pulling. - console.log( '\nšŸ”‘ Fetching GHCR token...' ); - let token; - try { - const tokenJson = await exec( 'curl', [ - '--silent', - '--fail', - `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io`, - ], { captureOutput: true } ); - token = JSON.parse( tokenJson ).token; - if ( ! token ) { - throw new Error( 'No token in response' ); + // Step 3: Download the blob (the zip file). + console.log( `\nšŸ“„ Downloading ${ zipName }...` ); + try { + await exec( 'curl', [ + '--fail', + '--location', + '--header', `Authorization: Bearer ${ token }`, + '--output', zipPath, + `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, + ] ); + console.log( 'āœ… Download complete' ); + } catch ( error ) { + console.error( 'āŒ Download failed:', error.message ); + process.exit( 1 ); } - console.log( 'āœ… Token acquired' ); - } catch ( error ) { - console.error( 'āŒ Failed to fetch token:', error.message ); - process.exit( 1 ); - } - // Step 2: Get the manifest to find the blob digest. - console.log( `\nšŸ“‹ Fetching manifest for ${ sha }...` ); - let digest; - try { - const manifestJson = await exec( 'curl', [ - '--silent', - '--fail', - '--header', `Authorization: Bearer ${ token }`, - '--header', 'Accept: application/vnd.oci.image.manifest.v1+json', - `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ sha }`, - ], { captureOutput: true } ); - const manifest = JSON.parse( manifestJson ); - digest = manifest?.layers?.[ 0 ]?.digest; - if ( ! digest ) { - throw new Error( 'No layer digest found in manifest' ); + // Remove existing gutenberg directory so the unzip is clean. + if ( fs.existsSync( gutenbergDir ) ) { + console.log( '\nšŸ—‘ļø Removing existing gutenberg directory...' ); + fs.rmSync( gutenbergDir, { recursive: true, force: true } ); } - console.log( `āœ… Blob digest: ${ digest }` ); - } catch ( error ) { - console.error( 'āŒ Failed to fetch manifest:', error.message ); - process.exit( 1 ); - } - // Step 3: Download the blob (the zip file). - console.log( `\nšŸ“„ Downloading ${ zipName }...` ); - try { - await exec( 'curl', [ - '--fail', - '--location', - '--header', `Authorization: Bearer ${ token }`, - '--output', zipPath, - `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, - ] ); - console.log( 'āœ… Download complete' ); - } catch ( error ) { - console.error( 'āŒ Download failed:', error.message ); - process.exit( 1 ); - } + fs.mkdirSync( gutenbergDir, { recursive: true } ); - // Remove existing gutenberg directory so the unzip is clean. - if ( fs.existsSync( gutenbergDir ) ) { - console.log( '\nšŸ—‘ļø Removing existing gutenberg directory...' ); - fs.rmSync( gutenbergDir, { recursive: true, force: true } ); - } + // Extract the zip into ./gutenberg. + console.log( `\nšŸ“¦ Extracting ${ zipName } into ./gutenberg...` ); + try { + await exec( 'unzip', [ '-q', zipPath, '-d', gutenbergDir ] ); + console.log( 'āœ… Extraction complete' ); + } catch ( error ) { + console.error( 'āŒ Extraction failed:', error.message ); + process.exit( 1 ); + } - fs.mkdirSync( gutenbergDir, { recursive: true } ); + // Clean up the zip file. + fs.rmSync( zipPath ); + } - // Extract the zip into ./gutenberg. - console.log( `\nšŸ“¦ Extracting ${ zipName } into ./gutenberg...` ); + // Verify the downloaded version matches the expected SHA. + console.log( '\nšŸ” Verifying Gutenberg version...' ); + const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); try { - await exec( 'unzip', [ '-q', zipPath, '-d', gutenbergDir ] ); - console.log( 'āœ… Extraction complete' ); + const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); + if ( installedHash !== sha ) { + throw new Error( + `SHA mismatch: expected ${ sha } but found ${ installedHash }. Run \`npm run grunt gutenberg-download -- --force\` to download the correct version.` + ); + } + console.log( 'āœ… Version verified' ); } catch ( error ) { - console.error( 'āŒ Extraction failed:', error.message ); + if ( error.code === 'ENOENT' ) { + console.error( `āŒ ${ hashFilePath } not found. The downloaded artifact may be malformed.` ); + } else { + console.error( `āŒ ${ error.message }` ); + } process.exit( 1 ); } - // Clean up the zip file. - fs.rmSync( zipPath ); - - console.log( '\nāœ… Gutenberg download complete!' ); + if ( downloaded ) { + console.log( '\nāœ… Gutenberg download complete!' ); + } } // Run main function From c7563fac63034bbadbeb9b4ae8eb084d191f7659 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:24:13 -0500 Subject: [PATCH 07/30] Apply suggestions from code review. - Add missing hard stop at the end of inline comment. - Make use if `fetch()` instead of `exec( 'curl' )`. Co-authored-by: Weston Ruter --- tools/gutenberg/download-gutenberg.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index f3ac989132256..356a568693f8a 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -108,12 +108,12 @@ async function main( force ) { console.log( '\nšŸ”‘ Fetching GHCR token...' ); let token; try { - const tokenJson = await exec( 'curl', [ - '--silent', - '--fail', - `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io`, - ], { captureOutput: true } ); - token = JSON.parse( tokenJson ).token; + const response = await fetch( `https://ghcr.io/token?scope=repository:${ghcrRepo}:pull&service=ghcr.io` ); + if ( ! response.ok ) { + throw new Error( `Failed to fetch token: ${response.status} ${response.statusText}` ); + } + const data = await response.json(); + token = data.token; if ( ! token ) { throw new Error( 'No token in response' ); } @@ -208,7 +208,7 @@ async function main( force ) { } } -// Run main function +// Run main function. const force = process.argv.includes( '--force' ); main( force ).catch( ( error ) => { console.error( 'āŒ Unexpected error:', error ); From 9cfff8e469b34f7adcb5532bbdd5c877487d5cdf Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:52:34 -0500 Subject: [PATCH 08/30] Several bug fixes and reverting a few changes. - Replace remaining instances of `exec( 'curl' )`. - Correctly include block PHP files. - Update upstream package details. - Update pinned hash. --- package.json | 4 +-- tools/gutenberg/copy-gutenberg-build.js | 16 +++++------ tools/gutenberg/download-gutenberg.js | 35 ++++++++++++++----------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 70463389ede88..627922c0fc816 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "03b95683ab264e18908bdc4b789bf104d69cb2d3", - "ghcrRepo": "WordPress/gutenberg/gutenberg-build" + "sha": "ee87528714ebbb9b673b8ced17d5d84c4209eca0", + "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { "node": ">=20.10.0", diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index a0117785abad7..1471144ef00b3 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -18,7 +18,6 @@ const glob = require( 'glob' ); const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); const gutenbergBuildDir = path.join( gutenbergDir, 'build' ); -const gutenbergPackagesDir = path.join( gutenbergDir, 'packages' ); // Determine build target from command line argument (--dev or --build-dir) // Default to 'src' for development @@ -80,14 +79,14 @@ const COPY_CONFIG = { name: 'block-library', scripts: 'scripts/block-library', styles: 'styles/block-library', - php: 'block-library/src', + php: 'scripts/block-library', }, { // Widget blocks name: 'widgets', scripts: 'scripts/widgets/blocks', styles: 'styles/widgets', - php: 'widgets/src/blocks', + php: 'scripts/widgets/blocks', }, ], }, @@ -222,7 +221,7 @@ function copyBlockAssets( config ) { for ( const source of config.sources ) { const scriptsSrc = path.join( gutenbergBuildDir, source.scripts ); const stylesSrc = path.join( gutenbergBuildDir, source.styles ); - const phpSrc = path.join( gutenbergPackagesDir, source.php ); + const phpSrc = source.php; if ( ! fs.existsSync( scriptsSrc ) ) { continue; @@ -256,7 +255,7 @@ function copyBlockAssets( config ) { blockDest, { recursive: true, - // Skip PHP, copied from packages + // Skip PHP, copied from build in steps 3 & 4 filter: f => ! f.endsWith( '.php' ), } ); @@ -276,7 +275,7 @@ function copyBlockAssets( config ) { } } - // 3. Copy PHP from packages + // 3. Copy PHP from build const blockPhpSrc = path.join( phpSrc, blockName, 'index.php' ); if ( fs.existsSync( blockPhpSrc ) ) { const phpDest = path.join( @@ -284,11 +283,10 @@ function copyBlockAssets( config ) { config.destination, `${ blockName }.php` ); - const content = fs.readFileSync( blockPhpSrc, 'utf8' ); - fs.writeFileSync( phpDest, content ); + fs.copyFileSync( blockPhpSrc, phpDest ); } - // 4. Copy PHP subdirectories from packages (e.g., shared/helpers.php) + // 4. Copy PHP subdirectories from build (e.g., navigation-link/shared/*.php) const blockPhpDir = path.join( phpSrc, blockName ); if ( fs.existsSync( blockPhpDir ) ) { const rootIndex = path.join( blockPhpDir, 'index.php' ); diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index 356a568693f8a..559e880678ae3 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -127,14 +127,16 @@ async function main( force ) { console.log( `\nšŸ“‹ Fetching manifest for ${ sha }...` ); let digest; try { - const manifestJson = await exec( 'curl', [ - '--silent', - '--fail', - '--header', `Authorization: Bearer ${ token }`, - '--header', 'Accept: application/vnd.oci.image.manifest.v1+json', - `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ sha }`, - ], { captureOutput: true } ); - const manifest = JSON.parse( manifestJson ); + const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ sha }`, { + headers: { + Authorization: `Bearer ${ token }`, + Accept: 'application/vnd.oci.image.manifest.v1+json', + }, + } ); + if ( ! response.ok ) { + throw new Error( `Failed to fetch manifest: ${ response.status } ${ response.statusText }` ); + } + const manifest = await response.json(); digest = manifest?.layers?.[ 0 ]?.digest; if ( ! digest ) { throw new Error( 'No layer digest found in manifest' ); @@ -148,13 +150,16 @@ async function main( force ) { // Step 3: Download the blob (the zip file). console.log( `\nšŸ“„ Downloading ${ zipName }...` ); try { - await exec( 'curl', [ - '--fail', - '--location', - '--header', `Authorization: Bearer ${ token }`, - '--output', zipPath, - `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, - ] ); + const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, { + headers: { + Authorization: `Bearer ${ token }`, + }, + } ); + if ( ! response.ok ) { + throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` ); + } + const buffer = await response.arrayBuffer(); + fs.writeFileSync( zipPath, Buffer.from( buffer ) ); console.log( 'āœ… Download complete' ); } catch ( error ) { console.error( 'āŒ Download failed:', error.message ); From 732a54be12bdc57a1409422b3478d604e749819d Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:37:11 -0500 Subject: [PATCH 09/30] Correct a few paths and destinations. --- tools/gutenberg/copy-gutenberg-build.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index 1471144ef00b3..0a7a4dd9ed94a 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -276,18 +276,19 @@ function copyBlockAssets( config ) { } // 3. Copy PHP from build - const blockPhpSrc = path.join( phpSrc, blockName, 'index.php' ); + const blockPhpSrc = path.join( phpSrc, `${ blockName }.php` ); if ( fs.existsSync( blockPhpSrc ) ) { const phpDest = path.join( wpIncludesDir, config.destination, + blockName, `${ blockName }.php` ); fs.copyFileSync( blockPhpSrc, phpDest ); } // 4. Copy PHP subdirectories from build (e.g., navigation-link/shared/*.php) - const blockPhpDir = path.join( phpSrc, blockName ); + const blockPhpDir = path.join( gutenbergBuildDir, phpSrc, blockName ); if ( fs.existsSync( blockPhpDir ) ) { const rootIndex = path.join( blockPhpDir, 'index.php' ); fs.cpSync( blockPhpDir, blockDest, { From 7f932eca881d0ce93039b4106355981216df769c Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:15:47 -0500 Subject: [PATCH 10/30] A few bug fixes. Also, update inline comment within `base.neon` --- Gruntfile.js | 1 - tests/phpstan/base.neon | 2 +- tools/gutenberg/copy-gutenberg-build.js | 15 +++++++-------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 874b5ee3a6efa..bcef597d1cae7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -53,7 +53,6 @@ module.exports = function(grunt) { webpackFiles = [ 'wp-includes/assets/*', 'wp-includes/css/dist', - 'wp-includes/blocks/**/*.css', '!wp-includes/assets/script-loader-packages.min.php', '!wp-includes/assets/script-modules-packages.min.php', ], diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon index 347c2198ae953..fa4663b5e9aca 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -105,7 +105,7 @@ parameters: - ../../src/wp-includes/deprecated.php - ../../src/wp-includes/ms-deprecated.php - ../../src/wp-includes/pluggable-deprecated.php - # These files are sourced by wordpress/gutenberg in `tools/release/sync-stable-blocks.js`. + # These files are autogenerated by tools/gutenberg/copy-gutenberg-build.js. - ../../src/wp-includes/blocks # Third-party libraries. - ../../src/wp-admin/includes/class-ftp-pure.php diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index 0a7a4dd9ed94a..e4989aa843d2e 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -221,7 +221,7 @@ function copyBlockAssets( config ) { for ( const source of config.sources ) { const scriptsSrc = path.join( gutenbergBuildDir, source.scripts ); const stylesSrc = path.join( gutenbergBuildDir, source.styles ); - const phpSrc = source.php; + const phpSrc = path.join( gutenbergBuildDir, source.php ); if ( ! fs.existsSync( scriptsSrc ) ) { continue; @@ -277,18 +277,17 @@ function copyBlockAssets( config ) { // 3. Copy PHP from build const blockPhpSrc = path.join( phpSrc, `${ blockName }.php` ); + const phpDest = path.join( + wpIncludesDir, + config.destination, + `${ blockName }.php` + ); if ( fs.existsSync( blockPhpSrc ) ) { - const phpDest = path.join( - wpIncludesDir, - config.destination, - blockName, - `${ blockName }.php` - ); fs.copyFileSync( blockPhpSrc, phpDest ); } // 4. Copy PHP subdirectories from build (e.g., navigation-link/shared/*.php) - const blockPhpDir = path.join( gutenbergBuildDir, phpSrc, blockName ); + const blockPhpDir = path.join( phpSrc, blockName ); if ( fs.existsSync( blockPhpDir ) ) { const rootIndex = path.join( blockPhpDir, 'index.php' ); fs.cpSync( blockPhpDir, blockDest, { From 4faa4863a9f275bddefe3bd21bd607945ca8b2cd Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:10:43 -0500 Subject: [PATCH 11/30] Update Gutenberg hash. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 627922c0fc816..4e01dfd1762a0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "ee87528714ebbb9b673b8ced17d5d84c4209eca0", + "sha": "cd4cf58db37e9f774f321df14138dfef5d7e475a", "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { From 82ae215ef6eefd5829906e0efabbcac51283ba34 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:49:04 -0500 Subject: [PATCH 12/30] Address some review feedback. --- tools/gutenberg/download-gutenberg.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index 559e880678ae3..d594e730dddd0 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -15,15 +15,17 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); +const { pipeline } = require( 'stream/promises' ); const path = require( 'path' ); // Paths const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); -const packageJsonPath = path.join( rootDir, 'package.json' ); /** - * Execute a command, streaming stdio directly so progress is visible. + * Execute a command. By default, stdio is inherited so progress is visible in + * the terminal. When `options.captureOutput` is true, stdout is piped and the + * promise resolves with the captured stdout once the process exits. * * @param {string} command - Command to execute. * @param {string[]} args - Command arguments. @@ -74,9 +76,7 @@ async function main( force ) { // Read Gutenberg configuration from package.json. let sha, ghcrRepo; try { - const packageJson = JSON.parse( - fs.readFileSync( packageJsonPath, 'utf8' ) - ); + const packageJson = require( path.join( rootDir, 'package.json' ) ); sha = packageJson.gutenberg?.sha; ghcrRepo = packageJson.gutenberg?.ghcrRepo; @@ -158,8 +158,7 @@ async function main( force ) { if ( ! response.ok ) { throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` ); } - const buffer = await response.arrayBuffer(); - fs.writeFileSync( zipPath, Buffer.from( buffer ) ); + await pipeline( response.body, fs.createWriteStream( zipPath ) ); console.log( 'āœ… Download complete' ); } catch ( error ) { console.error( 'āŒ Download failed:', error.message ); From d06292c0e032b55434056d9009dabf8b7a92fcb3 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:11:45 -0500 Subject: [PATCH 13/30] Extract the verification logic out of download. --- Gruntfile.js | 15 ++++- tools/gutenberg/download-gutenberg.js | 46 ++++----------- tools/gutenberg/gutenberg-utils.js | 82 +++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 38 deletions(-) create mode 100644 tools/gutenberg/gutenberg-utils.js diff --git a/Gruntfile.js b/Gruntfile.js index bcef597d1cae7..ec3cd9b051d0e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1474,6 +1474,17 @@ module.exports = function(grunt) { } ); // Gutenberg integration tasks. + grunt.registerTask( 'gutenberg:verify', 'Verifies the installed Gutenberg version matches the expected SHA.', function() { + const done = this.async(); + grunt.util.spawn( { + cmd: 'node', + args: [ 'tools/gutenberg/gutenberg-utils.js' ], + opts: { stdio: 'inherit' } + }, function( error ) { + done( ! error ); + } ); + } ); + grunt.registerTask( 'gutenberg-download', 'Downloads the built Gutenberg artifact.', function() { const done = this.async(); const args = [ 'tools/gutenberg/download-gutenberg.js' ]; @@ -1940,22 +1951,22 @@ module.exports = function(grunt) { grunt.registerTask( 'build', function() { if ( grunt.option( 'dev' ) ) { grunt.task.run( [ + 'gutenberg:verify', 'build:js', 'build:css', 'build:codemirror', - 'gutenberg-download', 'gutenberg-copy', 'copy-vendor-scripts', 'build:certificates' ] ); } else { grunt.task.run( [ + 'gutenberg:verify', 'build:certificates', 'build:files', 'build:js', 'build:css', 'build:codemirror', - 'gutenberg-download', 'gutenberg-copy', 'copy-vendor-scripts', 'replace:source-maps', diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index d594e730dddd0..360c39fbd237e 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -1,7 +1,7 @@ #!/usr/bin/env node /** - * Checkout Gutenberg Repository Script + * Download Gutenberg Repository Script. * * This script downloads a pre-built Gutenberg zip artifact from the GitHub * Container Registry and extracts it into the ./gutenberg directory. @@ -17,10 +17,7 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const { pipeline } = require( 'stream/promises' ); const path = require( 'path' ); - -// Paths -const rootDir = path.resolve( __dirname, '../..' ); -const gutenbergDir = path.join( rootDir, 'gutenberg' ); +const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion } = require( './gutenberg-utils' ); /** * Execute a command. By default, stdio is inherited so progress is visible in @@ -73,21 +70,15 @@ function exec( command, args, options = {} ) { async function main( force ) { console.log( 'šŸ” Checking Gutenberg configuration...' ); - // Read Gutenberg configuration from package.json. + /* + * Read Gutenberg configuration from package.json. + * + * Note: ghcr stands for GitHub Container Registry where wordpress-develop ready builds of the Gutenberg plugin + * are published on every repository push event. + */ let sha, ghcrRepo; try { - const packageJson = require( path.join( rootDir, 'package.json' ) ); - sha = packageJson.gutenberg?.sha; - ghcrRepo = packageJson.gutenberg?.ghcrRepo; - - if ( ! sha ) { - throw new Error( 'Missing "gutenberg.sha" in package.json' ); - } - - if ( ! ghcrRepo ) { - throw new Error( 'Missing "gutenberg.ghcrRepo" in package.json' ); - } - + ( { sha, ghcrRepo } = readGutenbergConfig() ); console.log( ` SHA: ${ sha }` ); console.log( ` GHCR repository: ${ ghcrRepo }` ); } catch ( error ) { @@ -188,24 +179,7 @@ async function main( force ) { } // Verify the downloaded version matches the expected SHA. - console.log( '\nšŸ” Verifying Gutenberg version...' ); - const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); - try { - const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); - if ( installedHash !== sha ) { - throw new Error( - `SHA mismatch: expected ${ sha } but found ${ installedHash }. Run \`npm run grunt gutenberg-download -- --force\` to download the correct version.` - ); - } - console.log( 'āœ… Version verified' ); - } catch ( error ) { - if ( error.code === 'ENOENT' ) { - console.error( `āŒ ${ hashFilePath } not found. The downloaded artifact may be malformed.` ); - } else { - console.error( `āŒ ${ error.message }` ); - } - process.exit( 1 ); - } + verifyGutenbergVersion(); if ( downloaded ) { console.log( '\nāœ… Gutenberg download complete!' ); diff --git a/tools/gutenberg/gutenberg-utils.js b/tools/gutenberg/gutenberg-utils.js new file mode 100644 index 0000000000000..7040eb02e2dc3 --- /dev/null +++ b/tools/gutenberg/gutenberg-utils.js @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +/** + * Gutenberg build utilities. + * + * Shared helpers used by the Gutenberg download script. When run directly, + * verifies that the installed Gutenberg build matches the SHA in package.json. + * + * @package WordPress + */ + +const fs = require( 'fs' ); +const path = require( 'path' ); + +// Paths +const rootDir = path.resolve( __dirname, '../..' ); +const gutenbergDir = path.join( rootDir, 'gutenberg' ); + +/** + * Read Gutenberg configuration from package.json. + * + * @return {{ sha: string, ghcrRepo: string }} The Gutenberg configuration. + * @throws {Error} If the configuration is missing or invalid. + */ +function readGutenbergConfig() { + const packageJson = require( path.join( rootDir, 'package.json' ) ); + const sha = packageJson.gutenberg?.sha; + const ghcrRepo = packageJson.gutenberg?.ghcrRepo; + + if ( ! sha ) { + throw new Error( 'Missing "gutenberg.sha" in package.json' ); + } + + if ( ! ghcrRepo ) { + throw new Error( 'Missing "gutenberg.ghcrRepo" in package.json' ); + } + + return { sha, ghcrRepo }; +} + +/** + * Verify that the installed Gutenberg version matches the expected SHA in + * package.json. Logs progress to the console and exits with a non-zero code + * on failure. + */ +function verifyGutenbergVersion() { + console.log( '\nšŸ” Verifying Gutenberg version...' ); + + let sha; + try { + ( { sha } = readGutenbergConfig() ); + } catch ( error ) { + console.error( 'āŒ Error reading package.json:', error.message ); + process.exit( 1 ); + } + + const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); + try { + const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); + if ( installedHash !== sha ) { + console.error( + `āŒ SHA mismatch: expected ${ sha } but found ${ installedHash }. Run \`npm run grunt gutenberg-download -- --force\` to download the correct version.` + ); + process.exit( 1 ); + } + } catch ( error ) { + if ( error.code === 'ENOENT' ) { + console.error( `āŒ .gutenberg-hash not found. Run \`npm run grunt gutenberg-download\` to download Gutenberg.` ); + } else { + console.error( `āŒ ${ error.message }` ); + } + process.exit( 1 ); + } + + console.log( 'āœ… Version verified' ); +} + +module.exports = { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion }; + +if ( require.main === module ) { + verifyGutenbergVersion(); +} From 83e549a0b5b3d9becf38764e08f5c7e350c7bb30 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:36:53 -0500 Subject: [PATCH 14/30] Change hyphenated grunt tasks to use colons. --- Gruntfile.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index ec3cd9b051d0e..f1da097d2ad3b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1485,7 +1485,7 @@ module.exports = function(grunt) { } ); } ); - grunt.registerTask( 'gutenberg-download', 'Downloads the built Gutenberg artifact.', function() { + grunt.registerTask( 'gutenberg:download', 'Downloads the built Gutenberg artifact.', function() { const done = this.async(); const args = [ 'tools/gutenberg/download-gutenberg.js' ]; if ( grunt.option( 'force' ) ) { @@ -1500,7 +1500,7 @@ module.exports = function(grunt) { } ); } ); - grunt.registerTask( 'gutenberg-copy', 'Copies Gutenberg build output to WordPress Core.', function() { + grunt.registerTask( 'gutenberg:copy', 'Copies Gutenberg build output to WordPress Core.', function() { const done = this.async(); const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; grunt.util.spawn( { @@ -1955,7 +1955,7 @@ module.exports = function(grunt) { 'build:js', 'build:css', 'build:codemirror', - 'gutenberg-copy', + 'gutenberg:copy', 'copy-vendor-scripts', 'build:certificates' ] ); @@ -1967,7 +1967,7 @@ module.exports = function(grunt) { 'build:js', 'build:css', 'build:codemirror', - 'gutenberg-copy', + 'gutenberg:copy', 'copy-vendor-scripts', 'replace:source-maps', 'verify:build' From 16722a4f81dd1caceff6c6556247c0931e1b5372 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:34:39 -0500 Subject: [PATCH 15/30] Remove `gutenberg` from `tools/gutenberg` names. It's redundant to include `gutenberg` in the file names. --- Gruntfile.js | 6 +++--- package.json | 4 ++-- tests/phpstan/base.neon | 2 +- tools/gutenberg/{copy-gutenberg-build.js => copy.js} | 4 ++-- tools/gutenberg/{download-gutenberg.js => download.js} | 2 +- tools/gutenberg/{gutenberg-utils.js => utils.js} | 0 webpack.config.js | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) rename tools/gutenberg/{copy-gutenberg-build.js => copy.js} (99%) rename tools/gutenberg/{download-gutenberg.js => download.js} (99%) rename tools/gutenberg/{gutenberg-utils.js => utils.js} (100%) diff --git a/Gruntfile.js b/Gruntfile.js index f1da097d2ad3b..b2d64ea9b7216 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1478,7 +1478,7 @@ module.exports = function(grunt) { const done = this.async(); grunt.util.spawn( { cmd: 'node', - args: [ 'tools/gutenberg/gutenberg-utils.js' ], + args: [ 'tools/gutenberg/utils.js' ], opts: { stdio: 'inherit' } }, function( error ) { done( ! error ); @@ -1487,7 +1487,7 @@ module.exports = function(grunt) { grunt.registerTask( 'gutenberg:download', 'Downloads the built Gutenberg artifact.', function() { const done = this.async(); - const args = [ 'tools/gutenberg/download-gutenberg.js' ]; + const args = [ 'tools/gutenberg/download.js' ]; if ( grunt.option( 'force' ) ) { args.push( '--force' ); } @@ -1505,7 +1505,7 @@ module.exports = function(grunt) { const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; grunt.util.spawn( { cmd: 'node', - args: [ 'tools/gutenberg/copy-gutenberg-build.js', `--build-dir=${ buildDir }` ], + args: [ 'tools/gutenberg/copy.js', `--build-dir=${ buildDir }` ], opts: { stdio: 'inherit' } }, function( error ) { done( ! error ); diff --git a/package.json b/package.json index 4e01dfd1762a0..d5a686530be8c 100644 --- a/package.json +++ b/package.json @@ -138,8 +138,8 @@ "test:e2e": "wp-scripts test-playwright --config tests/e2e/playwright.config.js", "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js", "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", - "gutenberg:download": "node tools/gutenberg/download-gutenberg.js", - "gutenberg:copy": "node tools/gutenberg/copy-gutenberg-build.js", + "gutenberg:copy": "node tools/gutenberg/copy.js", + "gutenberg:download": "node tools/gutenberg/download.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/tests/phpstan/base.neon b/tests/phpstan/base.neon index fa4663b5e9aca..318b9969a928d 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -105,7 +105,7 @@ parameters: - ../../src/wp-includes/deprecated.php - ../../src/wp-includes/ms-deprecated.php - ../../src/wp-includes/pluggable-deprecated.php - # These files are autogenerated by tools/gutenberg/copy-gutenberg-build.js. + # These files are autogenerated by tools/gutenberg/copy.js. - ../../src/wp-includes/blocks # Third-party libraries. - ../../src/wp-admin/includes/class-ftp-pure.php diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy.js similarity index 99% rename from tools/gutenberg/copy-gutenberg-build.js rename to tools/gutenberg/copy.js index e4989aa843d2e..fc8fc75fc71a7 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy.js @@ -570,7 +570,7 @@ function generateBlockRegistrationFiles() { // Generate require-dynamic-blocks.php const dynamicContent = ` `\t'${ name }',` ).join( '\n' ) } diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download.js similarity index 99% rename from tools/gutenberg/download-gutenberg.js rename to tools/gutenberg/download.js index 360c39fbd237e..10301ab3422a6 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download.js @@ -17,7 +17,7 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const { pipeline } = require( 'stream/promises' ); const path = require( 'path' ); -const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion } = require( './gutenberg-utils' ); +const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion } = require( './utils' ); /** * Execute a command. By default, stdio is inherited so progress is visible in diff --git a/tools/gutenberg/gutenberg-utils.js b/tools/gutenberg/utils.js similarity index 100% rename from tools/gutenberg/gutenberg-utils.js rename to tools/gutenberg/utils.js diff --git a/webpack.config.js b/webpack.config.js index 29ebbd696b875..2fbda4cf10165 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,7 +14,7 @@ module.exports = function ( // 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). + // the Gutenberg build (see tools/gutenberg/copy.js). // Note: developmentConfig returns an array of configs, so we spread it. const config = [ mediaConfig( env ), From 07f2e5c15b1943d0439e61aae66b45baacf4f2e5 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:51:48 -0500 Subject: [PATCH 16/30] Fix some JShint warnings. --- tools/gutenberg/copy.js | 8 ++++++-- tools/gutenberg/download.js | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index fc8fc75fc71a7..804e602e45bc6 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -767,8 +767,12 @@ function parsePHPArray( phpArrayContent ) { } else { currentPart += char; if ( ! inString ) { - if ( char === '(' ) depth++; - if ( char === ')' ) depth--; + if ( char === '(' ) { + depth++; + } + if ( char === ')' ) { + depth--; + } } } } diff --git a/tools/gutenberg/download.js b/tools/gutenberg/download.js index 10301ab3422a6..626e13bcf5733 100644 --- a/tools/gutenberg/download.js +++ b/tools/gutenberg/download.js @@ -17,6 +17,7 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const { pipeline } = require( 'stream/promises' ); const path = require( 'path' ); +const zlib = require( 'zlib' ); const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion } = require( './utils' ); /** From 6ec39d8b6c6aa84a97dada16b2bcacd9edd23b6f Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:37:20 -0500 Subject: [PATCH 17/30] Convert to using a `.tar.gz` file. This has better native support in Node. --- package.json | 2 +- tools/gutenberg/download.js | 116 ++++++++++++++---------------------- 2 files changed, 45 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index d5a686530be8c..30b0b84b1b480 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "cd4cf58db37e9f774f321df14138dfef5d7e475a", + "sha": "1ed7da329cc9ffb27cbe5d373ea15db309a135b7", "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { diff --git a/tools/gutenberg/download.js b/tools/gutenberg/download.js index 626e13bcf5733..be81fcadb81b6 100644 --- a/tools/gutenberg/download.js +++ b/tools/gutenberg/download.js @@ -3,7 +3,7 @@ /** * Download Gutenberg Repository Script. * - * This script downloads a pre-built Gutenberg zip artifact from the GitHub + * This script downloads a pre-built Gutenberg tar.gz artifact from the GitHub * Container Registry and extracts it into the ./gutenberg directory. * * The artifact is identified by the "gutenberg.sha" value in the root @@ -15,54 +15,12 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); +const { Writable } = require( 'stream' ); const { pipeline } = require( 'stream/promises' ); const path = require( 'path' ); const zlib = require( 'zlib' ); const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion } = require( './utils' ); -/** - * Execute a command. By default, stdio is inherited so progress is visible in - * the terminal. When `options.captureOutput` is true, stdout is piped and the - * promise resolves with the captured stdout once the process exits. - * - * @param {string} command - Command to execute. - * @param {string[]} args - Command arguments. - * @param {Object} options - Spawn options. - * @return {Promise} Promise that resolves with stdout when command completes successfully. - */ -function exec( command, args, options = {} ) { - return new Promise( ( resolve, reject ) => { - let stdout = ''; - - const child = spawn( command, args, { - cwd: options.cwd || rootDir, - stdio: options.captureOutput ? [ 'ignore', 'pipe', 'inherit' ] : 'inherit', - shell: process.platform === 'win32', - ...options, - } ); - - if ( options.captureOutput && child.stdout ) { - child.stdout.on( 'data', ( data ) => { - stdout += data.toString(); - } ); - } - - child.on( 'close', ( code ) => { - if ( code !== 0 ) { - reject( - new Error( - `${ command } ${ args.join( ' ' ) } failed with code ${ code }` - ) - ); - } else { - resolve( stdout.trim() ); - } - } ); - - child.on( 'error', reject ); - } ); -} - /** * Main execution function. * @@ -90,19 +48,17 @@ async function main( force ) { // Skip download if the gutenberg directory already exists and --force is not set. let downloaded = false; if ( ! force && fs.existsSync( gutenbergDir ) ) { - console.log( '\nā„¹ļø The `gutenberg` directory already exists. Use `npm run grunt gutenberg-download -- --force` to download a fresh copy.' ); + console.log( '\nā„¹ļø The `gutenberg` directory already exists. Use `npm run grunt gutenberg:download -- --force` to download a fresh copy.' ); } else { downloaded = true; - const zipName = `gutenberg-${ sha }.zip`; - const zipPath = path.join( rootDir, zipName ); // Step 1: Get an anonymous GHCR token for pulling. console.log( '\nšŸ”‘ Fetching GHCR token...' ); let token; try { - const response = await fetch( `https://ghcr.io/token?scope=repository:${ghcrRepo}:pull&service=ghcr.io` ); + const response = await fetch( `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io` ); if ( ! response.ok ) { - throw new Error( `Failed to fetch token: ${response.status} ${response.statusText}` ); + throw new Error( `Failed to fetch token: ${ response.status } ${ response.statusText }` ); } const data = await response.json(); token = data.token; @@ -139,8 +95,17 @@ async function main( force ) { process.exit( 1 ); } - // Step 3: Download the blob (the zip file). - console.log( `\nšŸ“„ Downloading ${ zipName }...` ); + // Remove existing gutenberg directory so the extraction is clean. + if ( fs.existsSync( gutenbergDir ) ) { + console.log( '\nšŸ—‘ļø Removing existing gutenberg directory...' ); + fs.rmSync( gutenbergDir, { recursive: true, force: true } ); + } + + fs.mkdirSync( gutenbergDir, { recursive: true } ); + + // Step 3: Stream the blob directly through gunzip into tar, writing + // into ./gutenberg with no temporary file on disk. + console.log( `\nšŸ“„ Downloading and extracting artifact...` ); try { const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, { headers: { @@ -150,33 +115,40 @@ async function main( force ) { if ( ! response.ok ) { throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` ); } - await pipeline( response.body, fs.createWriteStream( zipPath ) ); - console.log( 'āœ… Download complete' ); - } catch ( error ) { - console.error( 'āŒ Download failed:', error.message ); - process.exit( 1 ); - } - // Remove existing gutenberg directory so the unzip is clean. - if ( fs.existsSync( gutenbergDir ) ) { - console.log( '\nšŸ—‘ļø Removing existing gutenberg directory...' ); - fs.rmSync( gutenbergDir, { recursive: true, force: true } ); - } + // Spawn tar to read from stdin and extract into gutenbergDir. + // `tar` is available on macOS, Linux, and Windows 10+. + const tar = spawn( 'tar', [ '-x', '-C', gutenbergDir ], { + stdio: [ 'pipe', 'inherit', 'inherit' ], + } ); - fs.mkdirSync( gutenbergDir, { recursive: true } ); + const tarDone = new Promise( ( resolve, reject ) => { + tar.on( 'close', ( code ) => { + if ( code !== 0 ) { + reject( new Error( `tar exited with code ${ code }` ) ); + } else { + resolve(); + } + } ); + tar.on( 'error', reject ); + } ); - // Extract the zip into ./gutenberg. - console.log( `\nšŸ“¦ Extracting ${ zipName } into ./gutenberg...` ); - try { - await exec( 'unzip', [ '-q', zipPath, '-d', gutenbergDir ] ); - console.log( 'āœ… Extraction complete' ); + // Pipe: fetch body → gunzip → tar stdin. + // Decompressing in Node keeps the pipeline error handling + // consistent and means tar only sees plain tar data on stdin. + await pipeline( + response.body, + zlib.createGunzip(), + Writable.toWeb( tar.stdin ), + ); + + await tarDone; + + console.log( 'āœ… Download and extraction complete' ); } catch ( error ) { - console.error( 'āŒ Extraction failed:', error.message ); + console.error( 'āŒ Download/extraction failed:', error.message ); process.exit( 1 ); } - - // Clean up the zip file. - fs.rmSync( zipPath ); } // Verify the downloaded version matches the expected SHA. From a2c185654bbcb7729a3d4e21d799e5ffe217ea04 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:32:38 -0500 Subject: [PATCH 18/30] Use correct multi-line inline comment format. --- tools/gutenberg/copy.js | 50 ++++++++++++++++++++++++------------- tools/gutenberg/download.js | 20 +++++++++------ 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index 804e602e45bc6..c2ea9f8dfbc9e 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -19,8 +19,10 @@ const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); const gutenbergBuildDir = path.join( gutenbergDir, 'build' ); -// Determine build target from command line argument (--dev or --build-dir) -// Default to 'src' for development +/* + * Determine build target from command line argument (--dev or --build-dir). + * Default to 'src' for development. + */ const args = process.argv.slice( 2 ); const buildDirArg = args.find( ( arg ) => arg.startsWith( '--build-dir=' ) ); const buildTarget = buildDirArg @@ -69,8 +71,10 @@ const COPY_CONFIG = { copyAll: true, }, - // Blocks (to wp-includes/blocks/) - // Unified configuration for all block types + /* + * Blocks (to wp-includes/blocks/). + * Unified configuration for all block types. + */ blocks: { destination: 'blocks', sources: [ @@ -847,10 +851,12 @@ function parsePHPArray( phpArrayContent ) { function transformPHPContent( content ) { let transformed = content; - // Fix boot module asset file path for Core's different directory structure - // FROM: __DIR__ . '/../../modules/boot/index.min.asset.php' - // TO: ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php' - // This is needed because Core copies modules to a different location than the plugin structure + /* + * Fix boot module asset file path for Core's different directory structure. + * FROM: __DIR__ . '/../../modules/boot/index.min.asset.php' + * TO: ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php' + * This is needed because Core copies modules to a different location than the plugin structure. + */ transformed = transformed.replace( /__DIR__\s*\.\s*['"]\/\.\.\/\.\.\/modules\/boot\/index\.min\.asset\.php['"]/g, "ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php'" @@ -866,9 +872,11 @@ function transformPHPContent( content ) { * @return {string} Transformed content. */ function transformManifestPHP( content ) { - // Remove 'gutenberg' text domain from _x() calls - // FROM: _x( '...', 'icon label', 'gutenberg' ) - // TO: _x( '...', 'icon label' ) + /* + * Remove 'gutenberg' text domain from _x() calls. + * FROM: _x( '...', 'icon label', 'gutenberg' ) + * TO: _x( '...', 'icon label' ) + */ const transformedContent = content.replace( /_x\(\s*([^,]+),\s*([^,]+),\s*['"]gutenberg['"]\s*\)/g, '_x( $1, $2 )' @@ -933,9 +941,11 @@ async function main() { const scriptsSrc = path.join( gutenbergBuildDir, scriptsConfig.source ); const scriptsDest = path.join( wpIncludesDir, scriptsConfig.destination ); - // Transform function to remove source map comments from all JS files. - // Only match actual source map comments at the start of a line (possibly - // with whitespace), not occurrences inside string literals. + /* + * Transform function to remove source map comments from all JS files. + * Only match actual source map comments at the start of a line (possibly + * with whitespace), not occurrences inside string literals. + */ const removeSourceMaps = ( content ) => { return content.replace( /^\s*\/\/# sourceMappingURL=.*$/gm, '' ).trimEnd(); }; @@ -953,8 +963,10 @@ async function main() { scriptsConfig.directoryRenames && scriptsConfig.directoryRenames[ entry.name ] ) { - // Copy special directories with rename (vendors/ → vendor/) - // Only copy react-jsx-runtime from vendors (react and react-dom come from Core's node_modules) + /* + * Copy special directories with rename (vendors/ → vendor/). + * Only copy react-jsx-runtime from vendors (react and react-dom come from Core's node_modules). + */ const destName = scriptsConfig.directoryRenames[ entry.name ]; const dest = path.join( scriptsDest, destName ); @@ -992,8 +1004,10 @@ async function main() { ); } } else { - // Flatten package structure: package-name/index.js → package-name.js - // This matches Core's expected file structure + /* + * Flatten package structure: package-name/index.js → package-name.js. + * This matches Core's expected file structure. + */ const packageFiles = fs.readdirSync( src ); for ( const file of packageFiles ) { diff --git a/tools/gutenberg/download.js b/tools/gutenberg/download.js index be81fcadb81b6..4d12024ee3780 100644 --- a/tools/gutenberg/download.js +++ b/tools/gutenberg/download.js @@ -103,8 +103,10 @@ async function main( force ) { fs.mkdirSync( gutenbergDir, { recursive: true } ); - // Step 3: Stream the blob directly through gunzip into tar, writing - // into ./gutenberg with no temporary file on disk. + /* + * Step 3: Stream the blob directly through gunzip into tar, writing + * into ./gutenberg with no temporary file on disk. + */ console.log( `\nšŸ“„ Downloading and extracting artifact...` ); try { const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, { @@ -116,8 +118,10 @@ async function main( force ) { throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` ); } - // Spawn tar to read from stdin and extract into gutenbergDir. - // `tar` is available on macOS, Linux, and Windows 10+. + /* + * Spawn tar to read from stdin and extract into gutenbergDir. + * `tar` is available on macOS, Linux, and Windows 10+. + */ const tar = spawn( 'tar', [ '-x', '-C', gutenbergDir ], { stdio: [ 'pipe', 'inherit', 'inherit' ], } ); @@ -133,9 +137,11 @@ async function main( force ) { tar.on( 'error', reject ); } ); - // Pipe: fetch body → gunzip → tar stdin. - // Decompressing in Node keeps the pipeline error handling - // consistent and means tar only sees plain tar data on stdin. + /* + * Pipe: fetch body → gunzip → tar stdin. + * Decompressing in Node keeps the pipeline error handling + * consistent and means tar only sees plain tar data on stdin. + */ await pipeline( response.body, zlib.createGunzip(), From 50c6285961d7bd847f89d56a6b8fd5c2d627bd50 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:39:53 -0500 Subject: [PATCH 19/30] Some code cleanup. This removes some unused code and variables, updates some comments to be more accurate, and ensures all one line inline comments end with a period. --- tools/gutenberg/copy.js | 161 +++++++++++++++++------------------- tools/gutenberg/download.js | 2 +- tools/gutenberg/utils.js | 6 +- 3 files changed, 81 insertions(+), 88 deletions(-) diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index c2ea9f8dfbc9e..d87f82b6f40ed 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -14,7 +14,7 @@ const path = require( 'path' ); const json2php = require( 'json2php' ); const glob = require( 'glob' ); -// Paths +// Paths. const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); const gutenbergBuildDir = path.join( gutenbergDir, 'build' ); @@ -38,37 +38,34 @@ const wpIncludesDir = path.join( rootDir, buildTarget, 'wp-includes' ); * Defines what to copy from Gutenberg build and where it goes in Core. */ const COPY_CONFIG = { - // PHP infrastructure files (to wp-includes/build/) + // PHP infrastructure files (to wp-includes/build/). phpInfrastructure: { destination: 'build', files: [ 'routes.php', 'pages.php', 'constants.php' ], directories: [ 'pages', 'routes' ], }, - // JavaScript packages (to wp-includes/js/dist/) + // JavaScript packages (to wp-includes/js/dist/). scripts: { source: 'scripts', destination: 'js/dist', - copyDirectories: true, // Copy subdirectories - patterns: [ '*.js' ], - // Rename vendors/ to vendor/ when copying + copyDirectories: true, + // Rename vendors/ to vendor/ when copying. directoryRenames: { vendors: 'vendor', }, }, - // Script modules (to wp-includes/js/dist/script-modules/) + // Script modules (to wp-includes/js/dist/script-modules/). modules: { source: 'modules', destination: 'js/dist/script-modules', - copyAll: true, }, - // Styles (to wp-includes/css/dist/) + // Styles (to wp-includes/css/dist/). styles: { source: 'styles', destination: 'css/dist', - copyAll: true, }, /* @@ -79,14 +76,14 @@ const COPY_CONFIG = { destination: 'blocks', sources: [ { - // Block library blocks + // Block library blocks. name: 'block-library', scripts: 'scripts/block-library', styles: 'styles/block-library', php: 'scripts/block-library', }, { - // Widget blocks + // Widget blocks. name: 'widgets', scripts: 'scripts/widgets/blocks', styles: 'styles/widgets', @@ -95,7 +92,7 @@ const COPY_CONFIG = { ], }, - // Theme JSON files (from Gutenberg lib directory) + // Theme JSON files (from Gutenberg lib directory). themeJson: { files: [ { from: 'theme.json', to: 'theme.json' }, @@ -103,7 +100,7 @@ const COPY_CONFIG = { ], }, - // Specific files to copy to wp-includes/$destination + // Specific files to copy to wp-includes/$destination. wpIncludes: [ { files: [ 'packages/icons/src/manifest.php' ], @@ -160,7 +157,7 @@ function copyDirectory( src, dest, transform = null, options = {} ) { const destPath = path.join( dest, entry.name ); if ( entry.isDirectory() ) { - // Check if this directory is an experimental block + // Check if this directory is an experimental block. if ( options.excludeExperimental ) { const blockJsonPath = path.join( srcPath, 'block.json' ); if ( isExperimentalBlock( blockJsonPath ) ) { @@ -170,13 +167,13 @@ function copyDirectory( src, dest, transform = null, options = {} ) { copyDirectory( srcPath, destPath, transform, options ); } else { - // Skip source map files (.map) — these are not useful in Core + // Skip source map files (.map) — these are not useful in Core, // and the sourceMappingURL references are already stripped from JS files. if ( /\.map$/.test( entry.name ) ) { continue; } - // Skip non-minified VIPS files — they are ~10MB of inlined WASM + // Skip non-minified VIPS files — they are ~10MB of inlined WASM, // with no debugging value over the minified versions. if ( srcPath.includes( '/vips/' ) && @@ -185,14 +182,14 @@ function copyDirectory( src, dest, transform = null, options = {} ) { continue; } - // Skip PHP files if excludePHP is true + // Skip PHP files if excludePHP is true. if ( options.excludePHP && /\.php$/.test( entry.name ) ) { continue; } let content = fs.readFileSync( srcPath ); - // Apply transformation if provided and file is text + // Apply transformation if provided and file is text. if ( transform && /\.(php|js|css)$/.test( entry.name ) ) { try { content = transform( @@ -231,14 +228,14 @@ function copyBlockAssets( config ) { continue; } - // Get all block directories from the scripts source + // Get all block directories from the scripts source. const blockDirs = fs .readdirSync( scriptsSrc, { withFileTypes: true } ) .filter( ( entry ) => entry.isDirectory() ) .map( ( entry ) => entry.name ); for ( const blockName of blockDirs ) { - // Skip experimental blocks + // Skip experimental blocks. const blockJsonPath = path.join( scriptsSrc, blockName, @@ -259,7 +256,7 @@ function copyBlockAssets( config ) { blockDest, { recursive: true, - // Skip PHP, copied from build in steps 3 & 4 + // Skip PHP, copied from build in steps 3 & 4. filter: f => ! f.endsWith( '.php' ), } ); @@ -303,7 +300,7 @@ function copyBlockAssets( config ) { ( entry ) => hasPhpFiles( path.join( src, entry.name ) ) ); } - // Copy PHP files, but skip root index.php (handled by step 3) + // Copy PHP files, but skip root index.php (handled by step 3). return src.endsWith( '.php' ) && src !== rootIndex; }, } ); @@ -346,7 +343,7 @@ function generateScriptModulesPackages() { processDirectory( fullPath, baseDir ); } else if ( entry.name.endsWith( '.min.asset.php' ) ) { const relativePath = path.relative( baseDir, fullPath ); - // Normalize path separators to forward slashes for cross-platform consistency + // Normalize path separators to forward slashes for cross-platform consistency. const normalizedPath = relativePath .split( path.sep ) .join( '/' ); @@ -357,14 +354,14 @@ function generateScriptModulesPackages() { const jsPathRegular = jsPathMin.replace( /\.min\.js$/, '.js' ); try { - // Read and parse the PHP asset file + // Read and parse the PHP asset file. const phpContent = fs.readFileSync( fullPath, 'utf8' ); // Extract the array from PHP: 1 ) { currentArray += 'array('; } - i += 5; // Skip 'array(' + i += 5; // Skip 'array('. continue; } @@ -723,7 +720,7 @@ function parsePHPArray( phpArrayContent ) { } else if ( char === ')' ) { depth--; if ( depth === 0 ) { - // Found complete nested array + // Found complete nested array. const placeholder = `__ARRAY_${ nestedArrays.length }__`; nestedArrays.push( currentArray ); content = @@ -744,12 +741,12 @@ function parsePHPArray( phpArrayContent ) { } } - // Now parse the simplified content + // Now parse the simplified content. const result = {}; const values = []; let isAssociative = false; - // Split by top-level commas + // Split by top-level commas. const parts = []; depth = 0; inString = false; @@ -784,7 +781,7 @@ function parsePHPArray( phpArrayContent ) { parts.push( currentPart.trim() ); } - // Parse each part + // Parse each part. for ( const part of parts ) { const arrowMatch = part.match( /^(.+?)\s*=>\s*(.+)$/ ); @@ -793,7 +790,7 @@ function parsePHPArray( phpArrayContent ) { let key = arrowMatch[ 1 ].trim().replace( /^['"]|['"]$/g, '' ); let value = arrowMatch[ 2 ].trim(); - // Replace placeholders + // Replace placeholders. while ( value.match( /__ARRAY_(\d+)__/ ) ) { value = value.replace( /__ARRAY_(\d+)__/, ( match, index ) => { return 'array(' + nestedArrays[ parseInt( index ) ] + ')'; @@ -802,10 +799,10 @@ function parsePHPArray( phpArrayContent ) { result[ key ] = parseValue( value ); } else { - // No arrow, indexed array + // No arrow, indexed array. let value = part; - // Replace placeholders + // Replace placeholders. while ( value.match( /__ARRAY_(\d+)__/ ) ) { value = value.replace( /__ARRAY_(\d+)__/, ( match, index ) => { return 'array(' + nestedArrays[ parseInt( index ) ] + ')'; @@ -888,24 +885,20 @@ function transformManifestPHP( content ) { * Main execution function. */ async function main() { - console.log( 'šŸ” Checking Gutenberg build...' ); - console.log( ` Build target: ${ buildTarget }/` ); + console.log( `šŸ“¦ Copying Gutenberg build to ${ buildTarget }/...` ); - // Verify Gutenberg build exists if ( ! fs.existsSync( gutenbergBuildDir ) ) { console.error( 'āŒ Gutenberg build directory not found' ); - console.error( ' Run: node tools/gutenberg/build-gutenberg.js' ); + console.error( ' Run: npm run grunt gutenberg:download' ); process.exit( 1 ); } - console.log( 'āœ… Gutenberg build found' ); - - // 1. Copy PHP infrastructure + // 1. Copy PHP infrastructure. console.log( '\nšŸ“¦ Copying PHP infrastructure...' ); const phpConfig = COPY_CONFIG.phpInfrastructure; const phpDest = path.join( wpIncludesDir, phpConfig.destination ); - // Copy PHP files + // Copy PHP files. for ( const file of phpConfig.files ) { const src = path.join( gutenbergBuildDir, file ); const dest = path.join( phpDest, file ); @@ -923,7 +916,7 @@ async function main() { } } - // Copy PHP directories + // Copy PHP directories. for ( const dir of phpConfig.directories ) { const src = path.join( gutenbergBuildDir, dir ); const dest = path.join( phpDest, dir ); @@ -935,7 +928,7 @@ async function main() { } } - // 2. Copy JavaScript packages + // 2. Copy JavaScript packages. console.log( '\nšŸ“¦ Copying JavaScript packages...' ); const scriptsConfig = COPY_CONFIG.scripts; const scriptsSrc = path.join( gutenbergBuildDir, scriptsConfig.source ); @@ -957,7 +950,7 @@ async function main() { const src = path.join( scriptsSrc, entry.name ); if ( entry.isDirectory() ) { - // Check if this should be copied as a directory (like vendors/) + // Check if this should be copied as a directory (like vendors/). if ( scriptsConfig.copyDirectories && scriptsConfig.directoryRenames && @@ -972,7 +965,7 @@ async function main() { const dest = path.join( scriptsDest, destName ); if ( entry.name === 'vendors' ) { - // Only copy react-jsx-runtime files, skip react and react-dom + // Only copy react-jsx-runtime files, skip react and react-dom. const vendorFiles = fs.readdirSync( src ); let copiedCount = 0; fs.mkdirSync( dest, { recursive: true } ); @@ -997,7 +990,7 @@ async function main() { ` āœ… ${ entry.name }/ → ${ destName }/ (react-jsx-runtime only, ${ copiedCount } files)` ); } else { - // Copy other special directories normally + // Copy other special directories normally. copyDirectory( src, dest, removeSourceMaps ); console.log( ` āœ… ${ entry.name }/ → ${ destName }/` @@ -1015,7 +1008,7 @@ async function main() { /^index\.(js|min\.js|min\.asset\.php)$/.test( file ) ) { const srcFile = path.join( src, file ); - // Replace 'index.' with 'package-name.' + // Replace 'index.' with 'package-name.'. const destFile = file.replace( /^index\./, `${ entry.name }.` @@ -1026,7 +1019,7 @@ async function main() { recursive: true, } ); - // Apply source map removal for .js files + // Apply source map removal for .js files. if ( file.endsWith( '.js' ) ) { let content = fs.readFileSync( srcFile, @@ -1035,14 +1028,14 @@ async function main() { content = removeSourceMaps( content ); fs.writeFileSync( destPath, content ); } else { - // Copy other files as-is (.min.asset.php) + // Copy other files as-is (.min.asset.php). fs.copyFileSync( srcFile, destPath ); } } } } } else if ( entry.isFile() && entry.name.endsWith( '.js' ) ) { - // Copy root-level JS files + // Copy root-level JS files. const dest = path.join( scriptsDest, entry.name ); fs.mkdirSync( path.dirname( dest ), { recursive: true } ); @@ -1055,19 +1048,19 @@ async function main() { console.log( ' āœ… JavaScript packages copied' ); } - // 3. Copy script modules + // 3. Copy script modules. console.log( '\nšŸ“¦ Copying script modules...' ); const modulesConfig = COPY_CONFIG.modules; const modulesSrc = path.join( gutenbergBuildDir, modulesConfig.source ); const modulesDest = path.join( wpIncludesDir, modulesConfig.destination ); if ( fs.existsSync( modulesSrc ) ) { - // Use the same source map removal transform + // Use the same source map removal transform. copyDirectory( modulesSrc, modulesDest, removeSourceMaps ); console.log( ' āœ… Script modules copied' ); } - // 4. Copy styles + // 4. Copy styles. console.log( '\nšŸ“¦ Copying styles...' ); const stylesConfig = COPY_CONFIG.styles; const stylesSrc = path.join( gutenbergBuildDir, stylesConfig.source ); @@ -1078,7 +1071,7 @@ async function main() { console.log( ' āœ… Styles copied' ); } - // 5. Copy blocks (unified: scripts, styles, PHP, JSON) + // 5. Copy blocks (unified: scripts, styles, PHP, JSON). console.log( '\nšŸ“¦ Copying blocks...' ); const blocksDest = path.join( wpIncludesDir, @@ -1086,7 +1079,7 @@ async function main() { ); copyBlockAssets( COPY_CONFIG.blocks ); - // 6. Copy theme JSON files (from Gutenberg lib directory) + // 6. Copy theme JSON files (from Gutenberg lib directory). console.log( '\nšŸ“¦ Copying theme JSON files...' ); const themeJsonConfig = COPY_CONFIG.themeJson; const gutenbergLibDir = path.join( gutenbergDir, 'lib' ); @@ -1099,7 +1092,7 @@ async function main() { let content = fs.readFileSync( src, 'utf8' ); if ( themeJsonConfig.transform && fileMap.from === 'theme.json' ) { - // Transform schema URL for Core + // Transform schema URL for Core. content = content.replace( '"$schema": "../schemas/json/theme.json"', '"$schema": "https://schemas.wp.org/trunk/theme.json"' @@ -1113,7 +1106,7 @@ async function main() { } } - // Copy remaining files to wp-includes + // Copy remaining files to wp-includes. console.log( '\nšŸ“¦ Copying remaining files to wp-includes...' ); for ( const fileMap of COPY_CONFIG.wpIncludes ) { const dest = path.join( wpIncludesDir, fileMap.destination ); @@ -1125,7 +1118,7 @@ async function main() { } for ( const match of matches ) { const destPath = path.join( dest, path.basename( match ) ); - // Apply transformation for manifest.php to remove gutenberg text domain + // Apply transformation for manifest.php to remove gutenberg text domain. if ( path.basename( match ) === 'manifest.php' ) { let content = fs.readFileSync( match, 'utf8' ); content = transformManifestPHP( content ); @@ -1137,23 +1130,23 @@ async function main() { } } - // 7. Generate script-modules-packages.min.php from individual asset files - console.log( '\nšŸ“¦ Generating script-modules-packages.min.php...' ); + // 7. Generate script-modules-packages.php from individual asset files. + console.log( '\nšŸ“¦ Generating script-modules-packages.php...' ); generateScriptModulesPackages(); - // 8. Generate script-loader-packages.min.php - console.log( '\nšŸ“¦ Generating script-loader-packages.min.php...' ); + // 8. Generate script-loader-packages.php. + console.log( '\nšŸ“¦ Generating script-loader-packages.php...' ); generateScriptLoaderPackages(); - // 9. Generate require-dynamic-blocks.php and require-static-blocks.php + // 9. Generate require-dynamic-blocks.php and require-static-blocks.php. console.log( '\nšŸ“¦ Generating block registration files...' ); generateBlockRegistrationFiles(); - // 10. Generate blocks-json.php from block.json files + // 10. Generate blocks-json.php from block.json files. console.log( '\nšŸ“¦ Generating blocks-json.php...' ); generateBlocksJson(); - // Summary + // Summary. console.log( '\nāœ… Copy complete!' ); console.log( '\nšŸ“Š Summary:' ); console.log( ` PHP infrastructure: ${ phpDest }` ); @@ -1163,7 +1156,7 @@ async function main() { console.log( ` Blocks: ${ blocksDest }` ); } -// Run main function +// Run main function. main().catch( ( error ) => { console.error( 'āŒ Unexpected error:', error ); process.exit( 1 ); diff --git a/tools/gutenberg/download.js b/tools/gutenberg/download.js index 4d12024ee3780..f936136ffa25c 100644 --- a/tools/gutenberg/download.js +++ b/tools/gutenberg/download.js @@ -19,7 +19,7 @@ const { Writable } = require( 'stream' ); const { pipeline } = require( 'stream/promises' ); const path = require( 'path' ); const zlib = require( 'zlib' ); -const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion } = require( './utils' ); +const { gutenbergDir, readGutenbergConfig, verifyGutenbergVersion } = require( './utils' ); /** * Main execution function. diff --git a/tools/gutenberg/utils.js b/tools/gutenberg/utils.js index 7040eb02e2dc3..4a04210f699d7 100644 --- a/tools/gutenberg/utils.js +++ b/tools/gutenberg/utils.js @@ -12,7 +12,7 @@ const fs = require( 'fs' ); const path = require( 'path' ); -// Paths +// Paths. const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); @@ -59,13 +59,13 @@ function verifyGutenbergVersion() { const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); if ( installedHash !== sha ) { console.error( - `āŒ SHA mismatch: expected ${ sha } but found ${ installedHash }. Run \`npm run grunt gutenberg-download -- --force\` to download the correct version.` + `āŒ SHA mismatch: expected ${ sha } but found ${ installedHash }. Run \`npm run grunt gutenberg:download -- --force\` to download the correct version.` ); process.exit( 1 ); } } catch ( error ) { if ( error.code === 'ENOENT' ) { - console.error( `āŒ .gutenberg-hash not found. Run \`npm run grunt gutenberg-download\` to download Gutenberg.` ); + console.error( `āŒ .gutenberg-hash not found. Run \`npm run grunt gutenberg:download\` to download Gutenberg.` ); } else { console.error( `āŒ ${ error.message }` ); } From b1c38428ce3844ac49be2925f282511e638fbe58 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:12:30 -0500 Subject: [PATCH 20/30] Utilize pre-existing Grunt tasks. There are a few pre-existing grunt tasks that are performing identical tasks to some of the code in `copy.js`. Mainly: - `grunt-contrib-copy` copies files with the ability to perform custom transformations. - `grunt-contrib-replace` replaces strings and pattern matches, which is primarily used to remove `sourceMappingURL` from JavaScript files. --- Gruntfile.js | 130 ++++++++++++++++++++++--- package.json | 2 +- tools/gutenberg/copy.js | 211 ++-------------------------------------- 3 files changed, 126 insertions(+), 217 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index b2d64ea9b7216..2d71eae5f1a59 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -587,7 +587,97 @@ module.exports = function(grunt) { certificates: { src: 'vendor/composer/ca-bundle/res/cacert.pem', dest: SOURCE_DIR + 'wp-includes/certificates/ca-bundle.crt' - } + }, + // Gutenberg PHP infrastructure files (routes.php, pages.php, constants.php, pages/, routes/). + 'gutenberg-php': { + options: { + process: function( content ) { + // Fix boot module asset file path for Core's different directory structure. + return content.replace( + /__DIR__\s*\.\s*['"]\/..\/\..\/modules\/boot\/index\.min\.asset\.php['"]/g, + "ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php'" + ); + } + }, + files: [ { + expand: true, + cwd: 'gutenberg/build', + src: [ + 'routes.php', + 'pages.php', + 'constants.php', + 'pages/**/*.php', + 'routes/**/*.php', + ], + dest: WORKING_DIR + 'wp-includes/build/', + } ], + }, + 'gutenberg-modules': { + files: [ { + expand: true, + cwd: 'gutenberg/build/modules', + src: [ '**/*', '!**/*.map' ], + dest: WORKING_DIR + 'wp-includes/js/dist/script-modules/', + } ], + }, + 'gutenberg-styles': { + files: [ { + expand: true, + cwd: 'gutenberg/build/styles', + src: [ '**/*', '!**/*.map' ], + dest: WORKING_DIR + 'wp-includes/css/dist/', + } ], + }, + 'gutenberg-theme-json': { + options: { + process: function( content, srcpath ) { + // Replace the local schema URL with the canonical public URL for Core. + if ( path.basename( srcpath ) === 'theme.json' ) { + return content.replace( + '"$schema": "../schemas/json/theme.json"', + '"$schema": "https://schemas.wp.org/trunk/theme.json"' + ); + } + return content; + } + }, + files: [ + { + src: 'gutenberg/lib/theme.json', + dest: WORKING_DIR + 'wp-includes/theme.json', + }, + { + src: 'gutenberg/lib/theme-i18n.json', + dest: WORKING_DIR + 'wp-includes/theme-i18n.json', + }, + ], + }, + 'gutenberg-icons': { + options: { + process: function( content, srcpath ) { + // Remove the 'gutenberg' text domain from _x() calls in manifest.php. + if ( path.basename( srcpath ) === 'manifest.php' ) { + return content.replace( + /_x\(\s*([^,]+),\s*([^,]+),\s*['"]gutenberg['"]\s*\)/g, + '_x( $1, $2 )' + ); + } + return content; + } + }, + files: [ + { + src: 'gutenberg/packages/icons/src/manifest.php', + dest: WORKING_DIR + 'wp-includes/icons/manifest.php', + }, + { + expand: true, + cwd: 'gutenberg/packages/icons/src/library', + src: '*.svg', + dest: WORKING_DIR + 'wp-includes/icons/library/', + }, + ], + }, }, sass: { colors: { @@ -1322,20 +1412,21 @@ module.exports = function(grunt) { }, { expand: true, - flatten: true, - src: [ - BUILD_DIR + 'wp-includes/js/dist/block-editor.js', - BUILD_DIR + 'wp-includes/js/dist/commands.js', - ], - dest: BUILD_DIR + 'wp-includes/js/dist/' + cwd: BUILD_DIR + 'wp-includes/js/dist/', + src: [ '*.js' ], + dest: BUILD_DIR + 'wp-includes/js/dist/', }, { expand: true, - flatten: true, - src: [ - BUILD_DIR + 'wp-includes/js/dist/vendor/**/*.js' - ], - dest: BUILD_DIR + 'wp-includes/js/dist/vendor/' + cwd: BUILD_DIR + 'wp-includes/js/dist/vendor/', + src: [ '**/*.js' ], + dest: BUILD_DIR + 'wp-includes/js/dist/vendor/', + }, + { + expand: true, + cwd: BUILD_DIR + 'wp-includes/js/dist/script-modules/', + src: [ '**/*.js' ], + dest: BUILD_DIR + 'wp-includes/js/dist/script-modules/', } ] } @@ -1500,7 +1591,7 @@ module.exports = function(grunt) { } ); } ); - grunt.registerTask( 'gutenberg:copy', 'Copies Gutenberg build output to WordPress Core.', function() { + grunt.registerTask( 'gutenberg:copy', 'Copies Gutenberg JS packages and block assets to WordPress Core.', function() { const done = this.async(); const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; grunt.util.spawn( { @@ -1948,6 +2039,15 @@ module.exports = function(grunt) { } ); } ); + grunt.registerTask( 'build:gutenberg', [ + 'copy:gutenberg-php', + 'gutenberg:copy', + 'copy:gutenberg-modules', + 'copy:gutenberg-styles', + 'copy:gutenberg-theme-json', + 'copy:gutenberg-icons', + ] ); + grunt.registerTask( 'build', function() { if ( grunt.option( 'dev' ) ) { grunt.task.run( [ @@ -1955,7 +2055,7 @@ module.exports = function(grunt) { 'build:js', 'build:css', 'build:codemirror', - 'gutenberg:copy', + 'build:gutenberg', 'copy-vendor-scripts', 'build:certificates' ] ); @@ -1967,7 +2067,7 @@ module.exports = function(grunt) { 'build:js', 'build:css', 'build:codemirror', - 'gutenberg:copy', + 'build:gutenberg', 'copy-vendor-scripts', 'replace:source-maps', 'verify:build' diff --git a/package.json b/package.json index 30b0b84b1b480..fe34bc70bf120 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "wicg-inert": "3.1.3" }, "scripts": { - "postinstall": "npm run gutenberg:download && npm run gutenberg:copy -- --dev", + "postinstall": "npm run gutenberg:download && grunt build:gutenberg --dev", "build": "grunt build", "build:dev": "grunt build --dev", "dev": "grunt watch --dev", diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index d87f82b6f40ed..18db83372d87f 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -12,7 +12,6 @@ const fs = require( 'fs' ); const path = require( 'path' ); const json2php = require( 'json2php' ); -const glob = require( 'glob' ); // Paths. const rootDir = path.resolve( __dirname, '../..' ); @@ -839,48 +838,6 @@ function parsePHPArray( phpArrayContent ) { } } -/** - * Transform PHP file contents to work in Core. - * - * @param {string} content - File content. - * @return {string} Transformed content. - */ -function transformPHPContent( content ) { - let transformed = content; - - /* - * Fix boot module asset file path for Core's different directory structure. - * FROM: __DIR__ . '/../../modules/boot/index.min.asset.php' - * TO: ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php' - * This is needed because Core copies modules to a different location than the plugin structure. - */ - transformed = transformed.replace( - /__DIR__\s*\.\s*['"]\/\.\.\/\.\.\/modules\/boot\/index\.min\.asset\.php['"]/g, - "ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php'" - ); - - return transformed; -} - -/** - * Transform manifest.php to remove gutenberg text domain. - * - * @param {string} content - File content. - * @return {string} Transformed content. - */ -function transformManifestPHP( content ) { - /* - * Remove 'gutenberg' text domain from _x() calls. - * FROM: _x( '...', 'icon label', 'gutenberg' ) - * TO: _x( '...', 'icon label' ) - */ - const transformedContent = content.replace( - /_x\(\s*([^,]+),\s*([^,]+),\s*['"]gutenberg['"]\s*\)/g, - '_x( $1, $2 )' - ); - return transformedContent; -} - /** * Main execution function. */ @@ -893,56 +850,12 @@ async function main() { process.exit( 1 ); } - // 1. Copy PHP infrastructure. - console.log( '\nšŸ“¦ Copying PHP infrastructure...' ); - const phpConfig = COPY_CONFIG.phpInfrastructure; - const phpDest = path.join( wpIncludesDir, phpConfig.destination ); - - // Copy PHP files. - for ( const file of phpConfig.files ) { - const src = path.join( gutenbergBuildDir, file ); - const dest = path.join( phpDest, file ); - - if ( fs.existsSync( src ) ) { - fs.mkdirSync( path.dirname( dest ), { recursive: true } ); - let content = fs.readFileSync( src, 'utf8' ); - content = transformPHPContent( content ); - fs.writeFileSync( dest, content ); - console.log( ` āœ… ${ file }` ); - } else { - console.log( - ` āš ļø ${ file } not found (may not exist in this Gutenberg version)` - ); - } - } - - // Copy PHP directories. - for ( const dir of phpConfig.directories ) { - const src = path.join( gutenbergBuildDir, dir ); - const dest = path.join( phpDest, dir ); - - if ( fs.existsSync( src ) ) { - console.log( ` šŸ“ Copying ${ dir }/...` ); - copyDirectory( src, dest, transformPHPContent ); - console.log( ` āœ… ${ dir }/ copied` ); - } - } - - // 2. Copy JavaScript packages. + // 1. Copy JavaScript packages. console.log( '\nšŸ“¦ Copying JavaScript packages...' ); const scriptsConfig = COPY_CONFIG.scripts; const scriptsSrc = path.join( gutenbergBuildDir, scriptsConfig.source ); const scriptsDest = path.join( wpIncludesDir, scriptsConfig.destination ); - /* - * Transform function to remove source map comments from all JS files. - * Only match actual source map comments at the start of a line (possibly - * with whitespace), not occurrences inside string literals. - */ - const removeSourceMaps = ( content ) => { - return content.replace( /^\s*\/\/# sourceMappingURL=.*$/gm, '' ).trimEnd(); - }; - if ( fs.existsSync( scriptsSrc ) ) { const entries = fs.readdirSync( scriptsSrc, { withFileTypes: true } ); @@ -977,12 +890,7 @@ async function main() { const srcFile = path.join( src, file ); const destFile = path.join( dest, file ); - let content = fs.readFileSync( - srcFile, - 'utf8' - ); - content = removeSourceMaps( content ); - fs.writeFileSync( destFile, content ); + fs.copyFileSync( srcFile, destFile ); copiedCount++; } } @@ -991,7 +899,7 @@ async function main() { ); } else { // Copy other special directories normally. - copyDirectory( src, dest, removeSourceMaps ); + copyDirectory( src, dest ); console.log( ` āœ… ${ entry.name }/ → ${ destName }/` ); @@ -1019,18 +927,7 @@ async function main() { recursive: true, } ); - // Apply source map removal for .js files. - if ( file.endsWith( '.js' ) ) { - let content = fs.readFileSync( - srcFile, - 'utf8' - ); - content = removeSourceMaps( content ); - fs.writeFileSync( destPath, content ); - } else { - // Copy other files as-is (.min.asset.php). - fs.copyFileSync( srcFile, destPath ); - } + fs.copyFileSync( srcFile, destPath ); } } } @@ -1038,122 +935,34 @@ async function main() { // Copy root-level JS files. const dest = path.join( scriptsDest, entry.name ); fs.mkdirSync( path.dirname( dest ), { recursive: true } ); - - let content = fs.readFileSync( src, 'utf8' ); - content = removeSourceMaps( content ); - fs.writeFileSync( dest, content ); + fs.copyFileSync( src, dest ); } } console.log( ' āœ… JavaScript packages copied' ); } - // 3. Copy script modules. - console.log( '\nšŸ“¦ Copying script modules...' ); - const modulesConfig = COPY_CONFIG.modules; - const modulesSrc = path.join( gutenbergBuildDir, modulesConfig.source ); - const modulesDest = path.join( wpIncludesDir, modulesConfig.destination ); - - if ( fs.existsSync( modulesSrc ) ) { - // Use the same source map removal transform. - copyDirectory( modulesSrc, modulesDest, removeSourceMaps ); - console.log( ' āœ… Script modules copied' ); - } - - // 4. Copy styles. - console.log( '\nšŸ“¦ Copying styles...' ); - const stylesConfig = COPY_CONFIG.styles; - const stylesSrc = path.join( gutenbergBuildDir, stylesConfig.source ); - const stylesDest = path.join( wpIncludesDir, stylesConfig.destination ); - - if ( fs.existsSync( stylesSrc ) ) { - copyDirectory( stylesSrc, stylesDest ); - console.log( ' āœ… Styles copied' ); - } - - // 5. Copy blocks (unified: scripts, styles, PHP, JSON). + // 2. Copy blocks (unified: scripts, styles, PHP, JSON). console.log( '\nšŸ“¦ Copying blocks...' ); - const blocksDest = path.join( - wpIncludesDir, - COPY_CONFIG.blocks.destination - ); copyBlockAssets( COPY_CONFIG.blocks ); - // 6. Copy theme JSON files (from Gutenberg lib directory). - console.log( '\nšŸ“¦ Copying theme JSON files...' ); - const themeJsonConfig = COPY_CONFIG.themeJson; - const gutenbergLibDir = path.join( gutenbergDir, 'lib' ); - - for ( const fileMap of themeJsonConfig.files ) { - const src = path.join( gutenbergLibDir, fileMap.from ); - const dest = path.join( wpIncludesDir, fileMap.to ); - - if ( fs.existsSync( src ) ) { - let content = fs.readFileSync( src, 'utf8' ); - - if ( themeJsonConfig.transform && fileMap.from === 'theme.json' ) { - // Transform schema URL for Core. - content = content.replace( - '"$schema": "../schemas/json/theme.json"', - '"$schema": "https://schemas.wp.org/trunk/theme.json"' - ); - } - - fs.writeFileSync( dest, content ); - console.log( ` āœ… ${ fileMap.to }` ); - } else { - console.log( ` āš ļø Not found: ${ fileMap.from }` ); - } - } - - // Copy remaining files to wp-includes. - console.log( '\nšŸ“¦ Copying remaining files to wp-includes...' ); - for ( const fileMap of COPY_CONFIG.wpIncludes ) { - const dest = path.join( wpIncludesDir, fileMap.destination ); - fs.mkdirSync( dest, { recursive: true } ); - for ( const src of fileMap.files ) { - const matches = glob.sync( path.join( gutenbergDir, src ) ); - if ( ! matches.length ) { - throw new Error( `No files found matching '${ src }'` ); - } - for ( const match of matches ) { - const destPath = path.join( dest, path.basename( match ) ); - // Apply transformation for manifest.php to remove gutenberg text domain. - if ( path.basename( match ) === 'manifest.php' ) { - let content = fs.readFileSync( match, 'utf8' ); - content = transformManifestPHP( content ); - fs.writeFileSync( destPath, content ); - } else { - fs.copyFileSync( match, destPath ); - } - } - } - } - - // 7. Generate script-modules-packages.php from individual asset files. + // 3. Generate script-modules-packages.php from individual asset files. console.log( '\nšŸ“¦ Generating script-modules-packages.php...' ); generateScriptModulesPackages(); - // 8. Generate script-loader-packages.php. + // 4. Generate script-loader-packages.php. console.log( '\nšŸ“¦ Generating script-loader-packages.php...' ); generateScriptLoaderPackages(); - // 9. Generate require-dynamic-blocks.php and require-static-blocks.php. + // 5. Generate require-dynamic-blocks.php and require-static-blocks.php. console.log( '\nšŸ“¦ Generating block registration files...' ); generateBlockRegistrationFiles(); - // 10. Generate blocks-json.php from block.json files. + // 6. Generate blocks-json.php from block.json files. console.log( '\nšŸ“¦ Generating blocks-json.php...' ); generateBlocksJson(); - // Summary. console.log( '\nāœ… Copy complete!' ); - console.log( '\nšŸ“Š Summary:' ); - console.log( ` PHP infrastructure: ${ phpDest }` ); - console.log( ` JavaScript: ${ scriptsDest }` ); - console.log( ` Script modules: ${ modulesDest }` ); - console.log( ` Styles: ${ stylesDest }` ); - console.log( ` Blocks: ${ blocksDest }` ); } // Run main function. From 25c90b9c53854e5e2d02a6f57e2476c26bbb47c7 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:30:28 -0500 Subject: [PATCH 21/30] Try to fix timeout issues during `postinstall`. --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index fe34bc70bf120..3a444f9262c65 100644 --- a/package.json +++ b/package.json @@ -112,9 +112,10 @@ "wicg-inert": "3.1.3" }, "scripts": { - "postinstall": "npm run gutenberg:download && grunt build:gutenberg --dev", + "postinstall": "npm run gutenberg:download && npm run build:gutenberg -- --dev", "build": "grunt build", "build:dev": "grunt build --dev", + "build:gutenberg": "grunt build:gutenberg", "dev": "grunt watch --dev", "test": "grunt test", "watch": "grunt watch", From 330a83e5d9785867ea86948cb25473c4dfe6558b Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:35:58 -0500 Subject: [PATCH 22/30] `postinstall build:gutenberg` shouldn't be needed. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3a444f9262c65..f542a52dfa918 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "wicg-inert": "3.1.3" }, "scripts": { - "postinstall": "npm run gutenberg:download && npm run build:gutenberg -- --dev", + "postinstall": "npm run gutenberg:download", "build": "grunt build", "build:dev": "grunt build --dev", "build:gutenberg": "grunt build:gutenberg", From fbb1c58b87825cb56ec6d9371074f99bc9ef46d7 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:41:40 -0500 Subject: [PATCH 23/30] Fix coding standards issue. --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 2d71eae5f1a59..dcca3e553b486 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -595,7 +595,7 @@ module.exports = function(grunt) { // Fix boot module asset file path for Core's different directory structure. return content.replace( /__DIR__\s*\.\s*['"]\/..\/\..\/modules\/boot\/index\.min\.asset\.php['"]/g, - "ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php'" + 'ABSPATH . WPINC . \'/js/dist/script-modules/boot/index.min.asset.php\'' ); } }, From 3050cd1b0a0d439c86d4110814b718aadfdecb74 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 6 Mar 2026 20:23:17 -0600 Subject: [PATCH 24/30] Try using PHP to parse PHP instead of strings and RegExp --- tools/gutenberg/copy.js | 233 +++++++--------------------------------- 1 file changed, 39 insertions(+), 194 deletions(-) diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index 18db83372d87f..684f6c41c50c1 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -9,6 +9,7 @@ * @package WordPress */ +const child_process = require( 'child_process' ); const fs = require( 'fs' ); const path = require( 'path' ); const json2php = require( 'json2php' ); @@ -353,18 +354,9 @@ function generateScriptModulesPackages() { const jsPathRegular = jsPathMin.replace( /\.min\.js$/, '.js' ); try { - // Read and parse the PHP asset file. - const phpContent = fs.readFileSync( fullPath, 'utf8' ); - // Extract the array from PHP: 1 ) { - currentArray += 'array('; - } - i += 5; // Skip 'array('. - continue; - } - - if ( depth > 0 ) { - if ( char === '(' ) { - depth++; - currentArray += char; - } else if ( char === ')' ) { - depth--; - if ( depth === 0 ) { - // Found complete nested array. - const placeholder = `__ARRAY_${ nestedArrays.length }__`; - nestedArrays.push( currentArray ); - content = - content.substring( 0, arrayStart ) + - placeholder + - content.substring( i + 1 ); - i = arrayStart + placeholder.length - 1; - currentArray = ''; - } else { - currentArray += char; - } - } else { - currentArray += char; - } - } - } else if ( depth > 0 ) { - currentArray += char; - } - } - - // Now parse the simplified content. - const result = {}; - const values = []; - let isAssociative = false; - - // Split by top-level commas. - const parts = []; - depth = 0; - inString = false; - let currentPart = ''; - - for ( let i = 0; i < content.length; i++ ) { - const char = content[ i ]; - - if ( - ( char === "'" || char === '"' ) && - ( i === 0 || content[ i - 1 ] !== '\\' ) - ) { - inString = ! inString; - } - - if ( ! inString && char === ',' && depth === 0 ) { - parts.push( currentPart.trim() ); - currentPart = ''; - } else { - currentPart += char; - if ( ! inString ) { - if ( char === '(' ) { - depth++; - } - if ( char === ')' ) { - depth--; - } - } +function readReturnedValueFromPHPFile( phpFilepath ) { + const results = child_process.spawnSync( + 'php', + [ '-r', '$path = file_get_contents( "php://stdin" ); if ( ! is_file( $path ) ) { die( 1 ); } try { $data = require $path; } catch ( \\Throwable $e ) { die( 2 ); } $json = json_encode( $data ); if ( ! is_string( $json ) ) { die( 3 ); } echo $json;' ], + { + encoding: 'utf8', + input: phpFilepath, } - } - if ( currentPart.trim() ) { - parts.push( currentPart.trim() ); - } - - // Parse each part. - for ( const part of parts ) { - const arrowMatch = part.match( /^(.+?)\s*=>\s*(.+)$/ ); - - if ( arrowMatch ) { - isAssociative = true; - let key = arrowMatch[ 1 ].trim().replace( /^['"]|['"]$/g, '' ); - let value = arrowMatch[ 2 ].trim(); + ); - // Replace placeholders. - while ( value.match( /__ARRAY_(\d+)__/ ) ) { - value = value.replace( /__ARRAY_(\d+)__/, ( match, index ) => { - return 'array(' + nestedArrays[ parseInt( index ) ] + ')'; - } ); - } + switch ( results.status ) { + case 0: + return JSON.parse( results.stdout ); - result[ key ] = parseValue( value ); - } else { - // No arrow, indexed array. - let value = part; + case 1: + throw new Error( `Could not read PHP source file: '${ phpFilepath }'` ); - // Replace placeholders. - while ( value.match( /__ARRAY_(\d+)__/ ) ) { - value = value.replace( /__ARRAY_(\d+)__/, ( match, index ) => { - return 'array(' + nestedArrays[ parseInt( index ) ] + ')'; - } ); - } + case 2: + throw new Error( `PHP source file did not return value when imported: '${ phpFilepath }'` ); - values.push( parseValue( value ) ); - } + case 3: + throw new Error( `Could not serialize PHP source value into JSON: '${ phpFilepath }'` ); } - return isAssociative ? result : values; - - /** - * Parse a single value. - * - * @param {string} value - The value string to parse. - * @return {*} Parsed value. - */ - function parseValue( value ) { - value = value.trim(); - - if ( value.startsWith( 'array(' ) && value.endsWith( ')' ) ) { - return parsePHPArray( value.substring( 6, value.length - 1 ) ); - } else if ( value.match( /^['"].*['"]$/ ) ) { - return value.substring( 1, value.length - 1 ); - } else if ( value === 'true' ) { - return true; - } else if ( value === 'false' ) { - return false; - } else if ( ! isNaN( value ) && value !== '' ) { - return parseInt( value, 10 ); - } - return value; - } + throw new Error( `Unknown error while reading PHP source file: '${ phpFilepath }'` ); } /** From f588cf5dfe78b50e2bae53b09b61b647820824bf Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Fri, 6 Mar 2026 20:25:51 -0600 Subject: [PATCH 25/30] Move function to top so it shows more clearly in the diff. --- tools/gutenberg/copy.js | 72 ++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index 684f6c41c50c1..293643c63369a 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -113,6 +113,42 @@ const COPY_CONFIG = { ], }; +/** + * Given a path to a PHP file which returns a single value, converts that + * value into a native JavaScript value (limited by JSON serialization). + * + * @throws Error when PHP source file unable to be read, or PHP is unavailable. + * + * @param {string} phpFilepath Absolute path of PHP file returning a single value. + * @return {Object|Array} JavaScript representation of value from input file. + */ +function readReturnedValueFromPHPFile( phpFilepath ) { + const results = child_process.spawnSync( + 'php', + [ '-r', '$path = file_get_contents( "php://stdin" ); if ( ! is_file( $path ) ) { die( 1 ); } try { $data = require $path; } catch ( \\Throwable $e ) { die( 2 ); } $json = json_encode( $data ); if ( ! is_string( $json ) ) { die( 3 ); } echo $json;' ], + { + encoding: 'utf8', + input: phpFilepath, + } + ); + + switch ( results.status ) { + case 0: + return JSON.parse( results.stdout ); + + case 1: + throw new Error( `Could not read PHP source file: '${ phpFilepath }'` ); + + case 2: + throw new Error( `PHP source file did not return value when imported: '${ phpFilepath }'` ); + + case 3: + throw new Error( `Could not serialize PHP source value into JSON: '${ phpFilepath }'` ); + } + + throw new Error( `Unknown error while reading PHP source file: '${ phpFilepath }'` ); +} + /** * Check if a block is experimental by reading its block.json. * @@ -647,42 +683,6 @@ function generateBlocksJson() { ); } -/** - * Given a path to a PHP file which returns a single value, converts that - * value into a native JavaScript value (limited by JSON serialization). - * - * @throws Error when PHP source file unable to be read, or PHP is unavailable. - * - * @param {string} phpFilepath Absolute path of PHP file returning a single value. - * @return {Object|Array} JavaScript representation of value from input file. - */ -function readReturnedValueFromPHPFile( phpFilepath ) { - const results = child_process.spawnSync( - 'php', - [ '-r', '$path = file_get_contents( "php://stdin" ); if ( ! is_file( $path ) ) { die( 1 ); } try { $data = require $path; } catch ( \\Throwable $e ) { die( 2 ); } $json = json_encode( $data ); if ( ! is_string( $json ) ) { die( 3 ); } echo $json;' ], - { - encoding: 'utf8', - input: phpFilepath, - } - ); - - switch ( results.status ) { - case 0: - return JSON.parse( results.stdout ); - - case 1: - throw new Error( `Could not read PHP source file: '${ phpFilepath }'` ); - - case 2: - throw new Error( `PHP source file did not return value when imported: '${ phpFilepath }'` ); - - case 3: - throw new Error( `Could not serialize PHP source value into JSON: '${ phpFilepath }'` ); - } - - throw new Error( `Unknown error while reading PHP source file: '${ phpFilepath }'` ); -} - /** * Main execution function. */ From e8220460a831c62be24fba3da0ae7d1c77a4fae6 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Sat, 7 Mar 2026 00:15:44 -0500 Subject: [PATCH 26/30] Bump commit hash. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f542a52dfa918..bbc314e487798 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "1ed7da329cc9ffb27cbe5d373ea15db309a135b7", + "sha": "af045ea22e784e8d5afb8077d6c006394b078f2a", "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { From 7d0ca6c0ac821ff4c8cc1921e33643f1e0429f5e Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:05:51 -0400 Subject: [PATCH 27/30] Update the pinned `gutenberg` hash. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4d3f3823f0d99..c3150ce358e34 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "f4d8a5803aa2fbe26e7d9af4d17e80a622b7bab8" + "ref": "7b7fa2bc97a8029a302bd6511cf0d206b5953172" }, "engines": { "node": ">=20.10.0", @@ -145,4 +145,4 @@ "sync-gutenberg-packages": "grunt sync-gutenberg-packages", "postsync-gutenberg-packages": "grunt wp-packages:sync-stable-blocks && grunt build --dev && grunt build" } -} \ No newline at end of file +} From fdda59236d9d3613bb8890431bd60de1b4aa1748 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:58:40 -0400 Subject: [PATCH 28/30] One more bump with additional upstream fixes. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5741e74a5a4fe..a7406e3adcee1 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "7b7fa2bc97a8029a302bd6511cf0d206b5953172", + "sha": "f290e6cb50d66809787fe198ef1cbc222482f37d", "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { From 7ce0157d530006f2eca10043839868da3bbdc120 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:17:36 -0400 Subject: [PATCH 29/30] Sync hashes. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a7406e3adcee1..117cf46f5455f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "f290e6cb50d66809787fe198ef1cbc222482f37d", + "sha": "9b8144036fa5faf75de43d4502ff9809fcf689ad", "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { From aad2939e868a00b61f02a31b6321bc0ab87b4961 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:50:38 -0400 Subject: [PATCH 30/30] Improvements to RegEx for modifying boot module asset. Co-authored-by: Dennis Snell --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index dcca3e553b486..355a8989db3db 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -594,7 +594,7 @@ module.exports = function(grunt) { process: function( content ) { // Fix boot module asset file path for Core's different directory structure. return content.replace( - /__DIR__\s*\.\s*['"]\/..\/\..\/modules\/boot\/index\.min\.asset\.php['"]/g, + /__DIR__\s*\.\s*(['"])\/..\/\..\/modules\/boot\/index\.min\.asset\.php\1/g, 'ABSPATH . WPINC . \'/js/dist/script-modules/boot/index.min.asset.php\'' ); }