close

Make WordPress Core

Changeset 61873


Ignore:
Timestamp:
03/10/2026 04:10:38 AM (4 days ago)
Author:
desrosj
Message:

Build/Test Tools: Remove the requirement to clone/build Gutenberg.

This iterates on the changes from [61438] by removing the need to:

  • Check out the WordPress/gutenberg repository at the pinned hash.
  • Run npm install within that checkout.
  • Run npm build within that checkout.

Instead, the build script will now download a prebuilt zip file published to the GitHub Container Registry by a GitHub Actions workflow recently merged to the Gutenberg Repository (related PR: https://github.com/WordPress/gutenberg/pull/75844).

This also removes redundant code responsible for:

  • Copying files from the gutenberg directory to the appropriate locations during the build script in favor of using grunt copy.
  • Modifying built files to replace specific text, such as sourceMappingURL, in favor of grunt replace.

The remaining files within the tools/gutenberg directory have been renamed to remove gutenberg from the file names. Since these are already nested in a gutenberg directory, that was redundant.

Since the intention of the pinned value for the repository in the package.json file is to specify a full-length commit hash, ref has been renamed to sha. In Git ref encompasses branches, tags, and commit hashes, so this hopefully makes it more clear that something like branch-name should not be used.

Follow up to [61438], [61439], [61458], [61492], [61677], [61867].

Props desrosj, dmsnell, westonruter, mcsf, jorbin.
See #64393.

Location:
trunk
Files:
2 added
3 deleted
4 edited
1 moved

Legend:

Unmodified
Added
Removed
  • trunk/Gruntfile.js

    r61830 r61873  
    5454            'wp-includes/assets/*',
    5555            'wp-includes/css/dist',
    56             'wp-includes/blocks/**/*.css',
    5756            '!wp-includes/assets/script-loader-packages.min.php',
    5857            '!wp-includes/assets/script-modules-packages.min.php',
     
    589588                src: 'vendor/composer/ca-bundle/res/cacert.pem',
    590589                dest: SOURCE_DIR + 'wp-includes/certificates/ca-bundle.crt'
    591             }
     590            },
     591            // Gutenberg PHP infrastructure files (routes.php, pages.php, constants.php, pages/, routes/).
     592            'gutenberg-php': {
     593                options: {
     594                    process: function( content ) {
     595                        // Fix boot module asset file path for Core's different directory structure.
     596                        return content.replace(
     597                            /__DIR__\s*\.\s*(['"])\/..\/\..\/modules\/boot\/index\.min\.asset\.php\1/g,
     598                            'ABSPATH . WPINC . \'/js/dist/script-modules/boot/index.min.asset.php\''
     599                        );
     600                    }
     601                },
     602                files: [ {
     603                    expand: true,
     604                    cwd: 'gutenberg/build',
     605                    src: [
     606                        'routes.php',
     607                        'pages.php',
     608                        'constants.php',
     609                        'pages/**/*.php',
     610                        'routes/**/*.php',
     611                    ],
     612                    dest: WORKING_DIR + 'wp-includes/build/',
     613                } ],
     614            },
     615            'gutenberg-modules': {
     616                files: [ {
     617                    expand: true,
     618                    cwd: 'gutenberg/build/modules',
     619                    src: [ '**/*', '!**/*.map' ],
     620                    dest: WORKING_DIR + 'wp-includes/js/dist/script-modules/',
     621                } ],
     622            },
     623            'gutenberg-styles': {
     624                files: [ {
     625                    expand: true,
     626                    cwd: 'gutenberg/build/styles',
     627                    src: [ '**/*', '!**/*.map' ],
     628                    dest: WORKING_DIR + 'wp-includes/css/dist/',
     629                } ],
     630            },
     631            'gutenberg-theme-json': {
     632                options: {
     633                    process: function( content, srcpath ) {
     634                        // Replace the local schema URL with the canonical public URL for Core.
     635                        if ( path.basename( srcpath ) === 'theme.json' ) {
     636                            return content.replace(
     637                                '"$schema": "../schemas/json/theme.json"',
     638                                '"$schema": "https://schemas.wp.org/trunk/theme.json"'
     639                            );
     640                        }
     641                        return content;
     642                    }
     643                },
     644                files: [
     645                    {
     646                        src: 'gutenberg/lib/theme.json',
     647                        dest: WORKING_DIR + 'wp-includes/theme.json',
     648                    },
     649                    {
     650                        src: 'gutenberg/lib/theme-i18n.json',
     651                        dest: WORKING_DIR + 'wp-includes/theme-i18n.json',
     652                    },
     653                ],
     654            },
     655            'gutenberg-icons': {
     656                options: {
     657                    process: function( content, srcpath ) {
     658                        // Remove the 'gutenberg' text domain from _x() calls in manifest.php.
     659                        if ( path.basename( srcpath ) === 'manifest.php' ) {
     660                            return content.replace(
     661                                /_x\(\s*([^,]+),\s*([^,]+),\s*['"]gutenberg['"]\s*\)/g,
     662                                '_x( $1, $2 )'
     663                            );
     664                        }
     665                        return content;
     666                    }
     667                },
     668                files: [
     669                    {
     670                        src: 'gutenberg/packages/icons/src/manifest.php',
     671                        dest: WORKING_DIR + 'wp-includes/icons/manifest.php',
     672                    },
     673                    {
     674                        expand: true,
     675                        cwd: 'gutenberg/packages/icons/src/library',
     676                        src: '*.svg',
     677                        dest: WORKING_DIR + 'wp-includes/icons/library/',
     678                    },
     679                ],
     680            },
    592681        },
    593682        sass: {
     
    13241413                    {
    13251414                        expand: true,
    1326                         flatten: true,
    1327                         src: [
    1328                             BUILD_DIR + 'wp-includes/js/dist/block-editor.js',
    1329                             BUILD_DIR + 'wp-includes/js/dist/commands.js',
    1330                         ],
    1331                         dest: BUILD_DIR + 'wp-includes/js/dist/'
     1415                        cwd: BUILD_DIR + 'wp-includes/js/dist/',
     1416                        src: [ '*.js' ],
     1417                        dest: BUILD_DIR + 'wp-includes/js/dist/',
    13321418                    },
    13331419                    {
    13341420                        expand: true,
    1335                         flatten: true,
    1336                         src: [
    1337                             BUILD_DIR + 'wp-includes/js/dist/vendor/**/*.js'
    1338                         ],
    1339                         dest: BUILD_DIR + 'wp-includes/js/dist/vendor/'
     1421                        cwd: BUILD_DIR + 'wp-includes/js/dist/vendor/',
     1422                        src: [ '**/*.js' ],
     1423                        dest: BUILD_DIR + 'wp-includes/js/dist/vendor/',
     1424                    },
     1425                    {
     1426                        expand: true,
     1427                        cwd: BUILD_DIR + 'wp-includes/js/dist/script-modules/',
     1428                        src: [ '**/*.js' ],
     1429                        dest: BUILD_DIR + 'wp-includes/js/dist/script-modules/',
    13401430                    }
    13411431                ]
     
    14761566
    14771567    // Gutenberg integration tasks.
    1478     grunt.registerTask( 'gutenberg-checkout', 'Checks out the Gutenberg repository.', function() {
     1568    grunt.registerTask( 'gutenberg:verify', 'Verifies the installed Gutenberg version matches the expected SHA.', function() {
    14791569        const done = this.async();
    14801570        grunt.util.spawn( {
    14811571            cmd: 'node',
    1482             args: [ 'tools/gutenberg/checkout-gutenberg.js' ],
     1572            args: [ 'tools/gutenberg/utils.js' ],
    14831573            opts: { stdio: 'inherit' }
    14841574        }, function( error ) {
     
    14871577    } );
    14881578
    1489     grunt.registerTask( 'gutenberg-build', 'Builds the Gutenberg repository.', function() {
     1579    grunt.registerTask( 'gutenberg:download', 'Downloads the built Gutenberg artifact.', function() {
    14901580        const done = this.async();
     1581        const args = [ 'tools/gutenberg/download.js' ];
     1582        if ( grunt.option( 'force' ) ) {
     1583            args.push( '--force' );
     1584        }
    14911585        grunt.util.spawn( {
    14921586            cmd: 'node',
    1493             args: [ 'tools/gutenberg/build-gutenberg.js' ],
     1587            args,
    14941588            opts: { stdio: 'inherit' }
    14951589        }, function( error ) {
     
    14981592    } );
    14991593
    1500     grunt.registerTask( 'gutenberg-copy', 'Copies Gutenberg build output to WordPress Core.', function() {
     1594    grunt.registerTask( 'gutenberg:copy', 'Copies Gutenberg JS packages and block assets to WordPress Core.', function() {
    15011595        const done = this.async();
    15021596        const buildDir = grunt.option( 'dev' ) ? 'src' : 'build';
    15031597        grunt.util.spawn( {
    15041598            cmd: 'node',
    1505             args: [ 'tools/gutenberg/copy-gutenberg-build.js', `--build-dir=${ buildDir }` ],
    1506             opts: { stdio: 'inherit' }
    1507         }, function( error ) {
    1508             done( ! error );
    1509         } );
    1510     } );
    1511 
    1512     grunt.registerTask( 'gutenberg-sync', 'Syncs Gutenberg checkout and build if ref has changed.', function() {
    1513         const done = this.async();
    1514         grunt.util.spawn( {
    1515             cmd: 'node',
    1516             args: [ 'tools/gutenberg/sync-gutenberg.js' ],
     1599            args: [ 'tools/gutenberg/copy.js', `--build-dir=${ buildDir }` ],
    15171600            opts: { stdio: 'inherit' }
    15181601        }, function( error ) {
     
    19572040    } );
    19582041
     2042    grunt.registerTask( 'build:gutenberg', [
     2043        'copy:gutenberg-php',
     2044        'gutenberg:copy',
     2045        'copy:gutenberg-modules',
     2046        'copy:gutenberg-styles',
     2047        'copy:gutenberg-theme-json',
     2048        'copy:gutenberg-icons',
     2049    ] );
     2050
    19592051    grunt.registerTask( 'build', function() {
    19602052        if ( grunt.option( 'dev' ) ) {
    19612053            grunt.task.run( [
     2054                'gutenberg:verify',
    19622055                'build:js',
    19632056                'build:css',
    19642057                'build:codemirror',
    1965                 'gutenberg-sync',
    1966                 'gutenberg-copy',
     2058                'build:gutenberg',
    19672059                'copy-vendor-scripts',
    19682060                'build:certificates'
     
    19702062        } else {
    19712063            grunt.task.run( [
     2064                'gutenberg:verify',
    19722065                'build:certificates',
    19732066                'build:files',
     
    19752068                'build:css',
    19762069                'build:codemirror',
    1977                 'gutenberg-sync',
    1978                 'gutenberg-copy',
     2070                'build:gutenberg',
    19792071                'copy-vendor-scripts',
    19802072                'replace:source-maps',
  • trunk/package.json

    r61870 r61873  
    88    },
    99    "gutenberg": {
    10         "ref": "9b8144036fa5faf75de43d4502ff9809fcf689ad"
     10        "sha": "9b8144036fa5faf75de43d4502ff9809fcf689ad",
     11        "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build"
    1112    },
    1213    "engines": {
     
    112113    },
    113114    "scripts": {
    114         "postinstall": "npm run gutenberg:sync && npm run gutenberg:copy -- --dev",
     115        "postinstall": "npm run gutenberg:download",
    115116        "build": "grunt build",
    116117        "build:dev": "grunt build --dev",
     118        "build:gutenberg": "grunt build:gutenberg",
    117119        "dev": "grunt watch --dev",
    118120        "test": "grunt test",
     
    138140        "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js",
    139141        "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan",
    140         "gutenberg:checkout": "node tools/gutenberg/checkout-gutenberg.js",
    141         "gutenberg:build": "node tools/gutenberg/build-gutenberg.js",
    142         "gutenberg:copy": "node tools/gutenberg/copy-gutenberg-build.js",
    143         "gutenberg:sync": "node tools/gutenberg/sync-gutenberg.js",
     142        "gutenberg:copy": "node tools/gutenberg/copy.js",
     143        "gutenberg:download": "node tools/gutenberg/download.js",
    144144        "vendor:copy": "node tools/vendors/copy-vendors.js",
    145145        "sync-gutenberg-packages": "grunt sync-gutenberg-packages",
  • trunk/tests/phpstan/base.neon

    r61808 r61873  
    106106            - ../../src/wp-includes/ms-deprecated.php
    107107            - ../../src/wp-includes/pluggable-deprecated.php
    108             # These files are sourced by wordpress/gutenberg in `tools/release/sync-stable-blocks.js`.
     108            # These files are autogenerated by tools/gutenberg/copy.js.
    109109            - ../../src/wp-includes/blocks
    110110            # Third-party libraries.
  • trunk/tools/gutenberg/copy.js

    r61872 r61873  
    1010 */
    1111
     12const child_process = require( 'child_process' );
    1213const fs = require( 'fs' );
    1314const path = require( 'path' );
    1415const json2php = require( 'json2php' );
    15 const glob = require( 'glob' );
    16 
    17 // Paths
     16
     17// Paths.
    1818const rootDir = path.resolve( __dirname, '../..' );
    1919const gutenbergDir = path.join( rootDir, 'gutenberg' );
    2020const gutenbergBuildDir = path.join( gutenbergDir, 'build' );
    21 const gutenbergPackagesDir = path.join( gutenbergDir, 'packages' );
    22 
    23 // Determine build target from command line argument (--dev or --build-dir)
    24 // Default to 'src' for development
     21
     22/*
     23 * Determine build target from command line argument (--dev or --build-dir).
     24 * Default to 'src' for development.
     25 */
    2526const args = process.argv.slice( 2 );
    2627const buildDirArg = args.find( ( arg ) => arg.startsWith( '--build-dir=' ) );
     
    3839 */
    3940const COPY_CONFIG = {
    40     // PHP infrastructure files (to wp-includes/build/)
     41    // PHP infrastructure files (to wp-includes/build/).
    4142    phpInfrastructure: {
    4243        destination: 'build',
     
    4546    },
    4647
    47     // JavaScript packages (to wp-includes/js/dist/)
     48    // JavaScript packages (to wp-includes/js/dist/).
    4849    scripts: {
    4950        source: 'scripts',
    5051        destination: 'js/dist',
    51         copyDirectories: true, // Copy subdirectories
    52         patterns: [ '*.js' ],
    53         // Rename vendors/ to vendor/ when copying
     52        copyDirectories: true,
     53        // Rename vendors/ to vendor/ when copying.
    5454        directoryRenames: {
    5555            vendors: 'vendor',
     
    5757    },
    5858
    59     // Script modules (to wp-includes/js/dist/script-modules/)
     59    // Script modules (to wp-includes/js/dist/script-modules/).
    6060    modules: {
    6161        source: 'modules',
    6262        destination: 'js/dist/script-modules',
    63         copyAll: true,
    6463    },
    6564
    66     // Styles (to wp-includes/css/dist/)
     65    // Styles (to wp-includes/css/dist/).
    6766    styles: {
    6867        source: 'styles',
    6968        destination: 'css/dist',
    70         copyAll: true,
    7169    },
    7270
    73     // Blocks (to wp-includes/blocks/)
    74     // Unified configuration for all block types
     71    /*
     72     * Blocks (to wp-includes/blocks/).
     73     * Unified configuration for all block types.
     74     */
    7575    blocks: {
    7676        destination: 'blocks',
    7777        sources: [
    7878            {
    79                 // Block library blocks
     79                // Block library blocks.
    8080                name: 'block-library',
    8181                scripts: 'scripts/block-library',
    8282                styles: 'styles/block-library',
    83                 php: 'block-library/src',
     83                php: 'scripts/block-library',
    8484            },
    8585            {
    86                 // Widget blocks
     86                // Widget blocks.
    8787                name: 'widgets',
    8888                scripts: 'scripts/widgets/blocks',
    8989                styles: 'styles/widgets',
    90                 php: 'widgets/src/blocks',
     90                php: 'scripts/widgets/blocks',
    9191            },
    9292        ],
    9393    },
    9494
    95     // Theme JSON files (from Gutenberg lib directory)
     95    // Theme JSON files (from Gutenberg lib directory).
    9696    themeJson: {
    9797        files: [
     
    102102    },
    103103
    104     // Specific files to copy to wp-includes/$destination
     104    // Specific files to copy to wp-includes/$destination.
    105105    wpIncludes: [
    106106        {
     
    116116
    117117/**
     118 * Given a path to a PHP file which returns a single value, converts that
     119 * value into a native JavaScript value (limited by JSON serialization).
     120 *
     121 * @throws Error when PHP source file unable to be read, or PHP is unavailable.
     122 *
     123 * @param {string} phpFilepath Absolute path of PHP file returning a single value.
     124 * @return {Object|Array} JavaScript representation of value from input file.
     125 */
     126function readReturnedValueFromPHPFile( phpFilepath ) {
     127    const results = child_process.spawnSync(
     128        'php',
     129        [ '-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;' ],
     130        {
     131            encoding: 'utf8',
     132            input: phpFilepath,
     133        }
     134    );
     135
     136    switch ( results.status ) {
     137        case 0:
     138            return JSON.parse( results.stdout );
     139
     140        case 1:
     141            throw new Error( `Could not read PHP source file: '${ phpFilepath }'` );
     142
     143        case 2:
     144            throw new Error( `PHP source file did not return value when imported: '${ phpFilepath }'` );
     145
     146        case 3:
     147            throw new Error( `Could not serialize PHP source value into JSON: '${ phpFilepath }'` );
     148    }
     149
     150    throw new Error( `Unknown error while reading PHP source file: '${ phpFilepath }'` );
     151}
     152
     153/**
    118154 * Check if a block is experimental by reading its block.json.
    119155 *
     
    159195
    160196        if ( entry.isDirectory() ) {
    161             // Check if this directory is an experimental block
     197            // Check if this directory is an experimental block.
    162198            if ( options.excludeExperimental ) {
    163199                const blockJsonPath = path.join( srcPath, 'block.json' );
     
    169205            copyDirectory( srcPath, destPath, transform, options );
    170206        } else {
    171             // Skip source map files (.map) — these are not useful in Core
     207            // Skip source map files (.map) — these are not useful in Core,
    172208            // and the sourceMappingURL references are already stripped from JS files.
    173209            if ( /\.map$/.test( entry.name ) ) {
     
    175211            }
    176212
    177             // Skip non-minified VIPS files — they are ~10MB of inlined WASM
     213            // Skip non-minified VIPS files — they are ~10MB of inlined WASM,
    178214            // with no debugging value over the minified versions.
    179215            if (
     
    184220            }
    185221
    186             // Skip PHP files if excludePHP is true
     222            // Skip PHP files if excludePHP is true.
    187223            if ( options.excludePHP && /\.php$/.test( entry.name ) ) {
    188224                continue;
     
    191227            let content = fs.readFileSync( srcPath );
    192228
    193             // Apply transformation if provided and file is text
     229            // Apply transformation if provided and file is text.
    194230            if ( transform && /\.(php|js|css)$/.test( entry.name ) ) {
    195231                try {
     
    224260        const scriptsSrc = path.join( gutenbergBuildDir, source.scripts );
    225261        const stylesSrc = path.join( gutenbergBuildDir, source.styles );
    226         const phpSrc = path.join( gutenbergPackagesDir, source.php );
     262        const phpSrc = path.join( gutenbergBuildDir, source.php );
    227263
    228264        if ( ! fs.existsSync( scriptsSrc ) ) {
     
    230266        }
    231267
    232         // Get all block directories from the scripts source
     268        // Get all block directories from the scripts source.
    233269        const blockDirs = fs
    234270            .readdirSync( scriptsSrc, { withFileTypes: true } )
     
    237273
    238274        for ( const blockName of blockDirs ) {
    239             // Skip experimental blocks
     275            // Skip experimental blocks.
    240276            const blockJsonPath = path.join(
    241277                scriptsSrc,
     
    258294                    {
    259295                        recursive: true,
    260                         // Skip PHP, copied from packages
     296                        // Skip PHP, copied from build in steps 3 & 4.
    261297                        filter: f => ! f.endsWith( '.php' ),
    262298                    }
     
    278314            }
    279315
    280             // 3. Copy PHP from packages
    281             const blockPhpSrc = path.join( phpSrc, blockName, 'index.php' );
     316            // 3. Copy PHP from build
     317            const blockPhpSrc = path.join( phpSrc, `${ blockName }.php` );
     318            const phpDest = path.join(
     319                wpIncludesDir,
     320                config.destination,
     321                `${ blockName }.php`
     322            );
    282323            if ( fs.existsSync( blockPhpSrc ) ) {
    283                 const phpDest = path.join(
    284                     wpIncludesDir,
    285                     config.destination,
    286                     `${ blockName }.php`
    287                 );
    288                 const content = fs.readFileSync( blockPhpSrc, 'utf8' );
    289                 fs.writeFileSync( phpDest, content );
    290             }
    291 
    292             // 4. Copy PHP subdirectories from packages (e.g., shared/helpers.php)
     324                fs.copyFileSync( blockPhpSrc, phpDest );
     325            }
     326
     327            // 4. Copy PHP subdirectories from build (e.g., navigation-link/shared/*.php)
    293328            const blockPhpDir = path.join( phpSrc, blockName );
    294329            if ( fs.existsSync( blockPhpDir ) ) {
     
    303338                            );
    304339                        }
    305                         // Copy PHP files, but skip root index.php (handled by step 3)
     340                        // Copy PHP files, but skip root index.php (handled by step 3).
    306341                        return src.endsWith( '.php' ) && src !== rootIndex;
    307342                    },
     
    346381            } else if ( entry.name.endsWith( '.min.asset.php' ) ) {
    347382                const relativePath = path.relative( baseDir, fullPath );
    348                 // Normalize path separators to forward slashes for cross-platform consistency
     383                // Normalize path separators to forward slashes for cross-platform consistency.
    349384                const normalizedPath = relativePath
    350385                    .split( path.sep )
     
    357392
    358393                try {
    359                     // Read and parse the PHP asset file
    360                     const phpContent = fs.readFileSync( fullPath, 'utf8' );
    361                     // Extract the array from PHP: <?php return array(...);
    362                     const match = phpContent.match(
    363                         /return\s+array\(([\s\S]*?)\);/
    364                     );
    365                     if ( match ) {
    366                         // Parse PHP array to JavaScript object
    367                         const assetData = parsePHPArray( match[ 1 ] );
    368                         assetsMin[ jsPathMin ] = assetData;
    369                         assetsRegular[ jsPathRegular ] = assetData;
    370                     }
     394                    const assetData = readReturnedValueFromPHPFile( fullPath );
     395                    assetsMin[ jsPathMin ] = assetData;
     396                    assetsRegular[ jsPathRegular ] = assetData;
    371397                } catch ( error ) {
    372398                    console.error(
     
    381407    processDirectory( modulesDir, modulesDir );
    382408
    383     // Generate both minified and non-minified PHP files using json2php
     409    // Generate both minified and non-minified PHP files using json2php.
    384410    const phpContentMin =
    385411        '<?php return ' +
     
    450476
    451477        try {
    452             // Read and parse the PHP asset file
    453             const phpContent = fs.readFileSync( assetFile, 'utf8' );
    454             // Extract the array from PHP: <?php return array(...);
    455             const match = phpContent.match( /return\s+array\(([\s\S]*?)\);/ );
    456             if ( match ) {
    457                 // Parse PHP array to JavaScript object
    458                 const assetData = parsePHPArray( match[ 1 ] );
    459 
    460                 // For regular scripts, use dependencies as-is.
    461                 if ( ! assetData.dependencies ) {
    462                     assetData.dependencies = [];
    463                 }
    464 
    465                 // Create entries for both minified and non-minified versions
    466                 const jsPathMin = `${ entry.name }.min.js`;
    467                 const jsPathRegular = `${ entry.name }.js`;
    468 
    469                 assetsMin[ jsPathMin ] = assetData;
    470                 assetsRegular[ jsPathRegular ] = assetData;
    471             }
     478            const assetData = readReturnedValueFromPHPFile( assetFile );
     479
     480            // For regular scripts, use dependencies as-is.
     481            if ( ! assetData.dependencies ) {
     482                assetData.dependencies = [];
     483            }
     484
     485            // Create entries for both minified and non-minified versions.
     486            const jsPathMin = `${ entry.name }.min.js`;
     487            const jsPathRegular = `${ entry.name }.js`;
     488
     489            assetsMin[ jsPathMin ] = assetData;
     490            assetsRegular[ jsPathRegular ] = assetData;
    472491        } catch ( error ) {
    473492            console.error(
     
    478497    }
    479498
    480     // Generate both minified and non-minified PHP files using json2php
     499    // Generate both minified and non-minified PHP files using json2php.
    481500    const phpContentMin =
    482501        '<?php return ' +
     
    525544    const staticBlocks = [];
    526545
    527     // Widget blocks to exclude (from @wordpress/widgets package)
     546    // Widget blocks to exclude (from @wordpress/widgets package).
    528547    const widgetBlocks = [ 'legacy-widget', 'widget-group' ];
    529548
     
    540559        }
    541560
    542         // Skip widget blocks
     561        // Skip widget blocks.
    543562        if ( widgetBlocks.includes( entry.name ) ) {
    544563            continue;
     
    549568        const phpFilePath = path.join( blocksDir, `${ entry.name }.php` );
    550569
    551         // Skip if block.json doesn't exist
     570        // Skip if block.json doesn't exist.
    552571        if ( ! fs.existsSync( blockJsonPath ) ) {
    553572            continue;
    554573        }
    555574
    556         // Check if it's experimental
     575        // Check if it's experimental.
    557576        if ( isExperimentalBlock( blockJsonPath ) ) {
    558577            continue;
    559578        }
    560579
    561         // Determine if it's dynamic (has a PHP file)
     580        // Determine if it's dynamic (has a PHP file).
    562581        if ( fs.existsSync( phpFilePath ) ) {
    563582            dynamicBlocks.push( entry.name );
     
    567586    }
    568587
    569     // Sort alphabetically
     588    // Sort alphabetically.
    570589    dynamicBlocks.sort();
    571590    staticBlocks.sort();
    572591
    573     // Generate require-dynamic-blocks.php
     592    // Generate require-dynamic-blocks.php.
    574593    const dynamicContent = `<?php
    575594
    576 // This file was autogenerated by tools/gutenberg/copy-gutenberg-build.js, do not change manually!
     595// This file was autogenerated by tools/gutenberg/copy.js, do not change manually!
    577596// Requires files for dynamic blocks necessary for core blocks registration.
    578597${ dynamicBlocks
     
    588607    );
    589608
    590     // Generate require-static-blocks.php
     609    // Generate require-static-blocks.php.
    591610    const staticContent = `<?php
    592611
    593 // This file was autogenerated by tools/gutenberg/copy-gutenberg-build.js, do not change manually!
     612// This file was autogenerated by tools/gutenberg/copy.js, do not change manually!
    594613// Returns folder names for static blocks necessary for core blocks registration.
    595614return array(
     
    646665    }
    647666
    648     // Generate the PHP file content using json2php for consistent formatting
     667    // Generate the PHP file content using json2php for consistent formatting.
    649668    const phpContent =
    650669        '<?php return ' +
     
    667686
    668687/**
    669  * Parse PHP array syntax to JavaScript object.
    670  * Uses a simple but effective approach for the specific format in asset files.
    671  *
    672  * @param {string} phpArrayContent - PHP array content (without outer 'array(' and ')').
    673  * @return {Object|Array} Parsed JavaScript object or array.
    674  */
    675 function parsePHPArray( phpArrayContent ) {
    676     phpArrayContent = phpArrayContent.trim();
    677 
    678     // First, extract all nested array() blocks and replace with placeholders
    679     const nestedArrays = [];
    680     let content = phpArrayContent;
    681     let depth = 0;
    682     let inString = false;
    683     let stringChar = '';
    684     let currentArray = '';
    685     let arrayStart = -1;
    686 
    687     for ( let i = 0; i < content.length; i++ ) {
    688         const char = content[ i ];
    689 
    690         // Track strings
    691         if (
    692             ( char === "'" || char === '"' ) &&
    693             ( i === 0 || content[ i - 1 ] !== '\\' )
    694         ) {
    695             if ( ! inString ) {
    696                 inString = true;
    697                 stringChar = char;
    698             } else if ( char === stringChar ) {
    699                 inString = false;
    700             }
    701         }
    702 
    703         if ( ! inString ) {
    704             // Look for array( keyword
    705             if ( content.substring( i, i + 6 ) === 'array(' ) {
    706                 if ( depth === 0 ) {
    707                     arrayStart = i;
    708                     currentArray = '';
    709                 }
    710                 depth++;
    711                 if ( depth > 1 ) {
    712                     currentArray += 'array(';
    713                 }
    714                 i += 5; // Skip 'array('
    715                 continue;
    716             }
    717 
    718             if ( depth > 0 ) {
    719                 if ( char === '(' ) {
    720                     depth++;
    721                     currentArray += char;
    722                 } else if ( char === ')' ) {
    723                     depth--;
    724                     if ( depth === 0 ) {
    725                         // Found complete nested array
    726                         const placeholder = `__ARRAY_${ nestedArrays.length }__`;
    727                         nestedArrays.push( currentArray );
    728                         content =
    729                             content.substring( 0, arrayStart ) +
    730                             placeholder +
    731                             content.substring( i + 1 );
    732                         i = arrayStart + placeholder.length - 1;
    733                         currentArray = '';
    734                     } else {
    735                         currentArray += char;
    736                     }
    737                 } else {
    738                     currentArray += char;
    739                 }
    740             }
    741         } else if ( depth > 0 ) {
    742             currentArray += char;
    743         }
    744     }
    745 
    746     // Now parse the simplified content
    747     const result = {};
    748     const values = [];
    749     let isAssociative = false;
    750 
    751     // Split by top-level commas
    752     const parts = [];
    753     depth = 0;
    754     inString = false;
    755     let currentPart = '';
    756 
    757     for ( let i = 0; i < content.length; i++ ) {
    758         const char = content[ i ];
    759 
    760         if (
    761             ( char === "'" || char === '"' ) &&
    762             ( i === 0 || content[ i - 1 ] !== '\\' )
    763         ) {
    764             inString = ! inString;
    765         }
    766 
    767         if ( ! inString && char === ',' && depth === 0 ) {
    768             parts.push( currentPart.trim() );
    769             currentPart = '';
    770         } else {
    771             currentPart += char;
    772             if ( ! inString ) {
    773                 if ( char === '(' ) depth++;
    774                 if ( char === ')' ) depth--;
    775             }
    776         }
    777     }
    778     if ( currentPart.trim() ) {
    779         parts.push( currentPart.trim() );
    780     }
    781 
    782     // Parse each part
    783     for ( const part of parts ) {
    784         const arrowMatch = part.match( /^(.+?)\s*=>\s*(.+)$/ );
    785 
    786         if ( arrowMatch ) {
    787             isAssociative = true;
    788             let key = arrowMatch[ 1 ].trim().replace( /^['"]|['"]$/g, '' );
    789             let value = arrowMatch[ 2 ].trim();
    790 
    791             // Replace placeholders
    792             while ( value.match( /__ARRAY_(\d+)__/ ) ) {
    793                 value = value.replace( /__ARRAY_(\d+)__/, ( match, index ) => {
    794                     return 'array(' + nestedArrays[ parseInt( index ) ] + ')';
    795                 } );
    796             }
    797 
    798             result[ key ] = parseValue( value );
    799         } else {
    800             // No arrow, indexed array
    801             let value = part;
    802 
    803             // Replace placeholders
    804             while ( value.match( /__ARRAY_(\d+)__/ ) ) {
    805                 value = value.replace( /__ARRAY_(\d+)__/, ( match, index ) => {
    806                     return 'array(' + nestedArrays[ parseInt( index ) ] + ')';
    807                 } );
    808             }
    809 
    810             values.push( parseValue( value ) );
    811         }
    812     }
    813 
    814     return isAssociative ? result : values;
    815 
    816     /**
    817      * Parse a single value.
    818      *
    819      * @param {string} value - The value string to parse.
    820      * @return {*} Parsed value.
    821      */
    822     function parseValue( value ) {
    823         value = value.trim();
    824 
    825         if ( value.startsWith( 'array(' ) && value.endsWith( ')' ) ) {
    826             return parsePHPArray( value.substring( 6, value.length - 1 ) );
    827         } else if ( value.match( /^['"].*['"]$/ ) ) {
    828             return value.substring( 1, value.length - 1 );
    829         } else if ( value === 'true' ) {
    830             return true;
    831         } else if ( value === 'false' ) {
    832             return false;
    833         } else if ( ! isNaN( value ) && value !== '' ) {
    834             return parseInt( value, 10 );
    835         }
    836         return value;
    837     }
    838 }
    839 
    840 /**
    841  * Transform PHP file contents to work in Core.
    842  *
    843  * @param {string} content  - File content.
    844  * @return {string} Transformed content.
    845  */
    846 function transformPHPContent( content ) {
    847     let transformed = content;
    848 
    849     // Fix boot module asset file path for Core's different directory structure
    850     // FROM: __DIR__ . '/../../modules/boot/index.min.asset.php'
    851     // TO:   ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php'
    852     // This is needed because Core copies modules to a different location than the plugin structure
    853     transformed = transformed.replace(
    854         /__DIR__\s*\.\s*['"]\/\.\.\/\.\.\/modules\/boot\/index\.min\.asset\.php['"]/g,
    855         "ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php'"
    856     );
    857 
    858     return transformed;
    859 }
    860 
    861 /**
    862  * Transform manifest.php to remove gutenberg text domain.
    863  *
    864  * @param {string} content - File content.
    865  * @return {string} Transformed content.
    866  */
    867 function transformManifestPHP( content ) {
    868     // Remove 'gutenberg' text domain from _x() calls
    869     // FROM: _x( '...', 'icon label', 'gutenberg' )
    870     // TO:   _x( '...', 'icon label' )
    871     const transformedContent = content.replace(
    872         /_x\(\s*([^,]+),\s*([^,]+),\s*['"]gutenberg['"]\s*\)/g,
    873         '_x( $1, $2 )'
    874     );
    875     return transformedContent;
    876 }
    877 
    878 /**
    879688 * Main execution function.
    880689 */
    881690async function main() {
    882     console.log( '🔍 Checking Gutenberg build...' );
    883     console.log( `   Build target: ${ buildTarget }/` );
    884 
    885     // Verify Gutenberg build exists
     691    console.log( `📦 Copying Gutenberg build to ${ buildTarget }/...` );
     692
    886693    if ( ! fs.existsSync( gutenbergBuildDir ) ) {
    887694        console.error( '❌ Gutenberg build directory not found' );
    888         console.error( '   Run: node tools/gutenberg/build-gutenberg.js' );
     695        console.error( '   Run: npm run grunt gutenberg:download' );
    889696        process.exit( 1 );
    890697    }
    891698
    892     console.log( '✅ Gutenberg build found' );
    893 
    894     // 1. Copy PHP infrastructure
    895     console.log( '\n📦 Copying PHP infrastructure...' );
    896     const phpConfig = COPY_CONFIG.phpInfrastructure;
    897     const phpDest = path.join( wpIncludesDir, phpConfig.destination );
    898 
    899     // Copy PHP files
    900     for ( const file of phpConfig.files ) {
    901         const src = path.join( gutenbergBuildDir, file );
    902         const dest = path.join( phpDest, file );
    903 
    904         if ( fs.existsSync( src ) ) {
    905             fs.mkdirSync( path.dirname( dest ), { recursive: true } );
    906             let content = fs.readFileSync( src, 'utf8' );
    907             content = transformPHPContent( content );
    908             fs.writeFileSync( dest, content );
    909             console.log( `   ✅ ${ file }` );
    910         } else {
    911             console.log(
    912                 `   ⚠️  ${ file } not found (may not exist in this Gutenberg version)`
    913             );
    914         }
    915     }
    916 
    917     // Copy PHP directories
    918     for ( const dir of phpConfig.directories ) {
    919         const src = path.join( gutenbergBuildDir, dir );
    920         const dest = path.join( phpDest, dir );
    921 
    922         if ( fs.existsSync( src ) ) {
    923             console.log( `   📁 Copying ${ dir }/...` );
    924             copyDirectory( src, dest, transformPHPContent );
    925             console.log( `   ✅ ${ dir }/ copied` );
    926         }
    927     }
    928 
    929     // 2. Copy JavaScript packages
     699    // 1. Copy JavaScript packages.
    930700    console.log( '\n📦 Copying JavaScript packages...' );
    931701    const scriptsConfig = COPY_CONFIG.scripts;
     
    933703    const scriptsDest = path.join( wpIncludesDir, scriptsConfig.destination );
    934704
    935     // Transform function to remove source map comments from all JS files.
    936     // Only match actual source map comments at the start of a line (possibly
    937     // with whitespace), not occurrences inside string literals.
    938     const removeSourceMaps = ( content ) => {
    939         return content.replace( /^\s*\/\/# sourceMappingURL=.*$/gm, '' ).trimEnd();
    940     };
    941 
    942705    if ( fs.existsSync( scriptsSrc ) ) {
    943706        const entries = fs.readdirSync( scriptsSrc, { withFileTypes: true } );
     
    947710
    948711            if ( entry.isDirectory() ) {
    949                 // Check if this should be copied as a directory (like vendors/)
     712                // Check if this should be copied as a directory (like vendors/).
    950713                if (
    951714                    scriptsConfig.copyDirectories &&
     
    953716                    scriptsConfig.directoryRenames[ entry.name ]
    954717                ) {
    955                     // Copy special directories with rename (vendors/ → vendor/)
    956                     // Only copy react-jsx-runtime from vendors (react and react-dom come from Core's node_modules)
     718                    /*
     719                     * Copy special directories with rename (vendors/ → vendor/).
     720                     * Only copy react-jsx-runtime from vendors (react and react-dom come from Core's node_modules).
     721                     */
    957722                    const destName =
    958723                        scriptsConfig.directoryRenames[ entry.name ];
     
    960725
    961726                    if ( entry.name === 'vendors' ) {
    962                         // Only copy react-jsx-runtime files, skip react and react-dom
     727                        // Only copy react-jsx-runtime files, skip react and react-dom.
    963728                        const vendorFiles = fs.readdirSync( src );
    964729                        let copiedCount = 0;
     
    972737                                const destFile = path.join( dest, file );
    973738
    974                                 let content = fs.readFileSync(
    975                                     srcFile,
    976                                     'utf8'
    977                                 );
    978                                 content = removeSourceMaps( content );
    979                                 fs.writeFileSync( destFile, content );
     739                                fs.copyFileSync( srcFile, destFile );
    980740                                copiedCount++;
    981741                            }
     
    985745                        );
    986746                    } else {
    987                         // Copy other special directories normally
    988                         copyDirectory( src, dest, removeSourceMaps );
     747                        // Copy other special directories normally.
     748                        copyDirectory( src, dest );
    989749                        console.log(
    990750                            `   ✅ ${ entry.name }/ → ${ destName }/`
     
    992752                    }
    993753                } else {
    994                     // Flatten package structure: package-name/index.js → package-name.js
    995                     // This matches Core's expected file structure
     754                    /*
     755                     * Flatten package structure: package-name/index.js → package-name.js.
     756                     * This matches Core's expected file structure.
     757                     */
    996758                    const packageFiles = fs.readdirSync( src );
    997759
     
    1001763                        ) {
    1002764                            const srcFile = path.join( src, file );
    1003                             // Replace 'index.' with 'package-name.'
     765                            // Replace 'index.' with 'package-name.'.
    1004766                            const destFile = file.replace(
    1005767                                /^index\./,
     
    1012774                            } );
    1013775
    1014                             // Apply source map removal for .js files
    1015                             if ( file.endsWith( '.js' ) ) {
    1016                                 let content = fs.readFileSync(
    1017                                     srcFile,
    1018                                     'utf8'
    1019                                 );
    1020                                 content = removeSourceMaps( content );
    1021                                 fs.writeFileSync( destPath, content );
    1022                             } else {
    1023                                 // Copy other files as-is (.min.asset.php)
    1024                                 fs.copyFileSync( srcFile, destPath );
    1025                             }
     776                            fs.copyFileSync( srcFile, destPath );
    1026777                        }
    1027778                    }
    1028779                }
    1029780            } else if ( entry.isFile() && entry.name.endsWith( '.js' ) ) {
    1030                 // Copy root-level JS files
     781                // Copy root-level JS files.
    1031782                const dest = path.join( scriptsDest, entry.name );
    1032783                fs.mkdirSync( path.dirname( dest ), { recursive: true } );
    1033 
    1034                 let content = fs.readFileSync( src, 'utf8' );
    1035                 content = removeSourceMaps( content );
    1036                 fs.writeFileSync( dest, content );
     784                fs.copyFileSync( src, dest );
    1037785            }
    1038786        }
     
    1041789    }
    1042790
    1043     // 3. Copy script modules
    1044     console.log( '\n📦 Copying script modules...' );
    1045     const modulesConfig = COPY_CONFIG.modules;
    1046     const modulesSrc = path.join( gutenbergBuildDir, modulesConfig.source );
    1047     const modulesDest = path.join( wpIncludesDir, modulesConfig.destination );
    1048 
    1049     if ( fs.existsSync( modulesSrc ) ) {
    1050         // Use the same source map removal transform
    1051         copyDirectory( modulesSrc, modulesDest, removeSourceMaps );
    1052         console.log( '   ✅ Script modules copied' );
    1053     }
    1054 
    1055     // 4. Copy styles
    1056     console.log( '\n📦 Copying styles...' );
    1057     const stylesConfig = COPY_CONFIG.styles;
    1058     const stylesSrc = path.join( gutenbergBuildDir, stylesConfig.source );
    1059     const stylesDest = path.join( wpIncludesDir, stylesConfig.destination );
    1060 
    1061     if ( fs.existsSync( stylesSrc ) ) {
    1062         copyDirectory( stylesSrc, stylesDest );
    1063         console.log( '   ✅ Styles copied' );
    1064     }
    1065 
    1066     // 5. Copy blocks (unified: scripts, styles, PHP, JSON)
     791    // 2. Copy blocks (unified: scripts, styles, PHP, JSON).
    1067792    console.log( '\n📦 Copying blocks...' );
    1068     const blocksDest = path.join(
    1069         wpIncludesDir,
    1070         COPY_CONFIG.blocks.destination
    1071     );
    1072793    copyBlockAssets( COPY_CONFIG.blocks );
    1073794
    1074     // 6. Copy theme JSON files (from Gutenberg lib directory)
    1075     console.log( '\n📦 Copying theme JSON files...' );
    1076     const themeJsonConfig = COPY_CONFIG.themeJson;
    1077     const gutenbergLibDir = path.join( gutenbergDir, 'lib' );
    1078 
    1079     for ( const fileMap of themeJsonConfig.files ) {
    1080         const src = path.join( gutenbergLibDir, fileMap.from );
    1081         const dest = path.join( wpIncludesDir, fileMap.to );
    1082 
    1083         if ( fs.existsSync( src ) ) {
    1084             let content = fs.readFileSync( src, 'utf8' );
    1085 
    1086             if ( themeJsonConfig.transform && fileMap.from === 'theme.json' ) {
    1087                 // Transform schema URL for Core
    1088                 content = content.replace(
    1089                     '"$schema": "../schemas/json/theme.json"',
    1090                     '"$schema": "https://schemas.wp.org/trunk/theme.json"'
    1091                 );
    1092             }
    1093 
    1094             fs.writeFileSync( dest, content );
    1095             console.log( `   ✅ ${ fileMap.to }` );
    1096         } else {
    1097             console.log( `   ⚠️  Not found: ${ fileMap.from }` );
    1098         }
    1099     }
    1100 
    1101     // Copy remaining files to wp-includes
    1102     console.log( '\n📦 Copying remaining files to wp-includes...' );
    1103     for ( const fileMap of COPY_CONFIG.wpIncludes ) {
    1104         const dest = path.join( wpIncludesDir, fileMap.destination );
    1105         fs.mkdirSync( dest, { recursive: true } );
    1106         for ( const src of fileMap.files ) {
    1107             const matches = glob.sync( path.join( gutenbergDir, src ) );
    1108             if ( ! matches.length ) {
    1109                 throw new Error( `No files found matching '${ src }'` );
    1110             }
    1111             for ( const match of matches ) {
    1112                 const destPath = path.join( dest, path.basename( match ) );
    1113                 // Apply transformation for manifest.php to remove gutenberg text domain
    1114                 if ( path.basename( match ) === 'manifest.php' ) {
    1115                     let content = fs.readFileSync( match, 'utf8' );
    1116                     content = transformManifestPHP( content );
    1117                     fs.writeFileSync( destPath, content );
    1118                 } else {
    1119                     fs.copyFileSync( match, destPath );
    1120                 }
    1121             }
    1122         }
    1123     }
    1124 
    1125     // 7. Generate script-modules-packages.min.php from individual asset files
    1126     console.log( '\n📦 Generating script-modules-packages.min.php...' );
     795    // 3. Generate script-modules-packages.php from individual asset files.
     796    console.log( '\n📦 Generating script-modules-packages.php...' );
    1127797    generateScriptModulesPackages();
    1128798
    1129     // 8. Generate script-loader-packages.min.php
    1130     console.log( '\n📦 Generating script-loader-packages.min.php...' );
     799    // 4. Generate script-loader-packages.php.
     800    console.log( '\n📦 Generating script-loader-packages.php...' );
    1131801    generateScriptLoaderPackages();
    1132802
    1133     // 9. Generate require-dynamic-blocks.php and require-static-blocks.php
     803    // 5. Generate require-dynamic-blocks.php and require-static-blocks.php.
    1134804    console.log( '\n📦 Generating block registration files...' );
    1135805    generateBlockRegistrationFiles();
    1136806
    1137     // 10. Generate blocks-json.php from block.json files
     807    // 6. Generate blocks-json.php from block.json files.
    1138808    console.log( '\n📦 Generating blocks-json.php...' );
    1139809    generateBlocksJson();
    1140810
    1141     // Summary
    1142811    console.log( '\n✅ Copy complete!' );
    1143     console.log( '\n📊 Summary:' );
    1144     console.log( `   PHP infrastructure: ${ phpDest }` );
    1145     console.log( `   JavaScript: ${ scriptsDest }` );
    1146     console.log( `   Script modules: ${ modulesDest }` );
    1147     console.log( `   Styles: ${ stylesDest }` );
    1148     console.log( `   Blocks: ${ blocksDest }` );
    1149812}
    1150813
    1151 // Run main function
     814// Run main function.
    1152815main().catch( ( error ) => {
    1153816    console.error( '❌ Unexpected error:', error );
  • trunk/webpack.config.js

    r61543 r61873  
    1515    // Only building Core-specific media files and development scripts.
    1616    // Blocks, packages, script modules, and vendors are now sourced from
    17     // the Gutenberg build (see tools/gutenberg/copy-gutenberg-build.js).
     17    // the Gutenberg build (see tools/gutenberg/copy.js).
    1818    // Note: developmentConfig returns an array of configs, so we spread it.
    1919    const config = [
Note: See TracChangeset for help on using the changeset viewer.