Commit 67ec574389b for woocommerce
commit 67ec574389bc5c67443fd15fa73b7a8b447b8edb
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date: Wed Nov 11 08:56:55 2026 +0100
[Performance] Introduce lazy loading block registration (inner blocks) (#70735)
This PR introduces lazy loading for cart/mini-cart/checkout inner blocks in order to speedup block initialization (which is the biggest timing block in Woo bootstrap sequence).
diff --git a/plugins/woocommerce/changelog/performance-optimize-blocks-registration b/plugins/woocommerce/changelog/performance-optimize-blocks-registration
new file mode 100644
index 00000000000..6e902bb959d
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-optimize-blocks-registration
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Introduce lazy block registration for inner blocks.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.json
index 525a2ab3711..fe099e1efcc 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.json
@@ -45,6 +45,17 @@
"type": "string"
}
},
+ "styles": [
+ {
+ "name": "without-price",
+ "label": "Hide Price",
+ "isDefault": true
+ },
+ {
+ "name": "with-price",
+ "label": "Show Price"
+ }
+ ],
"parent": [ "woocommerce/checkout-fields-block" ],
"textdomain": "woocommerce",
"$schema": "https://schemas.wp.org/trunk/block.json",
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 97652e2e1e3..601b89d2225 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -47823,12 +47823,6 @@ parameters:
count: 1
path: src/Blocks/BlockTypes/Checkout.php
- -
- message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\CheckoutActionsBlock\:\:initialize\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/Blocks/BlockTypes/CheckoutActionsBlock.php
-
-
message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\CheckoutActionsBlock\:\:register_style_variations\(\) has no return type specified\.$#'
identifier: missingType.return
@@ -50385,12 +50379,6 @@ parameters:
count: 1
path: src/Blocks/BlockTypesController.php
- -
- message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypesController\:\:register_blocks\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/Blocks/BlockTypesController.php
-
-
message: '#^Parameter \#1 \$str of function strtolower expects string, string\|null given\.$#'
identifier: argument.type
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php
index 42383139b01..244e274ea44 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php
@@ -145,8 +145,14 @@ abstract class AbstractBlock {
_doing_it_wrong( __METHOD__, esc_html( 'Block name is required.' ), '4.5.0' );
return false;
}
+
$this->integration_registry->initialize( $this->block_name . '_block' );
- $this->register_block_type();
+
+ // Performance note: if the block is lazily loadable, it's type has been already registered.
+ if ( ! \WP_Block_Type_Registry::get_instance()->is_registered( $this->get_full_block_name() ) ) {
+ $this->register_block_type();
+ }
+
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutActionsBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutActionsBlock.php
index 11511eaa9ba..f2ee53a3e4e 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutActionsBlock.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutActionsBlock.php
@@ -15,35 +15,23 @@ class CheckoutActionsBlock extends AbstractInnerBlock {
/**
* Initialize this block type.
*
- * - Hook into WP lifecycle.
- * - Register the block with WordPress.
+ * @return void
*/
protected function initialize() {
parent::initialize();
- add_action( 'wp_loaded', array( $this, 'register_style_variations' ) );
+ // Backward compatibility: keep hook for child classes (third-parties which might be overriding register_style_variations).
+ if ( static::class !== __CLASS__ ) {
+ add_action( 'wp_loaded', array( $this, 'register_style_variations' ) );
+ }
}
/**
* Register style variations for the block.
+ *
+ * @internal
+ * @deprecated 11.4.0 Style variations are now declared in block.json.
*/
public function register_style_variations() {
- register_block_style(
- $this->get_full_block_name(),
- array(
- 'name' => 'without-price',
- 'label' => __( 'Hide Price', 'woocommerce' ),
- 'is_default' => true,
- )
- );
-
- register_block_style(
- $this->get_full_block_name(),
- array(
- 'name' => 'with-price',
- 'label' => __( 'Show Price', 'woocommerce' ),
- 'is_default' => false,
- )
- );
}
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php
index b2d6e94131b..32da67eabca 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypesController.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php
@@ -46,6 +46,13 @@ final class BlockTypesController {
*/
protected $asset_data_registry;
+ /**
+ * Controller for lazy-loaded blocks.
+ *
+ * @var BlockTypesLazyController
+ */
+ private $lazy_blocks_controller;
+
/**
* Holds the registered blocks that have WooCommerce blocks as their parents.
*
@@ -68,12 +75,15 @@ final class BlockTypesController {
/**
* Constructor.
*
- * @param AssetApi $asset_api Instance of the asset API.
+ * @param AssetApi $asset_api Instance of the asset API.
* @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
*/
public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry ) {
$this->asset_api = $asset_api;
$this->asset_data_registry = $asset_data_registry;
+ // The lazy controller is not extensible as we are in rolling migration phase, hence no constructor injection.
+ $this->lazy_blocks_controller = new BlockTypesLazyController( $asset_api, $asset_data_registry );
+
$this->init();
}
@@ -88,11 +98,15 @@ final class BlockTypesController {
add_action( 'woocommerce_login_form_end', array( $this, 'redirect_to_field' ) );
add_action( 'woocommerce_register_form_end', array( $this, 'redirect_to_field' ) );
add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_legacy_widgets_with_block_equivalent' ) );
- add_filter( 'block_type_metadata', array( $this, 'handle_block_type_metadata' ) );
- add_filter( 'block_type_metadata_settings', array( $this, 'use_single_block_editor_style' ), 10, 2 );
add_filter( 'register_block_type_args', array( $this, 'enqueue_block_style_for_classic_themes' ), 10, 2 );
add_filter( 'block_core_breadcrumbs_post_type_settings', array( $this, 'set_product_breadcrumbs_preferred_taxonomy' ), 10, 3 );
add_filter( 'block_core_breadcrumbs_items', array( $this, 'apply_woocommerce_breadcrumb_filters' ), 10, 1 );
+
+ // Hooks relevant to '\register_block_type_from_metadata' (blocks registration workflows).
+ add_filter( 'block_type_metadata', array( $this, 'handle_block_type_metadata' ) );
+ if ( is_admin() ) {
+ add_filter( 'block_type_metadata_settings', array( $this, 'use_single_block_editor_style' ), 10, 2 );
+ }
}
/**
@@ -136,6 +150,8 @@ final class BlockTypesController {
/**
* Register blocks, hooking up assets and render functions as needed.
+ *
+ * @return void
*/
public function register_blocks() {
if ( self::$register_blocks_has_run ) {
@@ -145,11 +161,10 @@ final class BlockTypesController {
// Set before registering rather than after, so a registration failure is not retried on a later call.
self::$register_blocks_has_run = true;
$this->register_block_metadata();
- $block_types = $this->get_block_types();
-
- foreach ( $block_types as $block_type ) {
- $block_type_class = __NAMESPACE__ . '\\BlockTypes\\' . $block_type;
+ $block_classes = array_map( static fn( $type ) => __NAMESPACE__ . '\\BlockTypes\\' . $type, $this->get_block_types() );
+ $block_classes = array_diff( $block_classes, $this->lazy_blocks_controller->register_blocks( $block_classes ) );
+ foreach ( $block_classes as $block_type_class ) {
new $block_type_class( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry() );
}
}
@@ -388,6 +403,9 @@ final class BlockTypesController {
* @param array $allowed_namespaces List of namespaces.
*/
$allowed_namespaces = array_merge( array( 'woocommerce', 'woocommerce-checkout' ), (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_namespace', array() ) );
+ if ( in_array( $block_namespace, $allowed_namespaces, true ) ) {
+ return true;
+ }
/**
* Filters the list of allowed Block Names
@@ -399,13 +417,11 @@ final class BlockTypesController {
* @param array $allowed_namespaces List of namespaces.
*/
$allowed_blocks = (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_block', array() );
+ if ( ! empty( $allowed_blocks ) && in_array( $block_name, $allowed_blocks, true ) ) {
+ return true;
+ }
- $blocks_with_woo_parents = $this->get_registered_blocks_with_woocommerce_parent();
- $block_has_woo_parent = in_array( $block_name, array_keys( $blocks_with_woo_parents ), true );
- $in_allowed_namespace_list = in_array( $block_namespace, $allowed_namespaces, true );
- $in_allowed_block_list = in_array( $block_name, $allowed_blocks, true );
-
- return $block_has_woo_parent || $in_allowed_block_list || $in_allowed_namespace_list;
+ return ! empty( $block_name ) && isset( $this->get_registered_blocks_with_woocommerce_parent()[ $block_name ] );
}
/**
@@ -416,7 +432,6 @@ final class BlockTypesController {
* @return string
*/
public function add_data_attributes( $content, $block ) {
-
if ( ! is_string( $content ) || ! $this->block_should_have_data_attributes( $block['blockName'] ) ) {
return $content;
}
@@ -680,9 +695,9 @@ final class BlockTypesController {
if ( null === $should_enqueue_block_style_for_classic_themes ) {
$should_enqueue_block_style_for_classic_themes = ! (
is_admin() ||
- wp_is_block_theme() ||
- ( function_exists( 'wp_should_load_block_assets_on_demand' ) && wp_should_load_block_assets_on_demand() ) ||
- wp_should_load_separate_core_block_assets()
+ wp_should_load_separate_core_block_assets() ||
+ wp_should_load_block_assets_on_demand() ||
+ wp_is_block_theme()
);
}
if ( ! $should_enqueue_block_style_for_classic_themes ) {
@@ -728,16 +743,10 @@ final class BlockTypesController {
* @return array Block metadata.
*/
public function handle_block_type_metadata( $metadata ) {
- if ( ! is_array( $metadata ) || ! $this->is_woocommerce_block_metadata( $metadata ) ) {
- return $metadata;
- }
-
- if ( isset( $metadata['version'] ) && '' !== $metadata['version'] ) {
- return $metadata;
+ if ( is_array( $metadata ) && '' === ( $metadata['version'] ?? '' ) && $this->is_woocommerce_block_metadata( $metadata ) ) {
+ $metadata['version'] = $this->asset_api->wc_version;
}
- $metadata['version'] = $this->asset_api->wc_version;
-
return $metadata;
}
@@ -756,18 +765,13 @@ final class BlockTypesController {
* @return array Block settings.
*/
public function use_single_block_editor_style( $settings, $metadata ) {
- if (
- ! BlockEditorUnifiedAssets::is_enabled() ||
- ! is_admin() ||
- ! $this->is_woocommerce_block_metadata( $metadata ) ) {
- return $settings;
+ if ( $this->is_woocommerce_block_metadata( $metadata ) && BlockEditorUnifiedAssets::is_enabled() ) {
+ $settings['style_handles'] = array();
+ $settings['style'] = array();
+ $settings['editor_style_handles'] = array( 'wc-block-library-style' );
+ $settings['editor_style'] = array( 'wc-block-library-style' );
}
- $settings['style_handles'] = array();
- $settings['style'] = array();
- $settings['editor_style_handles'] = array( 'wc-block-library-style' );
- $settings['editor_style'] = array( 'wc-block-library-style' );
-
return $settings;
}
@@ -780,22 +784,16 @@ final class BlockTypesController {
*/
private function is_woocommerce_block_metadata( $metadata ) {
static $blocks_path = null;
-
if ( null === $blocks_path ) {
$resolved_path = realpath( WC_ABSPATH . 'assets/client/blocks' );
- $blocks_path = false === $resolved_path
- ? ''
- : trailingslashit( wp_normalize_path( $resolved_path ) );
+ $blocks_path = false === $resolved_path ? '' : trailingslashit( wp_normalize_path( $resolved_path ) );
}
- if ( '' === $blocks_path || empty( $metadata['file'] ) ) {
- return false;
+ if ( '' !== $blocks_path && ! empty( $metadata['file'] ) ) {
+ return str_starts_with( wp_normalize_path( $metadata['file'] ), $blocks_path );
}
- return str_starts_with(
- wp_normalize_path( $metadata['file'] ),
- $blocks_path
- );
+ return false;
}
/**
diff --git a/plugins/woocommerce/src/Blocks/BlockTypesLazyController.php b/plugins/woocommerce/src/Blocks/BlockTypesLazyController.php
new file mode 100644
index 00000000000..24018a2de37
--- /dev/null
+++ b/plugins/woocommerce/src/Blocks/BlockTypesLazyController.php
@@ -0,0 +1,188 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Blocks;
+
+use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
+use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
+use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
+use Automattic\WooCommerce\Internal\Features\BlockEditorUnifiedAssets;
+
+/**
+ * Registers side-effect-free blocks deferring class instantiation to render time.
+ *
+ * @internal
+ */
+final class BlockTypesLazyController {
+ /**
+ * Instance of the asset API.
+ *
+ * @var AssetApi
+ */
+ private AssetApi $asset_api;
+
+ /**
+ * Instance of the asset data registry.
+ *
+ * @var AssetDataRegistry
+ */
+ private AssetDataRegistry $asset_data_registry;
+
+ /**
+ * Processed block classes registry (class FQN → full block name).
+ *
+ * @var array<string,string>
+ */
+ private array $registered_blocks = array();
+
+ /**
+ * Constructor.
+ *
+ * @param AssetApi $asset_api Instance of the asset API.
+ * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
+ */
+ public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry ) {
+ $this->asset_api = $asset_api;
+ $this->asset_data_registry = $asset_data_registry;
+ }
+
+ /**
+ * Register side-effect-free blocks (class instantiation deferred to render time).
+ *
+ * @param string[] $whitelist Block class FQNs intended for initialization.
+ * @return string[]
+ */
+ public function register_blocks( array $whitelist ): array {
+ if ( ! empty( $this->registered_blocks ) ) {
+ return array_keys( array_filter( $this->registered_blocks, array( \WP_Block_Type_Registry::get_instance(), 'is_registered' ) ) );
+ }
+
+ $whitelisted = array_flip( $whitelist );
+ $unified_editor_assets = BlockEditorUnifiedAssets::is_enabled();
+
+ $inner_block_groups = array(
+ $this->get_cart_inner_blocks(),
+ $this->get_mini_cart_inner_blocks(),
+ $this->get_checkout_inner_blocks(),
+ );
+
+ foreach ( $inner_block_groups as $blocks_group ) {
+ foreach ( $blocks_group as $block_name => $block_class ) {
+ if ( isset( $whitelisted[ $block_class ] ) ) {
+ $registered_block_type = register_block_type_from_metadata(
+ $this->asset_api->generate_block_metadata_path( $block_name, 'inner-blocks/' ),
+ array(
+ // Please, don't extend this list further! We expect everyone to lean onto block.json.
+ 'render_callback' => $this->create_lazy_render_callback( $block_class ),
+ 'editor_style' => $unified_editor_assets ? 'wc-block-library-style' : 'wc-blocks-editor-style',
+ )
+ );
+ if ( $registered_block_type instanceof \WP_Block_Type ) {
+ $this->registered_blocks[ $block_class ] = $registered_block_type->name;
+ }
+ }
+ }
+ }
+
+ return array_keys( $this->registered_blocks );
+ }
+
+ /**
+ * Create a render callback that lazily instantiates the block class on it's first render.
+ *
+ * @param string $block_type_class Fully-qualified class name.
+ * @return callable
+ */
+ private function create_lazy_render_callback( string $block_type_class ): callable {
+ $asset_api = $this->asset_api;
+ $asset_data_registry = $this->asset_data_registry;
+
+ return static function ( $attributes = array(), $content = '', $block = null ) use ( $block_type_class, $asset_api, $asset_data_registry ) {
+ static $instance = null;
+ $instance = $instance ?? new $block_type_class( $asset_api, $asset_data_registry, new IntegrationRegistry() );
+ return $instance->render_callback( $attributes, $content, $block );
+ };
+ }
+
+ /**
+ * Side effect free and stateless Cart inner blocks.
+ *
+ * @return array<string,string>
+ */
+ private function get_cart_inner_blocks(): array {
+ // ProceedToCheckoutBlock: excluded — overrides enqueue_data() with editor-visible checkout page URL.
+ return array(
+ 'cart-accepted-payment-methods-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartAcceptedPaymentMethodsBlock::class,
+ 'cart-cross-sells-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartCrossSellsBlock::class,
+ 'cart-cross-sells-products-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartCrossSellsProductsBlock::class,
+ 'cart-express-payment-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartExpressPaymentBlock::class,
+ 'cart-items-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartItemsBlock::class,
+ 'cart-line-items-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartLineItemsBlock::class,
+ 'cart-order-summary-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryBlock::class,
+ 'cart-order-summary-coupon-form-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryCouponFormBlock::class,
+ 'cart-order-summary-discount-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryDiscountBlock::class,
+ 'cart-order-summary-fee-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryFeeBlock::class,
+ 'cart-order-summary-heading-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryHeadingBlock::class,
+ 'cart-order-summary-shipping-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryShippingBlock::class,
+ 'cart-order-summary-subtotal-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummarySubtotalBlock::class,
+ 'cart-order-summary-taxes-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryTaxesBlock::class,
+ 'cart-order-summary-totals-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryTotalsBlock::class,
+ 'cart-totals-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartTotalsBlock::class,
+ 'empty-cart-block' => \Automattic\WooCommerce\Blocks\BlockTypes\EmptyCartBlock::class,
+ 'filled-cart-block' => \Automattic\WooCommerce\Blocks\BlockTypes\FilledCartBlock::class,
+ );
+ }
+
+ /**
+ * Side effect free and stateless Checkout inner blocks.
+ *
+ * @return array<string,string>
+ */
+ private function get_checkout_inner_blocks(): array {
+ // CheckoutExpressPaymentBlock: excluded — overrides initialize() with save_post hook for express payment attribute sync.
+ return array(
+ 'checkout-actions-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutActionsBlock::class,
+ 'checkout-additional-information-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutAdditionalInformationBlock::class,
+ 'checkout-billing-address-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutBillingAddressBlock::class,
+ 'checkout-contact-information-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutContactInformationBlock::class,
+ 'checkout-fields-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutFieldsBlock::class,
+ 'checkout-order-note-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderNoteBlock::class,
+ 'checkout-order-summary-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryBlock::class,
+ 'checkout-order-summary-cart-items-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryCartItemsBlock::class,
+ 'checkout-order-summary-coupon-form-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryCouponFormBlock::class,
+ 'checkout-order-summary-discount-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryDiscountBlock::class,
+ 'checkout-order-summary-fee-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryFeeBlock::class,
+ 'checkout-order-summary-shipping-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryShippingBlock::class,
+ 'checkout-order-summary-subtotal-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummarySubtotalBlock::class,
+ 'checkout-order-summary-taxes-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryTaxesBlock::class,
+ 'checkout-order-summary-totals-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryTotalsBlock::class,
+ 'checkout-payment-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutPaymentBlock::class,
+ 'checkout-pickup-options-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutPickupOptionsBlock::class,
+ 'checkout-shipping-address-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutShippingAddressBlock::class,
+ 'checkout-shipping-method-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutShippingMethodBlock::class,
+ 'checkout-shipping-methods-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutShippingMethodsBlock::class,
+ 'checkout-terms-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutTermsBlock::class,
+ 'checkout-totals-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutTotalsBlock::class,
+ );
+ }
+
+ /**
+ * Side effect free and stateless MiniCart inner blocks.
+ *
+ * @return array<string,string>
+ */
+ private function get_mini_cart_inner_blocks(): array {
+ // MiniCartFooterBlock: excluded — overrides enqueue_data() with editor-visible footer description.
+ return array(
+ 'empty-mini-cart-contents-block' => \Automattic\WooCommerce\Blocks\BlockTypes\EmptyMiniCartContentsBlock::class,
+ 'filled-mini-cart-contents-block' => \Automattic\WooCommerce\Blocks\BlockTypes\FilledMiniCartContentsBlock::class,
+ 'mini-cart-cart-button-block' => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartCartButtonBlock::class,
+ 'mini-cart-checkout-button-block' => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartCheckoutButtonBlock::class,
+ 'mini-cart-items-block' => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartItemsBlock::class,
+ 'mini-cart-products-table-block' => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartProductsTableBlock::class,
+ 'mini-cart-shopping-button-block' => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartShoppingButtonBlock::class,
+ 'mini-cart-title-block' => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartTitleBlock::class,
+ 'mini-cart-title-items-counter-block' => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartTitleItemsCounterBlock::class,
+ 'mini-cart-title-label-block' => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartTitleLabelBlock::class,
+ );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypesLazyControllerTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypesLazyControllerTest.php
new file mode 100644
index 00000000000..7e42f85c156
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypesLazyControllerTest.php
@@ -0,0 +1,83 @@
+<?php declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks;
+
+use Automattic\WooCommerce\Blocks\Assets\Api;
+use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
+use Automattic\WooCommerce\Blocks\BlockTypes\CartItemsBlock;
+use Automattic\WooCommerce\Blocks\BlockTypes\CheckoutActionsBlock;
+use Automattic\WooCommerce\Blocks\BlockTypes\EmptyMiniCartContentsBlock;
+use Automattic\WooCommerce\Blocks\BlockTypesLazyController;
+use Automattic\WooCommerce\Blocks\Package;
+use Automattic\WooCommerce\Tests\Blocks\Helpers\BlockTypeRegistryTrait;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the BlockTypesLazyController class.
+ */
+final class BlockTypesLazyControllerTest extends WC_Unit_Test_Case {
+ use BlockTypeRegistryTrait;
+
+ /**
+ * The blocks map for test-cases to operate on.
+ *
+ * @var array<string,string>
+ */
+ private const TEST_BLOCKS = array(
+ CartItemsBlock::class => 'woocommerce/cart-items-block',
+ CheckoutActionsBlock::class => 'woocommerce/checkout-actions-block',
+ EmptyMiniCartContentsBlock::class => 'woocommerce/empty-mini-cart-contents-block',
+ );
+
+ /**
+ * Controller instance.
+ *
+ * @var BlockTypesLazyController
+ */
+ private $sut;
+
+ /**
+ * Set up test fixtures.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->sut = new BlockTypesLazyController(
+ Package::container()->get( Api::class ),
+ Package::container()->get( AssetDataRegistry::class )
+ );
+
+ $registry = \WP_Block_Type_Registry::get_instance();
+ foreach ( self::TEST_BLOCKS as $name ) {
+ $this->block_types_before_test[ $name ] = $registry->get_registered( $name );
+ if ( $registry->is_registered( $name ) ) {
+ $registry->unregister( $name );
+ }
+ }
+ }
+
+ /**
+ * @testdox Should register whitelisted blocks and detect WP registry removal.
+ */
+ public function test_registers_whitelisted_blocks_and_excludes_unregistered_on_repeat(): void {
+ $whitelist = array_keys( self::TEST_BLOCKS );
+ $registered = $this->sut->register_blocks( $whitelist );
+
+ sort( $registered );
+ $this->assertSame( $whitelist, $registered, 'All whitelisted block classes should be registered.' );
+
+ $wp_registry = \WP_Block_Type_Registry::get_instance();
+ foreach ( self::TEST_BLOCKS as $name ) {
+ $this->assertTrue( $wp_registry->is_registered( $name ), "$name should be in WP block registry." );
+ }
+
+ $first_class = array_key_first( self::TEST_BLOCKS );
+ $first_name = self::TEST_BLOCKS[ $first_class ];
+
+ $wp_registry->unregister( $first_name );
+ $repeat = $this->sut->register_blocks( $whitelist );
+
+ sort( $repeat );
+ $this->assertSame( array_values( array_diff( $whitelist, array( $first_class ) ) ), $repeat, "$first_name should be excluded from repeat call after WP registry removal." );
+ }
+}