diff --git a/Gruntfile.js b/Gruntfile.js index 2ce79d03bddc6..dae8c3e972e4c 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,32 @@ 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 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 ); + } + }, + }, + dynamic: { dot: true, expand: true, @@ -289,7 +336,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 +712,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 +732,7 @@ module.exports = function(grunt) { src: [ 'pages/**/*.js', ], - dest: WORKING_DIR + 'wp-includes/build/', + dest: SOURCE_DIR + 'wp-includes/build/', } ], }, 'gutenberg-modules': { @@ -706,7 +746,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 +759,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 +778,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 +790,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 +813,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', } ], }, }, @@ -1667,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 ); @@ -1677,10 +1717,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 +2203,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 +2233,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/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/copy.js b/tools/gutenberg/copy.js index 8589c9581bed1..3da78e4b14611 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -6,32 +6,59 @@ * 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 */ 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' ); -// Paths. const rootDir = path.resolve( __dirname, '../..' ); 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 {Record} directoryRenames - Map of source directory name → destination directory name. + */ -/* - * Determine build target from command line argument (--dev or --build-dir). - * Default to 'src' for development. +/** + * 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. */ -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' ); +/** + * 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. @@ -81,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' ); @@ -109,104 +136,244 @@ 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 + * 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. */ -function copyBlockAssets( config ) { - const blocksDest = path.join( wpIncludesDir, config.destination ); +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' ) + ) ); +} - 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 ); +/** + * 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 ) ) { - continue; - } + if ( ! fs.existsSync( scriptsSrc ) ) { + return; + } - // 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 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. + * + * @param {BlockConfig} 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 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 {BlockConfig} 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' ); + + /** + * @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 ) ) - ); - } - // Copy PHP files, but skip root index.php (handled by step 3). - return src.endsWith( '.php' ) && src !== rootIndex; - }, + filter: hasPhpFiles, } ); } } 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 {BlockConfig} 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)` ); } } @@ -218,6 +385,7 @@ function copyBlockAssets( config ) { */ function generateScriptModulesPackages() { const modulesDir = path.join( gutenbergBuildDir, 'modules' ); + /** @type {Record} */ const assets = {}; /** @@ -254,7 +422,7 @@ function generateScriptModulesPackages() { } catch ( error ) { console.error( ` ⚠️ Error reading ${ relativePath }:`, - error.message + error instanceof Error ? error.message : String( error ) ); } } @@ -291,6 +459,7 @@ function generateScriptModulesPackages() { */ function generateScriptLoaderPackages() { const scriptsDir = path.join( gutenbergBuildDir, 'scripts' ); + /** @type {Record} */ const assets = {}; if ( ! fs.existsSync( scriptsDir ) ) { @@ -326,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 ) ); } } @@ -354,9 +523,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,12 +617,15 @@ ${ 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' ); + /** @type {Record} */ const blocks = {}; if ( ! fs.existsSync( blocksDir ) ) { @@ -478,7 +651,7 @@ function generateBlocksJson() { } catch ( error ) { console.error( ` ⚠️ Error reading ${ entry.name }/block.json:`, - error.message + error instanceof Error ? error.message : String( error ) ); } } @@ -508,7 +681,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' ); @@ -518,95 +691,18 @@ 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 ); + copyScripts( COPY_CONFIG.scripts ); - if ( fs.existsSync( scriptsSrc ) ) { - const entries = fs.readdirSync( scriptsSrc, { withFileTypes: true } ); + console.log( '\n📦 Copying block.json files...' ); + copyBlockJson( COPY_CONFIG.blocks ); - 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' ); - } + console.log( '\n📦 Copying block PHP files...' ); + copyBlockPhp( COPY_CONFIG.blocks ); - // 2. Copy blocks (unified: scripts, styles, PHP, JSON). - console.log( '\n📦 Copying blocks...' ); - copyBlockAssets( 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(); 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 ); } 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" ]