diff --git a/Gruntfile.js b/Gruntfile.js index 2ce79d03bddc6..b9fb402317d30 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 managed 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..58ef96bc5d5ad 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,87 @@ 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 {string} scriptsSrc - Path to the Gutenberg scripts source (e.g. `scripts/block-library`). + * @return {string[]} Stable block directory names. + */ +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 + * @param {Object} config - Block configuration from `COPY_CONFIG.blocks`. */ -function copyBlockAssets( config ) { +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 blockJsonSrc = path.join( blockSrc, 'block.json' ); + if ( fs.existsSync( blockJsonSrc ) ) { + fs.copyFileSync( + blockJsonSrc, + path.join( blockDest, 'block.json' ) ); } + } - // 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 +193,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 +200,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 +391,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 +485,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 +548,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 +642,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();