From 49d0dd310250c7232e8bde4e586ec0d3818d0011 Mon Sep 17 00:00:00 2001 From: KarunyaChavan Date: Fri, 9 Oct 2026 14:25:46 +0530 Subject: [PATCH 1/2] fix(export): register template parts with area and title in theme.json. When exporting or saving templates to theme files, parts/*.html files were written to disk, but corresponding entries in theme.json under templateParts were never registered. This caused custom template parts or plugin-provided parts (e.g., WooCommerce mini-cart) to lose their area classification and human-friendly titles, falling back to 'uncategorized' and raw slug names. - Add CBT_Theme_Templates::add_template_parts_to_theme_json_data() to merge exported template parts with their name, title, and area, deduplicating by slug and preserving existing entries. - Add CBT_Theme_Templates::update_theme_json_template_parts() to write templateParts directly to local theme.json during add_templates_to_local() and invalidate theme JSON resolver caches. - Update CBT_Theme_JSON_Resolver::export_theme_data() to merge exported template parts into theme.json during ZIP exports and theme cloning. --- includes/create-theme/resolver_additions.php | 8 ++ includes/create-theme/theme-templates.php | 87 ++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/includes/create-theme/resolver_additions.php b/includes/create-theme/resolver_additions.php index 3de2cc25..521af530 100644 --- a/includes/create-theme/resolver_additions.php +++ b/includes/create-theme/resolver_additions.php @@ -1,5 +1,7 @@ get_data(); + if ( class_exists( 'CBT_Theme_Templates' ) ) { + $parts_export_type = ( 'variation' === $content ) ? 'user' : $content; + $exported_parts = CBT_Theme_Templates::get_theme_templates( $parts_export_type )->parts; + $data = CBT_Theme_Templates::add_template_parts_to_theme_json_data( $data, $exported_parts ); + } + // Add the schema. if ( empty( $schema ) ) { global $wp_version; diff --git a/includes/create-theme/theme-templates.php b/includes/create-theme/theme-templates.php index 88f30572..aaf57bf8 100644 --- a/includes/create-theme/theme-templates.php +++ b/includes/create-theme/theme-templates.php @@ -300,6 +300,93 @@ public static function add_templates_to_local( $export_type, $path = null, $slug ); } } + + if ( ! empty( $theme_templates->parts ) ) { + self::update_theme_json_template_parts( $base_dir, $theme_templates->parts ); + } + } + + /** + * Ensure each exported template part has a templateParts entry with name, title, and area + * in theme.json, unless an entry for that name already exists. + * + * @param array $theme_json_data theme.json data array. + * @param array $parts Array of WP_Block_Template objects. + * @return array Updated theme.json data array. + */ + public static function add_template_parts_to_theme_json_data( array $theme_json_data, array $parts ) { + if ( empty( $parts ) ) { + return $theme_json_data; + } + + $existing_by_name = array(); + if ( isset( $theme_json_data['templateParts'] ) && is_array( $theme_json_data['templateParts'] ) ) { + foreach ( $theme_json_data['templateParts'] as $index => $part_entry ) { + if ( is_array( $part_entry ) && isset( $part_entry['name'] ) ) { + $existing_by_name[ $part_entry['name'] ] = $index; + } + } + } else { + $theme_json_data['templateParts'] = array(); + } + + foreach ( $parts as $part ) { + if ( ! is_object( $part ) || empty( $part->slug ) ) { + continue; + } + + $slug = sanitize_key( $part->slug ); + $title = ! empty( $part->title ) ? sanitize_text_field( $part->title ) : $slug; + $area = ! empty( $part->area ) ? sanitize_key( $part->area ) : 'uncategorized'; + + if ( isset( $existing_by_name[ $slug ] ) ) { + $idx = $existing_by_name[ $slug ]; + if ( empty( $theme_json_data['templateParts'][ $idx ]['area'] ) && ! empty( $area ) ) { + $theme_json_data['templateParts'][ $idx ]['area'] = $area; + } + if ( empty( $theme_json_data['templateParts'][ $idx ]['title'] ) && ! empty( $title ) ) { + $theme_json_data['templateParts'][ $idx ]['title'] = $title; + } + continue; + } + + $theme_json_data['templateParts'][] = array( + 'area' => $area, + 'name' => $slug, + 'title' => $title, + ); + $existing_by_name[ $slug ] = count( $theme_json_data['templateParts'] ) - 1; + } + + return $theme_json_data; + } + + /** + * Update theme.json in the specified base directory to include entries for template parts. + * + * @param string $base_dir Base directory of the theme. + * @param array $parts Array of template part objects. + */ + public static function update_theme_json_template_parts( $base_dir, array $parts ) { + $theme_json_path = $base_dir . DIRECTORY_SEPARATOR . 'theme.json'; + if ( ! file_exists( $theme_json_path ) ) { + return; + } + + $theme_json_raw = file_get_contents( $theme_json_path ); + $theme_json = json_decode( $theme_json_raw, true ); + if ( ! is_array( $theme_json ) ) { + return; + } + + $updated_theme_json = self::add_template_parts_to_theme_json_data( $theme_json, $parts ); + if ( $updated_theme_json !== $theme_json ) { + file_put_contents( $theme_json_path, CBT_Theme_JSON_Resolver::stringify( $updated_theme_json ) ); + if ( class_exists( 'CBT_Theme_JSON_Resolver' ) ) { + CBT_Theme_JSON_Resolver::clean_cached_data(); + } + wp_get_theme()->cache_delete(); + } } /** From 2c8c332251a7f5f68dfaa4b03e9abd4d80407b56 Mon Sep 17 00:00:00 2001 From: KarunyaChavan Date: Fri, 9 Oct 2026 14:26:25 +0530 Subject: [PATCH 2/2] test(templates): add unit tests for template parts registration in theme.json. - test_add_template_parts_to_theme_json_data_adds_new_parts: Verifies new template parts are added with name, title, and area. - test_add_template_parts_to_theme_json_data_does_not_duplicate_existing: Ensures existing templateParts entries in theme.json are preserved without duplicates. - test_add_template_parts_to_theme_json_data_defaults_area_and_title: Verifies safe fallbacks to slug and 'uncategorized' when title or area are missing. - test_update_theme_json_template_parts_updates_file: Verifies file-level theme.json update and persistence on disk. - test_export_theme_data_includes_template_parts: Verifies integration with CBT_Theme_JSON_Resolver::export_theme_data(). --- tests/test-theme-templates.php | 134 +++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/tests/test-theme-templates.php b/tests/test-theme-templates.php index 0a81da24..2415e244 100644 --- a/tests/test-theme-templates.php +++ b/tests/test-theme-templates.php @@ -415,4 +415,138 @@ public function test_localize_cover_repeated_background_inline_style() { $new_template = CBT_Theme_Media::make_template_images_local( $template ); $this->assertStringContainsString( '/assets/images/pattern.webp', $new_template->content ); } + + /** + * Ensure that add_template_parts_to_theme_json_data adds new template parts to theme.json data. + */ + public function test_add_template_parts_to_theme_json_data_adds_new_parts() { + $part = new stdClass(); + $part->slug = 'custom-header'; + $part->title = 'Custom Header'; + $part->area = 'header'; + + $input_data = array( + 'version' => 3, + ); + $output_data = CBT_Theme_Templates::add_template_parts_to_theme_json_data( $input_data, array( $part ) ); + + $this->assertArrayHasKey( 'templateParts', $output_data ); + $this->assertCount( 1, $output_data['templateParts'] ); + $this->assertSame( 'custom-header', $output_data['templateParts'][0]['name'] ); + $this->assertSame( 'Custom Header', $output_data['templateParts'][0]['title'] ); + $this->assertSame( 'header', $output_data['templateParts'][0]['area'] ); + } + + /** + * Ensure that add_template_parts_to_theme_json_data does not overwrite or duplicate existing template parts. + */ + public function test_add_template_parts_to_theme_json_data_does_not_duplicate_existing() { + $part = new stdClass(); + $part->slug = 'header'; + $part->title = 'New Header Title'; + $part->area = 'header'; + + $input_data = array( + 'version' => 3, + 'templateParts' => array( + array( + 'area' => 'header', + 'name' => 'header', + 'title' => 'Original Header', + ), + ), + ); + $output_data = CBT_Theme_Templates::add_template_parts_to_theme_json_data( $input_data, array( $part ) ); + + $this->assertCount( 1, $output_data['templateParts'] ); + $this->assertSame( 'Original Header', $output_data['templateParts'][0]['title'] ); + } + + /** + * Ensure that add_template_parts_to_theme_json_data falls back to uncategorized and slug title when missing. + */ + public function test_add_template_parts_to_theme_json_data_defaults_area_and_title() { + $part = new stdClass(); + $part->slug = 'custom-sidebar'; + + $input_data = array( 'version' => 3 ); + $output_data = CBT_Theme_Templates::add_template_parts_to_theme_json_data( $input_data, array( $part ) ); + + $this->assertCount( 1, $output_data['templateParts'] ); + $this->assertSame( 'custom-sidebar', $output_data['templateParts'][0]['name'] ); + $this->assertSame( 'custom-sidebar', $output_data['templateParts'][0]['title'] ); + $this->assertSame( 'uncategorized', $output_data['templateParts'][0]['area'] ); + } + + /** + * Ensure that update_theme_json_template_parts persists template parts directly into theme.json on disk. + */ + public function test_update_theme_json_template_parts_updates_file() { + $temp_dir = get_temp_dir() . 'cbt-test-theme-' . uniqid(); + wp_mkdir_p( $temp_dir ); + + $initial_json = array( + 'version' => 3, + 'templateParts' => array( + array( + 'area' => 'header', + 'name' => 'header', + 'title' => 'Header', + ), + ), + ); + file_put_contents( $temp_dir . '/theme.json', wp_json_encode( $initial_json ) ); + + $part = new stdClass(); + $part->slug = 'custom-footer'; + $part->title = 'Custom Footer'; + $part->area = 'footer'; + + CBT_Theme_Templates::update_theme_json_template_parts( $temp_dir, array( $part ) ); + + $saved_content = json_decode( file_get_contents( $temp_dir . '/theme.json' ), true ); + + $this->assertCount( 2, $saved_content['templateParts'] ); + $this->assertSame( 'header', $saved_content['templateParts'][0]['name'] ); + $this->assertSame( 'custom-footer', $saved_content['templateParts'][1]['name'] ); + $this->assertSame( 'footer', $saved_content['templateParts'][1]['area'] ); + + // Clean up. + unlink( $temp_dir . '/theme.json' ); + rmdir( $temp_dir ); + } + + /** + * Ensure that export_theme_data includes user-created template parts under templateParts in theme.json. + */ + public function test_export_theme_data_includes_template_parts() { + if ( ! class_exists( 'CBT_Theme_JSON_Resolver' ) ) { + $this->markTestSkipped( 'CBT_Theme_JSON_Resolver is not loaded.' ); + } + + $post_id = wp_insert_post( + array( + 'post_type' => 'wp_template_part', + 'post_name' => 'my-custom-header', + 'post_title' => 'My Custom Header', + 'post_status' => 'publish', + 'post_content' => '

Content

', + ) + ); + wp_set_post_terms( $post_id, get_stylesheet(), 'wp_theme' ); + wp_set_post_terms( $post_id, 'header', 'wp_template_part_area' ); + + try { + $json_str = CBT_Theme_JSON_Resolver::export_theme_data( 'all' ); + $data = json_decode( $json_str, true ); + + $this->assertIsArray( $data ); + $this->assertArrayHasKey( 'templateParts', $data ); + $names = array_column( $data['templateParts'], 'name' ); + $this->assertContains( 'my-custom-header', $names ); + } finally { + wp_delete_post( $post_id, true ); + CBT_Theme_JSON_Resolver::clean_cached_data(); + } + } }