diff --git a/.github/workflows/test-old-branches.yml b/.github/workflows/test-old-branches.yml index 74f9c2d43d54c..ae651290d9cd8 100644 --- a/.github/workflows/test-old-branches.yml +++ b/.github/workflows/test-old-branches.yml @@ -7,7 +7,7 @@ on: - trunk paths: - '.github/workflows/test-old-branches.yml' - - '.github/workflows/reusable-phpunit-tests-v[1-2].yml' + - '.github/workflows/reusable-phpunit-tests-v[1-3].yml' # Run twice a month on the 1st and 15th at 00:00 UTC. schedule: - cron: '0 0 1 * *' 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/composer.json b/composer.json index 5c016d37316c1..6500e7ccbf8af 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "php": ">=7.4" }, "suggest": { - "ext-dom": "*" + "ext-dom": "*", + "ext-mysqli": "*" }, "require-dev": { "composer/ca-bundle": "1.5.12", 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/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index f48b8048c76e5..53933f0ac28a2 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -934,6 +934,7 @@ a#remove-post-thumbnail:hover, border-top: 1px solid #dcdcde; background: #f6f7f7; display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; } diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index 75e046ef8ffa7..71d3953179218 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -1357,7 +1357,7 @@ public function get_test_dotorg_communication() { $result['description'] .= sprintf( '

%s

', sprintf( - '%s %s', + '%s %s', /* translators: Hidden accessibility text. */ __( 'Error' ), sprintf( diff --git a/src/wp-admin/includes/class-wp-upgrader.php b/src/wp-admin/includes/class-wp-upgrader.php index 695ce50bf0d7e..ba27113ff73de 100644 --- a/src/wp-admin/includes/class-wp-upgrader.php +++ b/src/wp-admin/includes/class-wp-upgrader.php @@ -528,7 +528,7 @@ public function install_package( $args = array() ) { /* * Give the upgrade an additional 300 seconds (5 minutes) to ensure the install * doesn't prematurely timeout having used up the maximum script execution time - * upacking and downloading in WP_Upgrader->run(). + * downloading and unpacking in WP_Upgrader->run(). */ if ( function_exists( 'set_time_limit' ) ) { set_time_limit( 300 ); diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 95084b1db0576..935c613d561e9 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -316,7 +316,7 @@ function wp_create_image_subsizes( $file, $attachment_id ) { } if ( $scale_down ) { - // Resize the image. This will also convet it if needed. + // Resize the image. This will also convert it if needed. $resized = $editor->resize( $threshold, $threshold ); } elseif ( $convert ) { // The image will be converted (if possible) when saved. diff --git a/src/wp-includes/class-wpdb.php b/src/wp-includes/class-wpdb.php index e5300e6d75122..e9d7f986d5801 100644 --- a/src/wp-includes/class-wpdb.php +++ b/src/wp-includes/class-wpdb.php @@ -2758,7 +2758,7 @@ public function update( $table, $data, $where, $format = null, $where_format = n * @param string[]|string $where_format Optional. An array of formats to be mapped to each of the values in $where. * If string, that format will be used for all of the items in $where. * A format is one of '%d', '%f', '%s' (integer, float, string). - * If omitted, all values in $data will be treated as strings unless otherwise + * If omitted, all values in $where will be treated as strings unless otherwise * specified in wpdb::$field_types. Default null. * @return int|false The number of rows deleted, or false on error. */ @@ -3019,12 +3019,16 @@ protected function process_field_lengths( $data, $table ) { * the value in the column and row specified is returned. If $query is null, * the value in the specified column and row from the previous SQL result is returned. * + * Returns null both on failure and when the matched cell value is an empty + * string. To distinguish the two cases, check {@see self::$last_error}. + * * @since 0.71 * * @param string|null $query Optional. SQL query. Defaults to null, use the result from the previous query. * @param int $x Optional. Column of value to return. Indexed from 0. Default 0. * @param int $y Optional. Row of value to return. Indexed from 0. Default 0. - * @return string|null Database query result (as string), or null on failure. + * @return string|null Database query result (as string), or null on failure or when the value is an empty string. + * @phpstan-return non-empty-string|null */ public function get_var( $query = null, $x = 0, $y = 0 ) { $this->func_call = "\$db->get_var(\"$query\", $x, $y)"; @@ -3039,6 +3043,14 @@ public function get_var( $query = null, $x = 0, $y = 0 ) { // Extract var out of cached results based on x,y vals. if ( ! empty( $this->last_result[ $y ] ) ) { + /** + * Column values. + * + * These are returned from the database as strings, or null for SQL NULL, but get_object_vars() types the + * property values as mixed. + * + * @var list $values + */ $values = array_values( get_object_vars( $this->last_result[ $y ] ) ); } @@ -3059,6 +3071,24 @@ public function get_var( $query = null, $x = 0, $y = 0 ) { * respectively. Default OBJECT. * @param int $y Optional. Row to return. Indexed from 0. Default 0. * @return array|object|null Database query result in format specified by $output or null on failure. + * @phpstan-param 'OBJECT'|'ARRAY_A'|'ARRAY_N' $output + * @phpstan-return ( + * $query is non-falsy-string + * ? ( + * $output is 'OBJECT' + * ? stdClass|null + * : ( + * $output is 'ARRAY_A' + * ? array|null + * : ( + * $output is 'ARRAY_N' + * ? list|null + * : null + * ) + * ) + * ) + * : null + * ) */ public function get_row( $query = null, $output = OBJECT, $y = 0 ) { $this->func_call = "\$db->get_row(\"$query\",$output,$y)"; @@ -3104,6 +3134,7 @@ public function get_row( $query = null, $output = OBJECT, $y = 0 ) { * @param string|null $query Optional. SQL query. Defaults to previous query. * @param int $x Optional. Column to return. Indexed from 0. Default 0. * @return array Database query result. Array indexed from 0 by SQL result row number. + * @phpstan-return list */ public function get_col( $query = null, $x = 0 ) { if ( $query ) { @@ -3118,7 +3149,7 @@ public function get_col( $query = null, $x = 0 ) { // Extract the column values. if ( $this->last_result ) { for ( $i = 0, $j = count( $this->last_result ); $i < $j; $i++ ) { - $new_array[ $i ] = $this->get_var( null, $x, $i ); + $new_array[] = $this->get_var( null, $x, $i ); } } return $new_array; @@ -3129,18 +3160,47 @@ public function get_col( $query = null, $x = 0 ) { * * Executes a SQL query and returns the entire SQL result. * + * Returns an empty array when no rows match or when the database + * reports an error for the query. Returns null when $query is empty, + * when $output is not one of the recognized constants, or when the + * query cannot run because the connection is not ready. + * * @since 0.71 * - * @param string $query SQL query. - * @param string $output Optional. Any of ARRAY_A | ARRAY_N | OBJECT | OBJECT_K constants. - * With one of the first three, return an array of rows indexed - * from 0 by SQL result row number. Each row is an associative array - * (column => value, ...), a numerically indexed array (0 => value, ...), - * or an object ( ->column = value ), respectively. With OBJECT_K, - * return an associative array of row objects keyed by the value - * of each row's first column's value. Duplicate keys are discarded. - * Default OBJECT. - * @return array|object|null Database query results. + * @param string|null $query SQL query. + * @param string $output Optional. Any of ARRAY_A | ARRAY_N | OBJECT | OBJECT_K constants. + * With one of the first three, return an array of rows indexed + * from 0 by SQL result row number. Each row is an associative array + * (column => value, ...), a numerically indexed array (0 => value, ...), + * or an object ( ->column = value ), respectively. With OBJECT_K, + * return an associative array of row objects keyed by the value + * of each row's first column's value. Duplicate keys are discarded. + * Default OBJECT. + * @return array|null Database query results. Empty array when no rows match + * or on database error. Null when $query is empty, when + * $output is invalid, or when the connection is not ready. + * @phpstan-param 'OBJECT'|'OBJECT_K'|'ARRAY_A'|'ARRAY_N' $output + * @phpstan-return ( + * $query is non-falsy-string + * ? ( + * $output is 'OBJECT' + * ? list|null + * : ( + * $output is 'OBJECT_K' + * ? array + * : ( + * $output is 'ARRAY_A' + * ? list> + * : ( + * $output is 'ARRAY_N' + * ? list> + * : null + * ) + * ) + * ) + * ) + * : null + * ) */ public function get_results( $query = null, $output = OBJECT ) { $this->func_call = "\$db->get_results(\"$query\", $output)"; @@ -3167,7 +3227,15 @@ public function get_results( $query = null, $output = OBJECT ) { if ( $this->last_result ) { foreach ( $this->last_result as $row ) { $var_by_ref = get_object_vars( $row ); - $key = array_shift( $var_by_ref ); + /** + * The first column's value is used as the key. + * + * A SQL NULL value surfaces as null here, so coerce it to an empty string to avoid the deprecated + * use of null as an array offset (PHP 8.5+). + * + * @var array-key $key + */ + $key = array_shift( $var_by_ref ) ?? ''; if ( ! isset( $new_array[ $key ] ) ) { $new_array[ $key ] = $row; } diff --git a/src/wp-includes/compat-utf8.php b/src/wp-includes/compat-utf8.php index e1cab36ea3244..5fa8cde158789 100644 --- a/src/wp-includes/compat-utf8.php +++ b/src/wp-includes/compat-utf8.php @@ -65,7 +65,7 @@ function _wp_scan_utf8( string $bytes, int &$at, int &$invalid_length, ?int $max "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" . " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f", $i, - $end - $i + min( $end - $i, $max_count - $count ) ); if ( $count + $ascii_byte_count >= $max_count ) { diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index d7d2ff3fed89a..355d9f8a1ec37 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5290,20 +5290,41 @@ function _wp_to_kebab_case( $input_string ) { /** * Determines if the variable is a numeric-indexed array. * + * Note! This answers a different question than {@see array_is_list()} and is + * more flexible to handle situations where some numeric array indices + * have been removed. A numeric-indexed array is only a “list” when the + * array keys form a contiguous range from zero to the highest key. + * + * Example: + * + * true === wp_is_numeric_array( array( 1, 2, 3, 4 ) ); + * false === wp_is_numeric_array( array( 'name' => 'WordPress' ) ); + * + * // All-numeric keys vs. list. + * $above_two = array_filter( array( 1, 2, 8, 9 ), fn ( $v ) => $v > 2 ); + * $above_two === array( '2' => 8, '3' => 9 ); + * true === wp_is_numeric_array( $above_two ); + * false === array_is_list( $above_two ); + * * @since 4.4.0 * * @param mixed $data Variable to check. * @return bool Whether the variable is a list. + * + * @phpstan-assert-if-true array $data */ -function wp_is_numeric_array( $data ) { +function wp_is_numeric_array( $data ): bool { if ( ! is_array( $data ) ) { return false; } - $keys = array_keys( $data ); - $string_keys = array_filter( $keys, 'is_string' ); + foreach ( $data as $key => $value ) { + if ( is_string( $key ) ) { + return false; + } + } - return count( $string_keys ) === 0; + return true; } /** diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index a45d1697ea40a..0edb36d9c80bb 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -109,6 +109,8 @@ ), 'br' => array(), 'button' => array( + 'command' => true, + 'commandfor' => true, 'disabled' => true, 'name' => true, 'type' => true, @@ -2579,6 +2581,7 @@ function safecss_filter_attr( $css, $deprecated = '' ) { * Filters the list of allowed CSS attributes. * * @since 2.8.1 + * @since 7.1.0 Added support for SVG presentation attributes. * * @param string[] $attr Array of allowed CSS attributes. */ @@ -2737,6 +2740,71 @@ function safecss_filter_attr( $css, $deprecated = '' ) { 'aspect-ratio', 'container-type', + 'fill', + 'fill-opacity', + 'fill-rule', + + 'stroke', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke-width', + + 'color-interpolation', + 'color-interpolation-filters', + 'paint-order', + 'stop-color', + 'stop-opacity', + 'flood-color', + 'flood-opacity', + 'lighting-color', + + 'marker', + 'marker-end', + 'marker-mid', + 'marker-start', + + 'clip-path', + 'clip-rule', + 'mask', + 'mask-type', + + 'cx', + 'cy', + 'r', + 'rx', + 'ry', + 'x', + 'y', + 'd', + + 'alignment-baseline', + 'baseline-shift', + 'dominant-baseline', + 'glyph-orientation-horizontal', + 'glyph-orientation-vertical', + 'text-anchor', + 'unicode-bidi', + 'word-spacing', + + 'font-size-adjust', + 'font-stretch', + + 'color-rendering', + 'image-rendering', + 'shape-rendering', + 'text-rendering', + 'vector-effect', + + 'transform', + 'transform-origin', + + 'pointer-events', + 'visibility', + // Custom CSS properties. '--*', ) diff --git a/tests/phpunit/tests/compat/wpUtf8CodePointSpan.php b/tests/phpunit/tests/compat/wpUtf8CodePointSpan.php new file mode 100644 index 0000000000000..4bb5e0c272223 --- /dev/null +++ b/tests/phpunit/tests/compat/wpUtf8CodePointSpan.php @@ -0,0 +1,103 @@ +assertSame( + $expected_span, + _wp_utf8_codepoint_span( $text, $byte_offset, $max_code_points, $found_code_points ), + 'Should have found the expected byte span.' + ); + + $this->assertSame( + $expected_found, + $found_code_points, + 'Should have reported the expected number of code points.' + ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_codepoint_spans() { + $long_ascii_run = str_repeat( 'a', 1024 ); + + return array( + 'zero code point budget' => array( + 'abcdef', + 0, + 0, + 0, + 0, + ), + 'long ASCII run at start' => array( + $long_ascii_run, + 0, + 5, + 5, + 5, + ), + 'long ASCII run from non-zero offset' => array( + "zz{$long_ascii_run}", + 2, + 5, + 5, + 5, + ), + 'multibyte character before the boundary' => array( + "ab\u{1F170}cd", + 0, + 2, + 2, + 2, + ), + 'multibyte character at the boundary' => array( + "ab\u{1F170}cd", + 0, + 3, + strlen( "ab\u{1F170}" ), + 3, + ), + 'invalid span after the boundary' => array( + "ab\xF0\x9Fzz", + 0, + 2, + 2, + 2, + ), + 'invalid span at the boundary' => array( + "ab\xF0\x9Fzz", + 0, + 3, + 4, + 3, + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpIsNumericArray.php b/tests/phpunit/tests/functions/wpIsNumericArray.php index 4eeab0af81f2a..4bf7b0cc1695b 100644 --- a/tests/phpunit/tests/functions/wpIsNumericArray.php +++ b/tests/phpunit/tests/functions/wpIsNumericArray.php @@ -26,27 +26,34 @@ public function test_wp_is_numeric_array( $input, $expected ) { */ public function data_wp_is_numeric_array() { return array( - 'no index' => array( + 'no index' => array( 'test_array' => array( 'www', 'eee' ), 'expected' => true, ), - 'text index' => array( + 'text index' => array( 'test_array' => array( 'www' => 'eee' ), 'expected' => false, ), - 'numeric index' => array( + 'numeric index' => array( 'test_array' => array( 99 => 'eee' ), 'expected' => true, ), - '- numeric index' => array( + 'filtered list (missing numeric keys)' => array( + 'test_array' => array_filter( + array( 1, 12, 13, 15, 16, 17, 20 ), + fn ( $v ) => 0 === $v % 2 + ), + 'expected' => true, + ), + '- numeric index' => array( 'test_array' => array( -11 => 'eee' ), 'expected' => true, ), - 'numeric string index' => array( + 'numeric string index' => array( 'test_array' => array( '11' => 'eee' ), 'expected' => true, ), - 'nested number index' => array( + 'nested number index' => array( 'test_array' => array( 'next' => array( 11 => 'vvv', @@ -54,7 +61,7 @@ public function data_wp_is_numeric_array() { ), 'expected' => false, ), - 'nested string index' => array( + 'nested string index' => array( 'test_array' => array( '11' => array( 'eee' => 'vvv', @@ -62,7 +69,7 @@ public function data_wp_is_numeric_array() { ), 'expected' => true, ), - 'not an array' => array( + 'not an array' => array( 'test_array' => null, 'expected' => false, ), diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php index db507a6b26550..9ed3a45b2d90e 100644 --- a/tests/phpunit/tests/kses.php +++ b/tests/phpunit/tests/kses.php @@ -1000,6 +1000,7 @@ public function test_wp_kses_attr_no_attributes_allowed_with_false() { * @ticket 58551 * @ticket 60132 * @ticket 64414 + * @ticket 65457 * * @dataProvider data_safecss_filter_attr * @@ -1473,6 +1474,43 @@ public function data_safecss_filter_attr() { 'css' => 'display: grid', 'expected' => 'display: grid', ), + // SVG presentation attributes introduced in 7.1.0. + array( + 'css' => 'fill: none', + 'expected' => 'fill: none', + ), + array( + 'css' => 'fill-rule: evenodd', + 'expected' => 'fill-rule: evenodd', + ), + array( + 'css' => 'stroke: red', + 'expected' => 'stroke: red', + ), + array( + 'css' => 'stroke-width: 2', + 'expected' => 'stroke-width: 2', + ), + array( + 'css' => 'stroke-linecap: round', + 'expected' => 'stroke-linecap: round', + ), + array( + 'css' => 'paint-order: stroke', + 'expected' => 'paint-order: stroke', + ), + array( + 'css' => 'vector-effect: non-scaling-stroke', + 'expected' => 'vector-effect: non-scaling-stroke', + ), + array( + 'css' => 'clip-rule: evenodd', + 'expected' => 'clip-rule: evenodd', + ), + array( + 'css' => 'text-anchor: middle', + 'expected' => 'text-anchor: middle', + ), ); } @@ -1890,6 +1928,17 @@ public function test_wp_kses_main_tag_standard_attributes() { $this->assertEqualHTML( $html, wp_kses_post( $html ) ); } + /** + * Test that Invoker Commands API attributes are preserved on buttons in post content. + * + * @ticket 64576 + */ + public function test_wp_kses_button_invoker_command_attributes() { + $html = '
Content
'; + + $this->assertEqualHTML( $html, wp_kses_post( $html ) ); + } + /** * Test that object tags are allowed under limited circumstances. * 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" ]