interface PreliquifyRuntime { components: Map; mounted: Map; errors: Error[]; debug: boolean; } declare global { interface Window { __PRELIQUIFY__: { [key: string]: any; }; __preliquifyRuntime: PreliquifyRuntime; __PRELIQUIFY_DEBUG__: boolean; } } function safeHydrate( element: Element, Component: any, props: any, runtime: PreliquifyRuntime ): boolean { try { const preact = window.preact; if (!preact) { throw new Error( "Preact not found. Make sure preact is bundled or loaded before hydration." ); } const vnode = preact.h(Component, props); preact.render(vnode, element); const id = element.getAttribute("data-preliq-id"); if (id) { runtime.mounted.set(id, { element, Component, props }); } return true; } catch (error) { runtime.errors.push(error as Error); if (runtime.debug) { console.error("[__PRELIQUIFY__] Hydration error:", error); console.error("Element:", element); console.error("Component:", Component); console.error("Props:", props); } element.setAttribute("data-preliq-error", "true"); element.dispatchEvent( new CustomEvent("preliquify:error", { detail: { error, component: Component.name || "Unknown", props }, bubbles: true, }) ); return false; } } function decodeHtmlEntities(text: string): string { // Create a temporary element to decode HTML entities const textarea = document.createElement("textarea"); textarea.innerHTML = text; return textarea.value; } function parseProps(element: Element): Record { // First try to read from script tag (avoids HTML escaping issues) const scriptTag = element.querySelector("script[data-preliq-props]"); if (scriptTag) { try { const content = scriptTag.textContent || ""; const trimmed = content.trim(); if (trimmed) { // Check if content still has Liquid tags (means Liquid wasn't processed) if (trimmed.includes("{%") || trimmed.includes("{{")) { console.warn( "[Preliquify] Script tag contains Liquid expressions - Liquid may not have processed this template. Content:", trimmed.substring(0, 200) ); return {}; } // Decode HTML entities (e.g., " -> ") // If textContent already decoded it, this is a no-op // If innerHTML was used, this will decode entities const decoded = decodeHtmlEntities(trimmed); return JSON.parse(decoded); } } catch (error) { console.warn("[Preliquify] Failed to parse props from script:", error); console.warn( "[Preliquify] Script content:", scriptTag.textContent?.substring(0, 200) ); // Try decoding HTML entities as fallback try { const content = scriptTag.textContent || ""; const decoded = decodeHtmlEntities(content.trim()); return JSON.parse(decoded); } catch (decodeError) { // If decoding also fails, return empty props } } } // Fallback to data attribute (backward compatibility) const propsAttr = element.getAttribute("data-preliq-props"); if (!propsAttr) return {}; try { // Decode HTML entities from attribute as well const decoded = decodeHtmlEntities(propsAttr); return JSON.parse(decoded); } catch (error) { console.warn("[Preliquify] Failed to parse props:", propsAttr, error); return {}; } } function isElementVisible(element: Element): boolean { const rect = element.getBoundingClientRect(); const viewHeight = Math.max( document.documentElement.clientHeight, window.innerHeight ); const viewWidth = Math.max( document.documentElement.clientWidth, window.innerWidth ); const margin = 100; return !( rect.bottom < -margin || rect.top > viewHeight + margin || rect.right < -margin || rect.left > viewWidth + margin ); } function hydrateIslands(runtime: PreliquifyRuntime): void { const islands = document.querySelectorAll( "[data-preliq-island]:not([data-preliq-hydrated])" ); const visibleIslands: Element[] = []; const deferredIslands: Element[] = []; islands.forEach((island) => { if (isElementVisible(island)) { visibleIslands.push(island); } else { deferredIslands.push(island); } }); visibleIslands.forEach((island) => { hydrateIsland(island, runtime); }); if (deferredIslands.length > 0 && "IntersectionObserver" in window) { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { hydrateIsland(entry.target, runtime); observer.unobserve(entry.target); } }); }, { rootMargin: "100px", } ); deferredIslands.forEach((island) => observer.observe(island)); } else { deferredIslands.forEach((island) => hydrateIsland(island, runtime)); } // Retry pending islands (components that weren't found yet) // This handles the case where components register after initial hydration attempt const pendingIslands = document.querySelectorAll("[data-preliq-pending]"); if (pendingIslands.length > 0) { pendingIslands.forEach((island) => { const componentName = island.getAttribute("data-preliq-pending"); const Component = runtime.components.get(componentName || "") || window.__PRELIQUIFY__?.[componentName || ""]; if (Component) { // Component is now available, try to hydrate hydrateIsland(island, runtime); } }); } } function hydrateIsland(element: Element, runtime: PreliquifyRuntime): void { if (element.hasAttribute("data-preliq-hydrated")) return; const componentName = element.getAttribute("data-preliq-island"); const id = element.getAttribute("data-preliq-id"); if (!componentName) { console.warn("[Preliquify] Island missing component name:", element); return; } const Component = runtime.components.get(componentName) || window.__PRELIQUIFY__?.[componentName]; if (!Component) { // Component not found yet - this is expected when scripts load asynchronously // Don't set permanent error, just log a warning (only in debug mode to reduce noise) if (runtime.debug) { console.warn( `[Preliquify] Component "${componentName}" not found yet (will retry)` ); console.warn( `[Preliquify] Available components:`, Array.from(runtime.components.keys()) ); console.warn( `[Preliquify] Available in window.__PRELIQUIFY__:`, Object.keys(window.__PRELIQUIFY__ || {}) ); } // Set a temporary marker to track that we've tried (but don't mark as error) // This allows retries when the component becomes available element.setAttribute("data-preliq-pending", componentName); return; } // Component found - remove any pending marker element.removeAttribute("data-preliq-pending"); element.removeAttribute("data-preliq-error"); const props = parseProps(element); const hydrated = safeHydrate(element, Component, props, runtime); // Only mark as hydrated if render actually succeeded if (hydrated) { element.setAttribute("data-preliq-hydrated", "true"); element.dispatchEvent( new CustomEvent("preliquify:hydrated", { detail: { id, component: componentName }, bubbles: true, }) ); } } function initRuntime(): PreliquifyRuntime { if (window.__preliquifyRuntime) { return window.__preliquifyRuntime; } const runtime: PreliquifyRuntime = { components: new Map(), mounted: new Map(), errors: [], debug: window.__PRELIQUIFY_DEBUG__ || false, }; window.__preliquifyRuntime = runtime; if (!window.__PRELIQUIFY__) { window.__PRELIQUIFY__ = {}; } return runtime; } const runtime = initRuntime(); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { if (document.body) { hydrateIslands(runtime); } }); } else { if (document.body) { if ("requestIdleCallback" in window) { window.requestIdleCallback(() => hydrateIslands(runtime)); } else { setTimeout(() => hydrateIslands(runtime), 0); } } } // Initialize window.__PRELIQUIFY__ first if (!window.__PRELIQUIFY__) { window.__PRELIQUIFY__ = {}; } const Preliquify = { register(name: string, component: any): void { runtime.components.set(name, component); window.__PRELIQUIFY__[name] = component; // Trigger hydration when component registers (handles defer script timing) // Safe to call multiple times - hydrate() skips already-hydrated islands // This will retry any pending islands waiting for this component if (document.body) { hydrateIslands(runtime); } }, hydrate(container?: Element): void { const searchRoot = container || document.body; const islands = searchRoot.querySelectorAll( "[data-preliq-island]:not([data-preliq-hydrated])" ); islands.forEach((island) => hydrateIsland(island, runtime)); // Also retry any pending islands in this container const pendingIslands = searchRoot.querySelectorAll("[data-preliq-pending]"); pendingIslands.forEach((island) => { const componentName = island.getAttribute("data-preliq-pending"); const Component = runtime.components.get(componentName || "") || window.__PRELIQUIFY__?.[componentName || ""]; if (Component) { hydrateIsland(island, runtime); } }); }, getComponent(id: string): any { return runtime.mounted.get(id); }, getErrors(): Error[] { return [...runtime.errors]; }, setDebug(enabled: boolean): void { runtime.debug = enabled; }, unmount(id: string): boolean { const mounted = runtime.mounted.get(id); if (!mounted) return false; const preact = window.preact; if (preact) { preact.render(null, mounted.element); runtime.mounted.delete(id); mounted.element.removeAttribute("data-preliq-hydrated"); return true; } return false; }, update(id: string, newProps: Record): boolean { const mounted = runtime.mounted.get(id); if (!mounted) return false; const mergedProps = { ...mounted.props, ...newProps }; safeHydrate(mounted.element, mounted.Component, mergedProps, runtime); mounted.props = mergedProps; return true; }, }; // Expose all methods on window.__PRELIQUIFY__ Object.assign(window.__PRELIQUIFY__, Preliquify); export {};