From f4a738b6771a5ec5beb056599f99be004e3202c3 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:07:32 -0400 Subject: [PATCH 01/11] Ensure files removed upstream are properly cleaned. This adjusts the build script to ensure that any files deleted from the upstream built asset are removed as expected. This also solves a long-standing issue where unversioned files persist in `src` after being eliminated, which can cause `grunt verify:old-files` to fail. --- Gruntfile.js | 121 ++++++++++++++------- tools/gutenberg/copy.js | 227 ++++++++++++++++++++++++---------------- 2 files changed, 222 insertions(+), 126 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 2ce79d03bddc6..8e2e6ae014c4e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -41,18 +41,43 @@ module.exports = function(grunt) { 'wp-admin/css/colors/**/*.css', ], - // Built js files, in /src or /build. + // Built JavaScript files that do not belong to a more specific group. jsFiles = [ 'wp-admin/js/', - 'wp-includes/js/', + 'wp-includes/js/*', + /* + * This directory has shared responsibility and is manage through gutenbergUnversionedFiles, webpackFiles, + * and copy:vendor-js. + */ + '!wp-includes/js/dist', + 'wp-includes/js/dist/vendor/*.js', + // Managed by the Gutenberg-related tasks. + '!wp-includes/js/dist/vendor/react-jsx-runtime*', + ], + + // Files sourced from the Gutenberg repository built asset that are ignored by version control. + gutenbergUnversionedFiles = [ + SOURCE_DIR + 'wp-includes/blocks/*/*.css', + SOURCE_DIR + 'wp-includes/css/dist', + SOURCE_DIR + 'wp-includes/js/dist/*.js', + SOURCE_DIR + 'wp-includes/js/dist/script-modules', + SOURCE_DIR + 'wp-includes/js/dist/vendor/react-jsx-runtime*', ], - // All files copied from the Gutenberg repository excluded from version control. - gutenbergFiles = [ - 'wp-includes/js/dist', - 'wp-includes/css/dist', - // Old location kept temporarily to ensure they are cleaned up. - 'wp-includes/icons', + // Files sourced from the Gutenberg repository built asset that are managed through version control. + gutenbergVersionedFiles = [ + // Block assets (block.json, top-level PHP, nested PHP helpers). + SOURCE_DIR + 'wp-includes/blocks/*', + '!' + SOURCE_DIR + 'wp-includes/blocks/index.php', + SOURCE_DIR + 'wp-includes/images/icon-library', + SOURCE_DIR + 'wp-includes/theme.json', + SOURCE_DIR + 'wp-includes/theme-i18n.json', + // Routes and pages. + SOURCE_DIR + 'wp-includes/build', + // PHP manifests generated by gutenberg:copy. + SOURCE_DIR + 'wp-includes/assets/icon-library-manifest.php', + SOURCE_DIR + 'wp-includes/assets/script-loader-packages.php', + SOURCE_DIR + 'wp-includes/assets/script-modules-packages.php', ], // All files built by Webpack, in /src or /build. @@ -241,10 +266,27 @@ module.exports = function(grunt) { return setFilePath( WORKING_DIR, file ); } ), - // Clean files built by the tools/gutenberg scripts. - gutenberg: gutenbergFiles.map( function( file ) { - return setFilePath( WORKING_DIR, file ); - }), + /* + * Clean files sourced from the downloaded zip file built by the Gutenberg repository. + * + * All files originating from the Gutenberg repository's built assets (both tracked and untracked by version + * control) are deleted when `clean:gutenberg` is explicitly called. This ensures that versioned files that + * have been deleted upstream are also removed from version control in this repository. + * + * When `clean:gutenberg` is not explicitly called and run through `grunt clean`, only ignored files are + * cleaned. + */ + gutenberg: { + get src() { + const isExplicitGutenbergClean = + grunt.cli.tasks.length === 1 && + grunt.cli.tasks[ 0 ] !== 'clean'; + return isExplicitGutenbergClean + ? gutenbergUnversionedFiles.concat( gutenbergVersionedFiles ) + : gutenbergUnversionedFiles; + }, + }, + dynamic: { dot: true, expand: true, @@ -289,7 +331,6 @@ module.exports = function(grunt) { expand: true, cwd: SOURCE_DIR, src: buildFiles.concat( [ - '!wp-includes/assets/**', // Assets is extracted into separate copy tasks. '!js/**', // JavaScript is extracted into separate copy tasks. '!.{svn,git}', // Exclude version control folders. '!wp-includes/version.php', // Exclude version.php. @@ -666,24 +707,18 @@ module.exports = function(grunt) { 'constants.php', 'pages/**/*.php', ], - dest: WORKING_DIR + 'wp-includes/build/', + dest: SOURCE_DIR + 'wp-includes/build/', } ], }, /* - * Only copy files relevant to the routes specified in the registry file. - * - * While the registry file does not contain any experimental routes, the `gutenberg/build/routes` directory - * includes the files for all registered routes. Only the files related to the routes specified in the - * registry should be included in the WordPress build. - * - * The `src` list is populated at task runtime by `routes:setup`, which reads the registry after - * `gutenberg:download` has run. See the `routes:setup` task registration for implementation details. + * The list of route source files is populated from the contents of the registry.php file at task runtime by + * `routes:setup`. */ routes: { expand: true, cwd: 'gutenberg/build', src: [], - dest: WORKING_DIR + 'wp-includes/build/', + dest: SOURCE_DIR + 'wp-includes/build/', }, 'gutenberg-js': { files: [ { @@ -692,7 +727,7 @@ module.exports = function(grunt) { src: [ 'pages/**/*.js', ], - dest: WORKING_DIR + 'wp-includes/build/', + dest: SOURCE_DIR + 'wp-includes/build/', } ], }, 'gutenberg-modules': { @@ -706,7 +741,7 @@ module.exports = function(grunt) { // with no debugging value over the minified versions. '!vips/!(*.min).js', ], - dest: WORKING_DIR + 'wp-includes/js/dist/script-modules/', + dest: SOURCE_DIR + 'wp-includes/js/dist/script-modules/', } ], }, 'gutenberg-styles': { @@ -719,7 +754,7 @@ module.exports = function(grunt) { // Per-block CSS is copied to wp-includes/blocks/ by tools/gutenberg/copy.js. '!block-library/*/**', ], - dest: WORKING_DIR + 'wp-includes/css/dist/', + dest: SOURCE_DIR + 'wp-includes/css/dist/', } ], }, 'gutenberg-theme-json': { @@ -738,11 +773,11 @@ module.exports = function(grunt) { files: [ { src: 'gutenberg/lib/theme.json', - dest: WORKING_DIR + 'wp-includes/theme.json', + dest: SOURCE_DIR + 'wp-includes/theme.json', }, { src: 'gutenberg/lib/theme-i18n.json', - dest: WORKING_DIR + 'wp-includes/theme-i18n.json', + dest: SOURCE_DIR + 'wp-includes/theme-i18n.json', }, ], }, @@ -750,8 +785,8 @@ module.exports = function(grunt) { files: [ { expand: true, cwd: 'gutenberg/packages/icons/src/library', - src: '*.svg', - dest: WORKING_DIR + 'wp-includes/images/icon-library', + src: [ '*.svg' ], + dest: SOURCE_DIR + 'wp-includes/images/icon-library', } ], }, 'icon-library-manifest': { @@ -773,7 +808,7 @@ module.exports = function(grunt) { }, files: [ { src: 'gutenberg/packages/icons/src/manifest.php', - dest: WORKING_DIR + 'wp-includes/assets/icon-library-manifest.php', + dest: SOURCE_DIR + 'wp-includes/assets/icon-library-manifest.php', } ], }, }, @@ -1677,10 +1712,9 @@ module.exports = function(grunt) { 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( { cmd: 'node', - args: [ 'tools/gutenberg/copy.js', `--build-dir=${ buildDir }` ], + args: [ 'tools/gutenberg/copy.js' ], opts: { stdio: 'inherit' } }, function( error ) { done( ! error ); @@ -2164,10 +2198,23 @@ module.exports = function(grunt) { } ); } ); - grunt.registerTask( 'build:gutenberg', [ - 'copy:gutenberg-php', + // Detects and copies stable routes. + grunt.registerTask( 'build:routes', [ 'routes:setup', 'copy:routes', + ] ); + + /* + * Refresh the Gutenberg-sourced content in src/. + * + * clean:gutenberg must run first to ensure files removed upstream are purged. + * + * Because all of these tasks write to src/, the outcome is identical for build and build:dev. + */ + grunt.registerTask( 'build:gutenberg', [ + 'clean:gutenberg', + 'copy:gutenberg-php', + 'build:routes', 'copy:gutenberg-js', 'gutenberg:copy', 'copy:gutenberg-modules', @@ -2181,21 +2228,21 @@ module.exports = function(grunt) { if ( grunt.option( 'dev' ) ) { grunt.task.run( [ 'gutenberg:verify', + 'build:gutenberg', 'build:js', 'build:css', 'build:codemirror', - 'build:gutenberg', 'build:certificates' ] ); } else { grunt.task.run( [ 'gutenberg:verify', + 'build:gutenberg', 'build:certificates', 'build:files', 'build:js', 'build:css', 'build:codemirror', - 'build:gutenberg', 'replace:source-maps', 'verify:build' ] ); diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index 8589c9581bed1..da12bfffeb746 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -6,6 +6,14 @@ * This script copies and transforms Gutenberg's build output to WordPress Core. * It handles path transformations from plugin structure to Core structure. * + * Since a number of files sourced from the downloaded zip file are subject to + * version control, the `src/` directory is used as the destination for all + * outputs of this file (both versioned and unversioned). + * + * Grunt will copy the files appropriately when running `build` instead of + * `build:dev`, and the repository's configured ignore rules will manage what + * can be committed. + * * @package WordPress */ @@ -14,24 +22,10 @@ const path = require( 'path' ); const json2php = require( 'json2php' ); const { fromString } = require( 'php-array-reader' ); -// Paths. 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. - */ -const args = process.argv.slice( 2 ); -const buildDirArg = args.find( ( arg ) => arg.startsWith( '--build-dir=' ) ); -const buildTarget = buildDirArg - ? buildDirArg.split( '=' )[ 1 ] - : args.includes( '--dev' ) - ? 'src' - : 'build'; - -const wpIncludesDir = path.join( rootDir, buildTarget, 'wp-includes' ); +const wpIncludesDir = path.join( rootDir, 'src', 'wp-includes' ); /** * Copy configuration. @@ -109,86 +103,91 @@ function isExperimentalBlock( blockJsonPath ) { } /** - * Copy all assets for blocks from Gutenberg to Core. - * Handles scripts, styles, PHP, and JSON for all block types in a unified way. + * Generate a list of stable blocks. * - * @param {Object} config - Block configuration from COPY_CONFIG.blocks + * @param {string} scriptsSrc - Path to the Gutenberg scripts source (e.g. `scripts/block-library`). + * @return {string[]} Stable block directory names. */ -function copyBlockAssets( config ) { +function getStableBlocks( scriptsSrc ) { + if ( ! fs.existsSync( scriptsSrc ) ) { + return []; + } + return fs + .readdirSync( scriptsSrc, { withFileTypes: true } ) + .filter( ( entry ) => entry.isDirectory() ) + .map( ( entry ) => entry.name ) + .filter( ( blockName ) => ! isExperimentalBlock( + path.join( scriptsSrc, blockName, 'block.json' ) + ) ); +} + +/** + * Copy `block.json` files for every stable block. + * + * @param {Object} config - Block configuration from `COPY_CONFIG.blocks`. + */ +function copyBlockJson( config ) { const blocksDest = path.join( wpIncludesDir, config.destination ); for ( const source of config.sources ) { const scriptsSrc = path.join( gutenbergBuildDir, source.scripts ); - const stylesSrc = path.join( gutenbergBuildDir, source.styles ); - const phpSrc = path.join( gutenbergBuildDir, source.php ); - - if ( ! fs.existsSync( scriptsSrc ) ) { - continue; - } - - // 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. - const blockJsonPath = path.join( - scriptsSrc, - blockName, - 'block.json' - ); - if ( isExperimentalBlock( blockJsonPath ) ) { - continue; - } + const blocks = getStableBlocks( scriptsSrc ); + for ( const blockName of blocks ) { + const blockSrc = path.join( scriptsSrc, blockName ); const blockDest = path.join( blocksDest, blockName ); fs.mkdirSync( blockDest, { recursive: true } ); - // 1. Copy scripts/JSON (everything except PHP) - const blockScriptsSrc = path.join( scriptsSrc, blockName ); - if ( fs.existsSync( blockScriptsSrc ) ) { - fs.cpSync( - blockScriptsSrc, - blockDest, - { - recursive: true, - // Skip PHP, copied from build in steps 3 & 4. - filter: f => ! f.endsWith( '.php' ), - } + const jsonFiles = fs + .readdirSync( blockSrc, { withFileTypes: true } ) + .filter( ( entry ) => entry.isFile() && entry.name.endsWith( '.json' ) ) + .map( ( entry ) => entry.name ); + + for ( const file of jsonFiles ) { + fs.copyFileSync( + path.join( blockSrc, file ), + path.join( blockDest, file ) ); } + } - // 2. Copy styles (if they exist in per-block directory) - const blockStylesSrc = path.join( stylesSrc, blockName ); - if ( fs.existsSync( blockStylesSrc ) ) { - const cssFiles = fs - .readdirSync( blockStylesSrc ) - .filter( ( file ) => file.endsWith( '.css' ) ); - for ( const cssFile of cssFiles ) { - fs.copyFileSync( - path.join( blockStylesSrc, cssFile ), - path.join( blockDest, cssFile ) - ); - } - } + console.log( + ` āœ… ${ source.name } block.json copied (${ blocks.length } blocks)` + ); + } +} - // 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 ) ) { - fs.copyFileSync( blockPhpSrc, phpDest ); +/** + * Copy block PHP files for every stable block. + * + * Handles both the top-level `.php` dynamic block files and any nested + * `*.php` helpers under `/` (e.g. `navigation-link/shared/render-submenu-icon.php`). + * + * @param {Object} config - Block configuration from `COPY_CONFIG.blocks`. + */ +function copyBlockPhp( config ) { + const blocksDest = path.join( wpIncludesDir, config.destination ); + + for ( const source of config.sources ) { + const scriptsSrc = path.join( gutenbergBuildDir, source.scripts ); + const phpSrc = path.join( gutenbergBuildDir, source.php ); + const blocks = getStableBlocks( scriptsSrc ); + + for ( const blockName of blocks ) { + // Top-level .php (dynamic block file). + const topLevelPhpSrc = path.join( phpSrc, `${ blockName }.php` ); + const topLevelPhpDest = path.join( blocksDest, `${ blockName }.php` ); + if ( fs.existsSync( topLevelPhpSrc ) ) { + fs.mkdirSync( blocksDest, { recursive: true } ); + fs.copyFileSync( topLevelPhpSrc, topLevelPhpDest ); } - // 4. Copy PHP subdirectories from build (e.g., navigation-link/shared/*.php) + // Nested PHP helpers under /, excluding the block's own index.php. const blockPhpDir = path.join( phpSrc, blockName ); if ( fs.existsSync( blockPhpDir ) ) { + const blockDest = path.join( blocksDest, blockName ); const rootIndex = path.join( blockPhpDir, 'index.php' ); + fs.cpSync( blockPhpDir, blockDest, { recursive: true, filter: function hasPhpFiles( src ) { @@ -198,7 +197,6 @@ function copyBlockAssets( config ) { ( entry ) => hasPhpFiles( path.join( src, entry.name ) ) ); } - // Copy PHP files, but skip root index.php (handled by step 3). return src.endsWith( '.php' ) && src !== rootIndex; }, } ); @@ -206,7 +204,50 @@ function copyBlockAssets( config ) { } console.log( - ` āœ… ${ source.name } blocks copied (${ blockDirs.length } blocks)` + ` āœ… ${ source.name } block PHP copied (${ blocks.length } blocks)` + ); + } +} + +/** + * Copy per-block CSS files for every stable block. + * + * @param {Object} config - Block configuration from `COPY_CONFIG.blocks`. + */ +function copyBlockStyles( config ) { + const blocksDest = path.join( wpIncludesDir, config.destination ); + + for ( const source of config.sources ) { + const scriptsSrc = path.join( gutenbergBuildDir, source.scripts ); + const stylesSrc = path.join( gutenbergBuildDir, source.styles ); + const blocks = getStableBlocks( scriptsSrc ); + + let stylesCopied = 0; + for ( const blockName of blocks ) { + const blockStylesSrc = path.join( stylesSrc, blockName ); + if ( ! fs.existsSync( blockStylesSrc ) ) { + continue; + } + + const blockDest = path.join( blocksDest, blockName ); + fs.mkdirSync( blockDest, { recursive: true } ); + + const cssFiles = fs + .readdirSync( blockStylesSrc ) + .filter( ( file ) => file.endsWith( '.css' ) ); + for ( const cssFile of cssFiles ) { + fs.copyFileSync( + path.join( blockStylesSrc, cssFile ), + path.join( blockDest, cssFile ) + ); + } + if ( cssFiles.length > 0 ) { + stylesCopied++; + } + } + + console.log( + ` āœ… ${ source.name } block CSS copied (${ stylesCopied } blocks)` ); } } @@ -354,9 +395,10 @@ function generateScriptLoaderPackages() { } /** - * Generate require-dynamic-blocks.php and require-static-blocks.php. - * Reads all block.json files from wp-includes/blocks and categorizes them. - * Only includes blocks from block-library, not widgets. + * Generate `require-*-blocks.php` files. + * + * Reads all `block.json` files from the block-library (widgets are ignored) and + * creates `require-dynamic-blocks.php` and `require-static-blocks.php` files. */ function generateBlockRegistrationFiles() { const blocksDir = path.join( wpIncludesDir, 'blocks' ); @@ -447,9 +489,11 @@ ${ staticBlocks.map( ( name ) => `\t'${ name }',` ).join( '\n' ) } } /** - * Generate blocks-json.php from all block.json files. - * Reads all block.json files and combines them into a single PHP array. - * Uses json2php to maintain consistency with Core's formatting. + * Generate a `blocks-json.php` file. + * + * Reads all `block.json` files and combines them into a single PHP array. + * + * This must run after `copyBlockJson` has populated `wp-includes/blocks/`. */ function generateBlocksJson() { const blocksDir = path.join( wpIncludesDir, 'blocks' ); @@ -508,7 +552,7 @@ function generateBlocksJson() { * Main execution function. */ async function main() { - console.log( `šŸ“¦ Copying Gutenberg build to ${ buildTarget }/...` ); + console.log( 'šŸ“¦ Copying Gutenberg build to src/...' ); if ( ! fs.existsSync( gutenbergBuildDir ) ) { console.error( 'āŒ Gutenberg build directory not found' ); @@ -602,11 +646,16 @@ async function main() { console.log( ' āœ… JavaScript packages copied' ); } - // 2. Copy blocks (unified: scripts, styles, PHP, JSON). - console.log( '\nšŸ“¦ Copying blocks...' ); - copyBlockAssets( COPY_CONFIG.blocks ); + console.log( '\nšŸ“¦ Copying block.json files...' ); + copyBlockJson( COPY_CONFIG.blocks ); + + console.log( '\nšŸ“¦ Copying block PHP files...' ); + copyBlockPhp( COPY_CONFIG.blocks ); + + console.log( '\nšŸ“¦ Copying block CSS files...' ); + copyBlockStyles( COPY_CONFIG.blocks ); - // 3. Generate script-modules-packages.php from individual asset files. + // 3. Generate script-modules-packages.php. console.log( '\nšŸ“¦ Generating script-modules-packages.php...' ); generateScriptModulesPackages(); From 624c1cf07b09af93cc6b4beadfc13138b50f55b5 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:03:43 -0400 Subject: [PATCH 02/11] Fix `jshint` issue. --- Gruntfile.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 8e2e6ae014c4e..aa6ea9006e88c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -281,9 +281,9 @@ module.exports = function(grunt) { const isExplicitGutenbergClean = grunt.cli.tasks.length === 1 && grunt.cli.tasks[ 0 ] !== 'clean'; - return isExplicitGutenbergClean - ? gutenbergUnversionedFiles.concat( gutenbergVersionedFiles ) - : gutenbergUnversionedFiles; + return isExplicitGutenbergClean ? + gutenbergUnversionedFiles.concat( gutenbergVersionedFiles ) : + gutenbergUnversionedFiles; }, }, From 5541c0ddf448cf23f424f74658cb92562f125a6a Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:06:57 -0400 Subject: [PATCH 03/11] Applying Copilot suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Gruntfile.js | 4 ++-- tools/gutenberg/copy.js | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index aa6ea9006e88c..b9fb402317d30 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -46,8 +46,8 @@ module.exports = function(grunt) { 'wp-admin/js/', 'wp-includes/js/*', /* - * This directory has shared responsibility and is manage through gutenbergUnversionedFiles, webpackFiles, - * and copy:vendor-js. + * This directory has shared responsibility and is managed through + * gutenbergUnversionedFiles, webpackFiles, and copy:vendor-js. */ '!wp-includes/js/dist', 'wp-includes/js/dist/vendor/*.js', diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index da12bfffeb746..58ef96bc5d5ad 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -138,15 +138,11 @@ function copyBlockJson( config ) { const blockDest = path.join( blocksDest, blockName ); fs.mkdirSync( blockDest, { recursive: true } ); - const jsonFiles = fs - .readdirSync( blockSrc, { withFileTypes: true } ) - .filter( ( entry ) => entry.isFile() && entry.name.endsWith( '.json' ) ) - .map( ( entry ) => entry.name ); - - for ( const file of jsonFiles ) { + const blockJsonSrc = path.join( blockSrc, 'block.json' ); + if ( fs.existsSync( blockJsonSrc ) ) { fs.copyFileSync( - path.join( blockSrc, file ), - path.join( blockDest, file ) + blockJsonSrc, + path.join( blockDest, 'block.json' ) ); } } From bd30b7291c684c59d426ef0a4bbd75bc651c4512 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:51:11 -0400 Subject: [PATCH 04/11] Note that experimental blocks are excluded. --- tools/gutenberg/copy.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index 58ef96bc5d5ad..b0dedb62a1c8b 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -105,6 +105,8 @@ function isExperimentalBlock( blockJsonPath ) { /** * Generate a list of stable blocks. * + * Blocks marked as `"__experimental": true` in a `block.json` file are excluded. + * * @param {string} scriptsSrc - Path to the Gutenberg scripts source (e.g. `scripts/block-library`). * @return {string[]} Stable block directory names. */ From be5537231d3c89c84e7d05ee26cff340c53af382 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:56:25 -0400 Subject: [PATCH 05/11] Avoid ternary expression for clarity. Co-authored-by: aaronjorbin Co-authored-by: Aaron Jorbin --- Gruntfile.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index b9fb402317d30..cde85e8d98824 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -278,12 +278,11 @@ module.exports = function(grunt) { */ gutenberg: { get src() { - const isExplicitGutenbergClean = - grunt.cli.tasks.length === 1 && - grunt.cli.tasks[ 0 ] !== 'clean'; - return isExplicitGutenbergClean ? - gutenbergUnversionedFiles.concat( gutenbergVersionedFiles ) : - gutenbergUnversionedFiles; + if( grunt.cli.tasks.length === 1 && grunt.cli.tasks[ 0 ] !== 'clean' ) { + return gutenbergUnversionedFiles.concat( gutenbergVersionedFiles ); + } else { + return gutenbergUnversionedFiles; + } }, }, From 878e21434b9ca29d518f932e45d3bb58b0e0d98f Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:58:16 -0400 Subject: [PATCH 06/11] Move script-related logic into a function. --- tools/gutenberg/copy.js | 173 +++++++++++++++++++++------------------- 1 file changed, 90 insertions(+), 83 deletions(-) diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index b0dedb62a1c8b..90523d997f281 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -123,6 +123,95 @@ function getStableBlocks( scriptsSrc ) { ) ); } +/** + * Copy JavaScript files. + * + * @param {ScriptsConfig} config - Scripts configuration from `COPY_CONFIG.scripts`. + */ +function copyScripts( config ) { + const scriptsSrc = path.join( gutenbergBuildDir, config.source ); + const scriptsDest = path.join( wpIncludesDir, config.destination ); + + if ( ! fs.existsSync( scriptsSrc ) ) { + return; + } + + const entries = fs.readdirSync( scriptsSrc, { withFileTypes: true } ); + + for ( const entry of entries ) { + const src = path.join( scriptsSrc, entry.name ); + + if ( entry.isDirectory() ) { + // Check if this should be copied as a directory (like vendors/). + if ( + config.copyDirectories && + config.directoryRenames && + config.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). + */ + const destName = config.directoryRenames[ entry.name ]; + const dest = path.join( scriptsDest, destName ); + + if ( entry.name === 'vendors' ) { + // Only copy react-jsx-runtime files, skip react and react-dom. + const vendorFiles = fs.readdirSync( src ); + let copiedCount = 0; + fs.mkdirSync( dest, { recursive: true } ); + for ( const file of vendorFiles ) { + if ( + file.startsWith( 'react-jsx-runtime' ) && + file.endsWith( '.js' ) + ) { + const srcFile = path.join( src, file ); + const destFile = path.join( dest, file ); + + fs.copyFileSync( srcFile, destFile ); + copiedCount++; + } + } + console.log( + ` āœ… ${ entry.name }/ → ${ destName }/ (react-jsx-runtime only, ${ copiedCount } files)` + ); + } + } else { + /* + * 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 ) { + if ( /^index\.(js|min\.js)$/.test( file ) ) { + const srcFile = path.join( src, file ); + // Replace 'index.' with 'package-name.'. + const destFile = file.replace( + /^index\./, + `${ entry.name }.` + ); + const destPath = path.join( scriptsDest, destFile ); + + fs.mkdirSync( path.dirname( destPath ), { + recursive: true, + } ); + + fs.copyFileSync( srcFile, destPath ); + } + } + } + } else if ( entry.isFile() && entry.name.endsWith( '.js' ) ) { + // Copy root-level JS files. + const dest = path.join( scriptsDest, entry.name ); + fs.mkdirSync( path.dirname( dest ), { recursive: true } ); + fs.copyFileSync( src, dest ); + } + } + + console.log( ' āœ… JavaScript packages copied' ); +} + /** * Copy `block.json` files for every stable block. * @@ -560,89 +649,7 @@ async function main() { // 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 ); - - if ( fs.existsSync( scriptsSrc ) ) { - const entries = fs.readdirSync( scriptsSrc, { withFileTypes: true } ); - - for ( const entry of entries ) { - const src = path.join( scriptsSrc, entry.name ); - - if ( entry.isDirectory() ) { - // Check if this should be copied as a directory (like vendors/). - if ( - scriptsConfig.copyDirectories && - 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). - */ - const destName = - scriptsConfig.directoryRenames[ entry.name ]; - const dest = path.join( scriptsDest, destName ); - - if ( entry.name === 'vendors' ) { - // Only copy react-jsx-runtime files, skip react and react-dom. - const vendorFiles = fs.readdirSync( src ); - let copiedCount = 0; - fs.mkdirSync( dest, { recursive: true } ); - for ( const file of vendorFiles ) { - if ( - file.startsWith( 'react-jsx-runtime' ) && - file.endsWith( '.js' ) - ) { - const srcFile = path.join( src, file ); - const destFile = path.join( dest, file ); - - fs.copyFileSync( srcFile, destFile ); - copiedCount++; - } - } - console.log( - ` āœ… ${ entry.name }/ → ${ destName }/ (react-jsx-runtime only, ${ copiedCount } files)` - ); - } - } else { - /* - * 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 ) { - if ( - /^index\.(js|min\.js)$/.test( file ) - ) { - const srcFile = path.join( src, file ); - // Replace 'index.' with 'package-name.'. - const destFile = file.replace( - /^index\./, - `${ entry.name }.` - ); - const destPath = path.join( scriptsDest, destFile ); - - fs.mkdirSync( path.dirname( destPath ), { - recursive: true, - } ); - - fs.copyFileSync( srcFile, destPath ); - } - } - } - } else if ( entry.isFile() && entry.name.endsWith( '.js' ) ) { - // Copy root-level JS files. - const dest = path.join( scriptsDest, entry.name ); - fs.mkdirSync( path.dirname( dest ), { recursive: true } ); - fs.copyFileSync( src, dest ); - } - } - - console.log( ' āœ… JavaScript packages copied' ); - } + copyScripts( COPY_CONFIG.scripts ); console.log( '\nšŸ“¦ Copying block.json files...' ); copyBlockJson( COPY_CONFIG.blocks ); From 6258551b91c483fd32329581b237bedebf0278a7 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:58:27 -0400 Subject: [PATCH 07/11] Add type definitions. --- tools/gutenberg/copy.js | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index 90523d997f281..4f00700379718 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -27,6 +27,37 @@ const gutenbergDir = path.join( rootDir, 'gutenberg' ); const gutenbergBuildDir = path.join( gutenbergDir, 'build' ); const wpIncludesDir = path.join( rootDir, 'src', 'wp-includes' ); +/** + * JS package copy configuration. + * + * @typedef ScriptsConfig + * @type {object} + * @property {string} source - Gutenberg-relative source directory (e.g. `'scripts'`). + * @property {string} destination - Subpath under `wp-includes/` where packages land (e.g. `'js/dist'`). + * @property {boolean} copyDirectories - Whether to copy whole directories (with optional renames) as-is. + * @property {Object} directoryRenames - Map of source directory name → destination directory name. + */ + +/** + * One block family entry — block library, widget blocks, etc. + * + * @typedef BlockConfigSource + * @type {object} + * @property {string} name - Human-readable label (e.g. `'block-library'`, `'widgets'`). + * @property {string} scripts - Gutenberg-relative path to the block scripts directory. + * @property {string} styles - Gutenberg-relative path to the block styles directory. + * @property {string} php - Gutenberg-relative path to the block PHP directory. + */ + +/** + * Block copy configuration. + * + * @typedef BlockConfig + * @type {object} + * @property {string} destination - Subpath under `wp-includes/` where blocks land (e.g. `'blocks'`). + * @property {BlockConfigSource[]} sources - One entry per block family. + */ + /** * Copy configuration. * Defines what to copy from Gutenberg build and where it goes in Core. @@ -215,7 +246,7 @@ function copyScripts( config ) { /** * Copy `block.json` files for every stable block. * - * @param {Object} config - Block configuration from `COPY_CONFIG.blocks`. + * @param {BlockConfig} config - Block configuration from `COPY_CONFIG.blocks`. */ function copyBlockJson( config ) { const blocksDest = path.join( wpIncludesDir, config.destination ); @@ -250,7 +281,7 @@ function copyBlockJson( config ) { * Handles both the top-level `.php` dynamic block files and any nested * `*.php` helpers under `/` (e.g. `navigation-link/shared/render-submenu-icon.php`). * - * @param {Object} config - Block configuration from `COPY_CONFIG.blocks`. + * @param {BlockConfig} config - Block configuration from `COPY_CONFIG.blocks`. */ function copyBlockPhp( config ) { const blocksDest = path.join( wpIncludesDir, config.destination ); @@ -299,7 +330,7 @@ function copyBlockPhp( config ) { /** * Copy per-block CSS files for every stable block. * - * @param {Object} config - Block configuration from `COPY_CONFIG.blocks`. + * @param {BlockConfig} config - Block configuration from `COPY_CONFIG.blocks`. */ function copyBlockStyles( config ) { const blocksDest = path.join( wpIncludesDir, config.destination ); From 32d1436acef5d5b721421a49d3ba472ccc7f31d7 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:12:08 -0400 Subject: [PATCH 08/11] Use more specific type for mapping objects. Co-authored-by: westonruter Co-authored-by: Weston Ruter --- tools/gutenberg/copy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index 4f00700379718..f09a139fb0247 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -35,7 +35,7 @@ const wpIncludesDir = path.join( rootDir, 'src', 'wp-includes' ); * @property {string} source - Gutenberg-relative source directory (e.g. `'scripts'`). * @property {string} destination - Subpath under `wp-includes/` where packages land (e.g. `'js/dist'`). * @property {boolean} copyDirectories - Whether to copy whole directories (with optional renames) as-is. - * @property {Object} directoryRenames - Map of source directory name → destination directory name. + * @property {Record} directoryRenames - Map of source directory name → destination directory name. */ /** From acb5b5f8746b85e99edb6111406963a27d8058b5 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:32:17 -0400 Subject: [PATCH 09/11] Finish adding type definitions and checking. --- tools/gutenberg/copy.js | 39 +++++++++++++++++++++++++-------------- tsconfig.json | 1 + 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index 4f00700379718..a2e2e3d417583 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -19,7 +19,9 @@ const fs = require( 'fs' ); const path = require( 'path' ); -const json2php = require( 'json2php' ); +const json2php = /** @type {typeof import('json2php').default} */ ( + /** @type {unknown} */ ( require( 'json2php' ) ) +); const { fromString } = require( 'php-array-reader' ); const rootDir = path.resolve( __dirname, '../..' ); @@ -106,7 +108,7 @@ const COPY_CONFIG = { * @throws Error when PHP source file unable to be read or parsed. * * @param {string} phpFilepath Absolute path of PHP file returning a single value. - * @return {Object|Array} JavaScript representation of value from input file. + * @return {any} JavaScript representation of value from input file. */ function readReturnedValueFromPHPFile( phpFilepath ) { const content = fs.readFileSync( phpFilepath, 'utf8' ); @@ -306,17 +308,23 @@ function copyBlockPhp( config ) { const blockDest = path.join( blocksDest, blockName ); const rootIndex = path.join( blockPhpDir, 'index.php' ); + /** + * @param {string} src + * @return {boolean} + */ + function hasPhpFiles( src ) { + const stat = fs.statSync( src ); + if ( stat.isDirectory() ) { + return fs.readdirSync( src, { withFileTypes: true } ).some( + ( entry ) => hasPhpFiles( path.join( src, entry.name ) ) + ); + } + return src.endsWith( '.php' ) && src !== rootIndex; + } + fs.cpSync( blockPhpDir, blockDest, { recursive: true, - filter: function hasPhpFiles( src ) { - const stat = fs.statSync( src ); - if ( stat.isDirectory() ) { - return fs.readdirSync( src, { withFileTypes: true } ).some( - ( entry ) => hasPhpFiles( path.join( src, entry.name ) ) - ); - } - return src.endsWith( '.php' ) && src !== rootIndex; - }, + filter: hasPhpFiles, } ); } } @@ -377,6 +385,7 @@ function copyBlockStyles( config ) { */ function generateScriptModulesPackages() { const modulesDir = path.join( gutenbergBuildDir, 'modules' ); + /** @type {Record} */ const assets = {}; /** @@ -413,7 +422,7 @@ function generateScriptModulesPackages() { } catch ( error ) { console.error( ` āš ļø Error reading ${ relativePath }:`, - error.message + error instanceof Error ? error.message : String( error ) ); } } @@ -450,6 +459,7 @@ function generateScriptModulesPackages() { */ function generateScriptLoaderPackages() { const scriptsDir = path.join( gutenbergBuildDir, 'scripts' ); + /** @type {Record} */ const assets = {}; if ( ! fs.existsSync( scriptsDir ) ) { @@ -485,7 +495,7 @@ function generateScriptLoaderPackages() { } catch ( error ) { console.error( ` āš ļø Error reading ${ entry.name }/index.min.asset.php:`, - error.message + error instanceof Error ? error.message : String( error ) ); } } @@ -615,6 +625,7 @@ ${ staticBlocks.map( ( name ) => `\t'${ name }',` ).join( '\n' ) } */ function generateBlocksJson() { const blocksDir = path.join( wpIncludesDir, 'blocks' ); + /** @type {Record} */ const blocks = {}; if ( ! fs.existsSync( blocksDir ) ) { @@ -640,7 +651,7 @@ function generateBlocksJson() { } catch ( error ) { console.error( ` āš ļø Error reading ${ entry.name }/block.json:`, - error.message + error instanceof Error ? error.message : String( error ) ); } } diff --git a/tsconfig.json b/tsconfig.json index 87abe9fb7a42b..e9f36c374ac89 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,7 @@ "src/js/_enqueues/wp/code-editor.js", "src/js/_enqueues/lib/codemirror/javascript-lint.js", "src/js/_enqueues/lib/codemirror/htmlhint-kses.js", + "tools/gutenberg/copy.js", "tools/gutenberg/download.js", "tools/gutenberg/utils.js" ] From b36dada22aa5ae024aafd90335093135413fea82 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:19:43 -0400 Subject: [PATCH 10/11] Restore destructive cleanup as the default for clean:gutenberg. --- Gruntfile.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index cde85e8d98824..a7b3bc372736c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -278,10 +278,16 @@ module.exports = function(grunt) { */ gutenberg: { get src() { - if( grunt.cli.tasks.length === 1 && grunt.cli.tasks[ 0 ] !== 'clean' ) { - return gutenbergUnversionedFiles.concat( gutenbergVersionedFiles ); - } else { + const cli = grunt.cli.tasks; + // Preserve versioned files only when running bare `grunt clean`. + const isBareCleanSweep = + cli.includes( 'clean' ) && + ! cli.includes( 'clean:gutenberg' ); + + if ( isBareCleanSweep ) { return gutenbergUnversionedFiles; + } else { + return gutenbergUnversionedFiles.concat( gutenbergVersionedFiles ); } }, }, From 7922c809c996e5b009f6a90a642a68c35a374933 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:51:53 -0400 Subject: [PATCH 11/11] Remove `--dev` flag for `build:gutenberg`. --- Gruntfile.js | 2 +- package.json | 2 +- tools/gutenberg/utils.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index a7b3bc372736c..dae8c3e972e4c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1707,7 +1707,7 @@ module.exports = function(grunt) { */ grunt.util.spawn( { grunt: true, - args: [ 'build:gutenberg', '--dev' ], + args: [ 'build:gutenberg' ], opts: { stdio: 'inherit' } }, function( buildError ) { done( ! buildError ); diff --git a/package.json b/package.json index 430efdd2fba85..429e0469dd491 100644 --- a/package.json +++ b/package.json @@ -141,6 +141,6 @@ "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", "gutenberg:copy": "node tools/gutenberg/copy.js", "gutenberg:verify": "node tools/gutenberg/utils.js", - "gutenberg:download": "node tools/gutenberg/download.js && grunt build:gutenberg --dev" + "gutenberg:download": "node tools/gutenberg/download.js && grunt build:gutenberg" } } diff --git a/tools/gutenberg/utils.js b/tools/gutenberg/utils.js index 43047b5ee5dd7..3ba95199578b4 100644 --- a/tools/gutenberg/utils.js +++ b/tools/gutenberg/utils.js @@ -139,7 +139,7 @@ async function resolveExpectedSha( { ref, ghcrRepo, isMutable } ) { /** * Trigger a fresh download of the Gutenberg artifact by spawning download.js, - * then run `grunt build:gutenberg --dev` to copy the build to src/. + * then run `grunt build:gutenberg` to copy the build into src/. * Exits the process if either step fails. */ function downloadGutenberg() { @@ -148,7 +148,7 @@ function downloadGutenberg() { process.exit( downloadResult.status ?? 1 ); } - const buildResult = spawnSync( 'grunt', [ 'build:gutenberg', '--dev' ], { stdio: 'inherit', shell: true } ); + const buildResult = spawnSync( 'grunt', [ 'build:gutenberg' ], { stdio: 'inherit', shell: true } ); if ( buildResult.status !== 0 ) { process.exit( buildResult.status ?? 1 ); }