append() call sites). So the * combination of preloadForRequest() (fast path for the queried post) and this * filter (backstop for everything else Breakdance renders around it) covers * the whole composed page, not just the page's own content. */ final class AssetManager { private static bool $singleProductLoaded = false; private static bool $bridgeLoaded = false; private static bool $miniCartHydrationLoaded = false; private static bool $lateEnqueueWarned = false; private static bool $designTokensLoaded = false; private static bool $lifecycleLoaded = false; private static bool $cartHydrationLoaded = false; private static bool $atomsEnhanceLoaded = false; /** * FluentCart normally discovers its shortcodes before wp_head. Breakdance * elements render later, so inspect the compiled document early and load * only the FluentCart asset groups represented in it. */ public static function preloadForRequest(): void { if (is_admin() || !class_exists('FluentCart\\App\\Modules\\Templating\\AssetLoader')) { return; } $source = ''; $postId = function_exists('get_queried_object_id') ? (int) get_queried_object_id() : 0; if ($postId && function_exists('get_post_meta')) { $raw = get_post_meta($postId, '_breakdance_data', true); $source = is_string($raw) ? $raw : ''; } if (function_exists('is_singular') && is_singular('fluent-products')) { $source .= ' FluentCartBreakdanceElements\\SingleProduct'; } self::preloadFromSource((string) apply_filters('fcbd/preload_document_source', $source, $postId)); } /** @param mixed $dependencies */ public static function preloadFromBreakdanceDependencies($dependencies) { $encoded = function_exists('wp_json_encode') ? wp_json_encode($dependencies) : json_encode($dependencies); self::preloadFromSource(is_string($encoded) ? $encoded : ''); return $dependencies; } /** * Scans a raw document/dependency source string for FluentCart element type * names and loads only the FluentCart asset groups those types actually need. * * $source is untyped serialized data (postmeta JSON, or a JSON-encoded * Breakdance dependency payload from preloadFromBreakdanceDependencies()), so * this cannot do a real node-tree walk; it deliberately stays a string scan. * What changed from a loose substring check to hardened matching: each type * name is now required to appear as the tail of the fully qualified element * class name (`FluentCartBreakdanceElements\`, optionally * double-backslash-escaped as it is in JSON), not followed by another * identifier character. That rules out a future element such as * "ProductActionButton" or "MiniCartWidget" being mistaken for * "ProductAction"/"MiniCart" and pulling in the wrong asset group. */ public static function preloadFromSource(string $source): void { if ($source === '' || strpos($source, 'FluentCartBreakdanceElements') === false) { return; } // Tokens-first (WP11A): elk FC-element consumeert --bde-fc-* defaults, // dus de tokens-stylesheet laadt zodra er ook maar EEN FC-element op de // gecomponeerde pagina staat, onafhankelijk van FluentCarts AssetLoader. self::designTokens(); $loader = 'FluentCart\\App\\Modules\\Templating\\AssetLoader'; if (!class_exists($loader)) { return; } // A page WITHOUT any FluentCart Breakdance element type never reaches // this point (both early returns above gate on it), so nothing below // this line ever fires for such a page: this IS the "no FC element -> // no FC asset groups" guarantee asserted by tests/run.php (G0H, see // ELITE-PLAN.md V1.1-audit punt 2). Any group actually loaded past // this point is a real, source-confirmed FluentCart element type. self::warnIfLateAssetEnqueue(); if (self::sourceHasElementType($source, 'ProductAction') || self::sourceHasElementType($source, 'ProductCard') || self::sourceHasElementType($source, 'ProductPart') || self::sourceHasElementType($source, 'SingleProduct') ) { self::singleProduct(); } if (self::sourceHasElementType($source, 'ProductCollection') && method_exists($loader, 'loadProductArchiveAssets')) { $loader::loadProductArchiveAssets(); } if (self::sourceHasElementType($source, 'MiniCart')) { if (method_exists($loader, 'loadCartAssets')) { $loader::loadCartAssets(); } if (method_exists($loader, 'loadMiniCartAssets')) { $loader::loadMiniCartAssets(); } self::miniCartHydration(); } if (self::sourceHasElementType($source, 'CommerceView')) { self::preloadCommerceAssets($loader); } // WP8 (Golf 2, fork C): the new first-rank FC_Mini_Cart, FC_Cart and // FC_Coupon elements. Deliberately separate branches from the golf-0 // 'MiniCart'/'CommerceView' short names above (sourceHasElementType() // anchors on the FULL short name immediately after the namespace // separator, so 'MiniCart' never matches 'FCMiniCart' and vice versa; // this fork's elements are additive, the golf-0 aliases are untouched). if (self::sourceHasElementType($source, 'FCMiniCart')) { if (method_exists($loader, 'loadCartAssets')) { $loader::loadCartAssets(); } if (method_exists($loader, 'loadMiniCartAssets')) { $loader::loadMiniCartAssets(); } self::cartHydration(); } if (self::sourceHasElementType($source, 'FCCart') && method_exists($loader, 'loadCartAssets')) { $loader::loadCartAssets(); } if (self::sourceHasElementType($source, 'FCCoupon') && method_exists($loader, 'loadCartAssets')) { $loader::loadCartAssets(); } // WP6 (Golf 2, fork A): fast path voor de 15 single-product-atomen zodat // hun FluentCart-assetgroep voor wp_head laadt; het render-time // AssetManager::singleProduct() in AtomRenderer blijft de vangnet-route. $atomTypes = [ 'FCProductTitle', 'FCProductPrice', 'FCProductGallery', 'FCProductStock', 'FCProductSku', 'FCProductExcerpt', 'FCProductDescription', 'FCProductMeta', 'FCProductVariations', 'FCProductQuantity', 'FCAddToCart', 'FCBuyNow', 'FCBuySection', 'FCPackageInfo', 'FCSubscriptionTerms', 'FCProductBuilder', ]; foreach ($atomTypes as $atomType) { if (self::sourceHasElementType($source, $atomType)) { self::singleProduct(); break; } } // WP7 (Golf 2, fork B): de collections gebruiken product-card-markup; // laad de archive/card-groep zodra de grid of related aanwezig is. if ((self::sourceHasElementType($source, 'FcProducts') || self::sourceHasElementType($source, 'FcRelatedProducts')) && method_exists($loader, 'loadProductArchiveAssets') ) { $loader::loadProductArchiveAssets(); } // WP10 (Golf 2, fork D): the account family. FluentCart's customer // dashboard, receipt, login and registration are JS-hydrated: the // shortcode prints a container that FluentCart's own dashboard/account // assets fill in on load. Without those assets the element renders an // empty shell (the symptom on a Breakdance account page once FluentCart's // own route takeover is out of the way). preloadCommerceAssets() loads // exactly FluentCart's cart/checkout/customer-dashboard/thank-you groups. if (self::sourceHasElementType($source, 'FCDashboard') || self::sourceHasElementType($source, 'FCDashboardButton') || self::sourceHasElementType($source, 'FCReceipt') || self::sourceHasElementType($source, 'FCLogin') || self::sourceHasElementType($source, 'FCRegistration') ) { self::preloadCommerceAssets($loader); } } /** * Asset-timing contract (G0H, ELITE-PLAN.md V1.1-audit punt 2): FluentCart * asset groups must be discovered before `wp_head` prints `` * output, so enqueued scripts/styles actually land there. The fast path, * preloadForRequest(), is hooked at `wp_enqueue_scripts` priority 30 * (Plugin::hook()), which WordPress core always fires before `wp_head`. * The backstop, preloadFromBreakdanceDependencies(), is hooked to * `breakdance_append_dependencies`, which Breakdance's own renderer can * in principle also invoke later (e.g. during an AJAX-driven SSR re-render * of a single element after `wp_head` has already run for the original * page load). If THAT happens, silently "fixing" it by enqueuing late * would just produce a mini cart or product element without its * stylesheet/script and no trace of why. This asserts the timing * invariant instead of papering over a violation: it logs a clear, * one-time debug notice and still lets the enqueue proceed (a broken but * visible page beats an invisible flash of unstyled/unscripted content). */ private static function warnIfLateAssetEnqueue(): void { if (self::$lateEnqueueWarned) { return; } if (!function_exists('did_action') || !did_action('wp_head')) { return; } self::$lateEnqueueWarned = true; if (defined('WP_DEBUG') && WP_DEBUG && function_exists('error_log')) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Diagnostic logging, only reached when WP_DEBUG is enabled. error_log( 'SparkCore Commerce contract violation: a FluentCart asset group was discovered after ' . 'wp_head already fired. Expected discovery paths are wp_enqueue_scripts priority 30 (before ' . 'wp_head) and breakdance_append_dependencies as a backstop (see AssetManager docblock and ' . 'docs/audit/breakdance-api.md par. 7); this request took neither in time. The affected element ' . 'may render without its enqueued scripts/styles in .' ); } } /** * Enqueues assets/mini-cart-hydrate.js once per request, only when a * Mini Cart element is actually present on the composed page (this * method is only ever called from the "MiniCart" branch of * preloadFromSource() above). See ElementRenderer::miniCart() and the * script's own docblock for the cache-safety route this implements. */ private static function miniCartHydration(): void { if (self::$miniCartHydrationLoaded || !function_exists('wp_enqueue_script')) { return; } wp_enqueue_script( 'fcbd-mini-cart-hydrate', FCBD_URL . 'assets/mini-cart-hydrate.js', [], FCBD_VERSION, true ); self::$miniCartHydrationLoaded = true; } /** * WP8 (Golf 2, fork C): enqueues assets/cart-hydration.js (plus its * fcbd-lifecycle dependency) once per request, only when an FC_Mini_Cart * element is actually present on the composed page (the "FCMiniCart" * branch of preloadFromSource() above). Both modes need this script (it * hydrates the badge/total for the dropdown mode too, see * assets/cart-hydration.js par. 1), so keying it off the element's mere * presence, as the string scan does, is correct here. * * Deliberately does NOT suppress FluentCart's own global floating drawer * (`fluent_cart/buttons/enable_floating_cart_button`). That call used to * live here and was a real bug: preloadFromSource() decides via a string * scan for the element TYPE name, which cannot see the element's `mode` * property, so it suppressed the native drawer even for the DEFAULT * 'dropdown' mode, which renders no replacement drawer at all and was * left with a trigger button that opened nothing. The decision moved to * bd-elements/FC_Mini_Cart/renderer.php::renderDrawer(), the first point * where the mode is known and where a replacement provably exists; see * that method's docblock for the full timing evidence and * src/State/CustomerStateHydrator.php par. 4 for why suppression is * wanted at all in drawer mode. */ private static function cartHydration(): void { if (self::$cartHydrationLoaded || !function_exists('wp_enqueue_script')) { return; } self::lifecycle(); wp_enqueue_script( 'fcbd-cart-hydration', FCBD_URL . 'assets/cart-hydration.js', ['fcbd-lifecycle'], FCBD_VERSION, true ); self::$cartHydrationLoaded = true; } /** * Matches a FluentCart Breakdance element's short type name against a raw * source string, anchored to the fully qualified class name so it cannot * match a longer, unrelated identifier that merely starts with the same * characters (see preloadFromSource() docblock). */ private static function sourceHasElementType(string $source, string $shortName): bool { return (bool) preg_match( '/FluentCartBreakdanceElements\\\\{1,2}' . preg_quote($shortName, '/') . '(?![A-Za-z0-9_])/', $source ); } private static function preloadCommerceAssets(string $loader): void { if (method_exists($loader, 'loadCartAssets')) { $loader::loadCartAssets(); } if (method_exists($loader, 'loadCheckoutAssets')) { $loader::loadCheckoutAssets(); } if (method_exists($loader, 'loadCustomerDashboardGlobalAssets')) { $loader::loadCustomerDashboardGlobalAssets(); } if (method_exists($loader, 'loadCustomerDashboardAssets')) { $loader::loadCustomerDashboardAssets(); } if (method_exists($loader, 'enqueueThankYouPageAssets')) { $loader::enqueueThankYouPageAssets(); } } /** * Loads FluentCart's single-product asset group once per request. * * Note: FluentCart 1.3.27 has no separate "advanced variation" asset group or hook. * loadSingleProductAssets() already covers everything a single-product view needs * (audit-fix A1, see docs/audit/fluentcart-contracts.md par. 8). */ public static function singleProduct(): void { if (!self::$singleProductLoaded && class_exists('FluentCart\\App\\Modules\\Templating\\AssetLoader')) { \FluentCart\App\Modules\Templating\AssetLoader::loadSingleProductAssets(); self::$singleProductLoaded = true; } self::variationBridge(); self::atomsEnhance(); } /** * Hotfix atomen-interactiviteit + variations/price weergave (2026-07-15): * enqueues assets/atoms-enhance.js (the "period_style"/"options_style" * dropdown progressive enhancement, see that file's own docblock and * bd-elements/FC_Product_Variations/ssr.php). Same once-per-request, * fcbd-lifecycle-dependent enqueue pattern as variationBridge()/ * cartHydration() above; loaded from AtomRenderer::render() (via * singleProduct()) whenever any atom actually renders, same reasoning as * that method's own docblock. */ public static function atomsEnhance(): void { if (self::$atomsEnhanceLoaded || !function_exists('wp_enqueue_script')) { return; } self::lifecycle(); wp_enqueue_script( 'fcbd-atoms-enhance', FCBD_URL . 'assets/atoms-enhance.js', ['fcbd-lifecycle'], FCBD_VERSION, true ); self::$atomsEnhanceLoaded = true; } /** * Enqueues variation-sync.js through wp_enqueue_script()/wp_localize_script() * rather than declaring it via Element::dependencies() on the elements that * need it. This is a deliberate golf-0 deviation from ELITE-PLAN.md par. 1 * point 5 ("Officiele Breakdance-kanalen"): dependencies() (per * docs/audit/breakdance-api.md par. 7) only carries bare script/style URL * strings ($scripts/$styles in ElementDependenciesAndConditions) plus an * optional frontendCondition, it has no mechanism to attach localized data. * variation-sync.js needs the translated stock labels and the out-of-stock * status slug at runtime (fcbdVars, below), which only wp_localize_script() * can hand to a script tag. Once dependencies() (or a future Breakdance API) * gains a data-localization hook, this bridge can move to the official * channel and drop the manual once-per-request enqueue guard. */ /** * Enqueue the generated --bde-fc-* token stylesheet (WP11A). Idempotent, * and only ever called from behind the preloadFromSource() element gate, * so a page without FC elements never loads it. */ public static function designTokens(): void { if (self::$designTokensLoaded || !function_exists('wp_enqueue_style')) { return; } wp_enqueue_style( 'fcbd-tokens', FCBD_URL . 'assets/fcbd-tokens.css', [], FCBD_VERSION ); self::$designTokensLoaded = true; } /** * Enqueue the shared G2K element lifecycle (idempotent init, fcbd:reinit, * instance registry). Loads before variation-sync so instance-scoping can * consume it; every golf 2 element depends on this handle. */ public static function lifecycle(): void { if (self::$lifecycleLoaded || !function_exists('wp_enqueue_script')) { return; } wp_enqueue_script( 'fcbd-lifecycle', FCBD_URL . 'assets/fcbd-lifecycle.js', [], FCBD_VERSION, true ); self::$lifecycleLoaded = true; } public static function variationBridge(): void { if (self::$bridgeLoaded || !function_exists('wp_enqueue_script')) { return; } self::lifecycle(); wp_enqueue_script( 'fcbd-variation-sync', FCBD_URL . 'assets/variation-sync.js', ['fcbd-lifecycle'], FCBD_VERSION, true ); if (function_exists('wp_localize_script')) { wp_localize_script('fcbd-variation-sync', 'fcbdVars', [ 'stockLabels' => [ 'in-stock' => __('In stock', 'sparkcore-commerce'), 'out-of-stock' => __('Out of stock', 'sparkcore-commerce'), ], 'outOfStockStatus' => 'out-of-stock', ]); } self::$bridgeLoaded = true; } /** * Detects Breakdance's own single-element AJAX re-render (plugin/render/ * ajax-server-side-render.php serverSideRender(), action * "breakdance_server_side_render"): the builder calls this endpoint to * refresh ONE element in place after a settings change, and it renders * via `$element::ssr()` directly (plugin/elements/Element.class.php), * never through the per-node tree renderer that normally calls * `Element::dependencies()` for every rendered node (renderer.php * getDependencies(), invoked from the per-node reduce at * renderer.php:1407). Concretely: no wp_head, no wp_footer, no * wp_enqueue_scripts and no getFrontendAssets() call anywhere in that * file (verified by a full grep, zero matches), so this is the one * request context none of this file's wp_enqueue_script() methods ever * reach, however the page's own composed markup was scanned. * * Prefers Breakdance's own `\Breakdance\isRequestFromBuilderSsr()` * (plugin/util/is-request-from-builder-iframe.php) when it is loaded, * falling back to the same raw $_POST['action'] check it wraps * internally. That fallback shape matches the one already established in * ProductContext::isBuilderRequest() and keeps this testable from a bare * PHP harness with no Breakdance loaded (see tests/assets/run-assets.php). */ private static function isBuilderSsrRequest(): bool { if (function_exists('Breakdance\\isRequestFromBuilderSsr')) { return \Breakdance\isRequestFromBuilderSsr(); } $action = ''; // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- Read-only builder-request detection (compares the action name only); unslashed and sanitized below, no state is changed. if (isset($_POST['action'])) { $action = (string) $_POST['action']; $action = function_exists('wp_unslash') ? (string) wp_unslash($action) : $action; $action = function_exists('sanitize_text_field') ? sanitize_text_field($action) : $action; } // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput return $action === 'breakdance_server_side_render'; } /** * Official `Element::dependencies()` channel (docs/audit/breakdance-api.md * par. 7) for a script list that carries no localized data of its own. * Deliberately gated to isBuilderSsrRequest() only: everywhere else * (a normal front-end render, or a full page load inside the builder * iframe) this returns false, and the matching wp_enqueue_script() method * elsewhere in this file remains the sole delivery channel for that same * script, completely unchanged from before this addition. That mutual * exclusion between the two channels, not a dedup trick, is what keeps a * script from ever loading twice: dependencies() only ever contributes * anything during the one request wp_enqueue_script() cannot reach at * all, so the two can never both be active for the same request. * * A value returned here still needs to actually reach Breakdance's * single-element AJAX response, which does not call dependencies() on * its own (see this method's own docblock); appendElementDependenciesForBuilderSsr() * below, hooked at the bottom of this file, is what bridges the two. * * @param string[] $scripts Absolute script URLs, in execution order. * @param string[] $inlineScripts Raw JS run once, after the scripts above. * @return array>|false */ public static function builderSsrDependencies(array $scripts, array $inlineScripts = []) { if (!$scripts || !self::isBuilderSsrRequest()) { return false; } $entry = [ 'scripts' => $scripts, 'frontendCondition' => 'return true;', ]; if ($inlineScripts) { $entry['inlineScripts'] = $inlineScripts; } return [$entry]; } /** * dependencies() payload shared by every single-product atom: the 15 WP6 * atoms (FC_Product_Title/Price/Gallery/Stock/SKU/Excerpt/Description/ * Meta/Variations/Quantity, FC_Add_To_Cart, FC_Buy_Now, FC_Buy_Section, * FC_Package_Info, FC_Subscription_Terms, FC_Product_Builder) plus the * golf-0 Product_Action/Product_Card/Product_Part/Single_Product aliases * (the same type list singleProduct() below already gates on). This is * the exact fcbd-lifecycle.js + atoms-enhance.js + variation-sync.js trio * atomsEnhance()/variationBridge() already load for a normal render. * * Only variation-sync.js's fcbdVars localized data (stockLabels, * outOfStockStatus) has no carrier in the official dependencies() schema: * ElementDependenciesAndConditions has scripts/inlineScripts/styles/ * inlineStyles/builderCondition/frontendCondition and nothing else (docs/ * audit/breakdance-api.md par. 7), no localize-style data key. This * mirrors that same payload as a plain * `window.fcbdVars = window.fcbdVars || {...}` inlineScript instead: * inlineScripts IS part of the official schema, and variation-sync.js * only ever reads window.fcbdVars lazily, inside its * fluentCartSingleProductVariationChanged listener (never at parse time, * see assets/variation-sync.js stockLabelFor()/setActions()), so this * does not have to win any race against the deferred script tag: it only * has to run before the shopper changes a variation, which is always * true once the element has finished rendering at all. * * variationSyncInlineVarsForBuilderSsr() below keeps its own literal copy * of that data rather than calling into variationBridge(), so nothing * this fork adds can ever change what that pinned, already-tested method * does for a normal render. */ public static function singleProductAtomDependencies() { return self::builderSsrDependencies( [ FCBD_URL . 'assets/fcbd-lifecycle.js', FCBD_URL . 'assets/atoms-enhance.js', FCBD_URL . 'assets/variation-sync.js', ], [self::variationSyncInlineVarsForBuilderSsr()] ); } /** * Mirrors variationBridge()'s wp_localize_script('fcbd-variation-sync', * 'fcbdVars', [...]) payload for the one context that call never reaches * (see singleProductAtomDependencies() docblock). Deliberately a literal * copy, not a shared call into variationBridge(), so editing this can * never change variationBridge()'s own, separately tested behavior. */ private static function variationSyncInlineVarsForBuilderSsr(): string { $vars = [ 'stockLabels' => [ 'in-stock' => __('In stock', 'sparkcore-commerce'), 'out-of-stock' => __('Out of stock', 'sparkcore-commerce'), ], 'outOfStockStatus' => 'out-of-stock', ]; $encoded = function_exists('wp_json_encode') ? wp_json_encode($vars) : json_encode($vars); return 'window.fcbdVars = window.fcbdVars || ' . $encoded . ';'; } /** * dependencies() payload for FC_Mini_Cart: the fcbd-lifecycle.js + * cart-hydration.js pair cartHydration() elsewhere in this file already * loads for a normal render (see singleProductAtomDependencies() * docblock for why this is gated and safe from double-loading). */ public static function fcMiniCartDependencies() { return self::builderSsrDependencies([ FCBD_URL . 'assets/fcbd-lifecycle.js', FCBD_URL . 'assets/cart-hydration.js', ]); } /** * dependencies() payload for the golf-0 Mini_Cart alias: the standalone * mini-cart-hydrate.js miniCartHydration() elsewhere in this file already * loads for a normal render. No fcbd-lifecycle.js dependency here: * assets/mini-cart-hydrate.js never reads window.fcbdLifecycle, unlike * cart-hydration.js. */ public static function miniCartDependencies() { return self::builderSsrDependencies([ FCBD_URL . 'assets/mini-cart-hydrate.js', ]); } /** * Bridges the official Element::dependencies() channel into Breakdance's * single-element AJAX re-render response (plugin/render/ajax-server- * side-render.php serverSideRender()). That function renders via * `$element::ssr()` directly and never walks the per-node tree renderer * that normally collects dependencies() for every node (see * isBuilderSsrRequest()'s docblock), so nothing else ever calls * dependencies() for the one element being re-rendered there. * * Hooked (bottom of this file) to `breakdance_builder_ssr_rendered_html`, * the one filter serverSideRender() itself runs * (ajax-server-side-render.php:65) right before it reads * ScriptAndStyleHolder::getInstance()->dependencies into the JSON * response two lines later (line 70), so appending here lands in time. * * Scoped to this plugin's own element namespace only * (FluentCartBreakdanceElements\\), so this can only ever add what one of * this plugin's own elements declared through the very method this fork * is fixing; it does not attempt to plug this gap for Breakdance's own or * any other plugin's elements. A declared entry whose frontendCondition * is anything other than the unconditional 'return true;' this plugin's * own dependencies() methods use is skipped rather than guessed at: this * bridge does not evaluate Twig or eval PHP, unlike the tree renderer's * own getDependencies(). * * Registered at the top level of this file, not in Plugin::hook(), * because this fork's file area is this file only. fluentcart- * breakdance.php require_once's this file unconditionally and early * (before Plugin::hook() runs), so the registration below still fires on * every request, exactly like the two hooks Plugin::hook() itself adds * for this same class. * * @param mixed $ssrResult * @param mixed $properties * @param mixed $elementSlug * @param mixed $parentProperties * @return mixed */ public static function appendElementDependenciesForBuilderSsr($ssrResult, $properties, $elementSlug, $parentProperties) { if (!is_string($elementSlug) || strpos($elementSlug, 'FluentCartBreakdanceElements\\') !== 0 || !class_exists($elementSlug) || !method_exists($elementSlug, 'dependencies') || !class_exists('\\Breakdance\\Render\\ScriptAndStyleHolder') ) { return $ssrResult; } $declared = $elementSlug::dependencies(); if (!is_array($declared)) { return $ssrResult; } $holder = \Breakdance\Render\ScriptAndStyleHolder::getInstance(); foreach ($declared as $entry) { if (!is_array($entry)) { continue; } $condition = trim((string) ($entry['frontendCondition'] ?? '')); if ($condition !== '' && $condition !== 'return true;') { continue; } $holder->append($entry); } return $ssrResult; } } /** * See AssetManager::appendElementDependenciesForBuilderSsr()'s own docblock * for why this registers here rather than in Plugin::hook(): this file is * require_once'd unconditionally and early by fluentcart-breakdance.php, so * this line always runs, on every request, before any of Breakdance's own * request-type dispatching happens. */ if (function_exists('add_filter')) { add_filter( 'breakdance_builder_ssr_rendered_html', [AssetManager::class, 'appendElementDependenciesForBuilderSsr'], 10, 4 ); }