From 68ae4e51601006d1c8b465884767f4aa7ddda869 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Thu, 9 Jul 2026 17:33:35 -0700 Subject: [PATCH] fix(core,producer): harden render reapply wiring --- packages/core/package.json | 3 +- .../generated/position-edits-render-inline.ts | 2 +- packages/core/src/runtime/positionEdits.ts | 111 +++++++++++++----- .../producer/src/services/htmlCompiler.ts | 3 +- 4 files changed, 88 insertions(+), 31 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 70d9edc8e..5337f5e4a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -399,13 +399,14 @@ }, "scripts": { "build": "bun run build:hyperframes-runtime && bun run build:position-edits-render && tsc && tsx scripts/rewrite-esm-extensions.ts", - "test": "vitest run", + "test": "bun run check:position-edits-render && vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit", "lint:runtime-preview-guards": "tsx scripts/lint-runtime-preview-guards.ts", "build:hyperframes-runtime": "tsx scripts/build-hyperframes-runtime-artifact.ts", "build:position-edits-render": "tsx scripts/build-position-edits-render.ts", + "check:position-edits-render": "bun run build:position-edits-render && git diff --exit-code -- src/generated/position-edits-render-inline.ts", "build:hyperframes-runtime:modular": "SANDBOX_RUNTIME_VARIANT=modular tsx scripts/build-hyperframes-runtime-artifact.ts", "build:hyperframe-runtime": "tsx scripts/build-hyperframes-runtime-artifact.ts", "test:hyperframe-runtime-contract": "tsx scripts/test-hyperframe-runtime-contract.ts", diff --git a/packages/core/src/generated/position-edits-render-inline.ts b/packages/core/src/generated/position-edits-render-inline.ts index b01a8c7d4..0448cccb1 100644 --- a/packages/core/src/generated/position-edits-render-inline.ts +++ b/packages/core/src/generated/position-edits-render-inline.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by scripts/build-position-edits-render.ts - do not edit const POSITION_EDITS_RENDER_IIFE: string = - '"use strict";(()=>{function g(){return globalThis}function w(t,e){if(typeof window>"u")return;let n=g(),o=n.__hf?.onSwallowed;if(o)try{o({label:t,error:e})}catch(r){}(n.__hfDebug||n.__HYPERFRAMES_DEBUG)&&console.debug(`[hyperframes] ${t} swallowed:`,e)}var E=null;function h(t,e){if(E)try{E({source:"hf-preview",type:"analytics",event:t,properties:e??{}})}catch(n){w("runtime.analytics.site1",n)}}var l="data-hf-edit-base-x",p="data-hf-edit-base-y",d="data-hf-edit-original-translate",u=t=>{let e=parseFloat(t??"");return Number.isFinite(e)?e:0},$=t=>{let e=[],n=0,o="";for(let r of t.trim())r==="("&&(n+=1),r===")"&&(n=Math.max(0,n-1)),/\\s/.test(r)&&n===0?(o&&e.push(o),o=""):o+=r;return o&&e.push(o),e},k=/^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)px$/,f=(t,e)=>k.test(t)&&k.test(e)?`${parseFloat(t)+parseFloat(e)}px`:`calc(${t} + ${e})`,D=(t,e,n)=>{if(!t||t==="none")return`${e} ${n}`;let[o,r,i]=$(t);if(o===void 0)return`${e} ${n}`;if(r===void 0)return`${f(o,e)} ${n}`;let a=i===void 0?"":` ${i}`;return`${f(o,e)} ${f(r,n)}${a}`},L=t=>{try{t.ownerDocument.defaultView?.gsap?.getProperty?.(t,"x")}catch{}},M=t=>{let e=t.style.getPropertyValue("translate").trim();if(e)return e==="none"?"":e;try{let n=t.ownerDocument.defaultView,o=n?n.getComputedStyle(t).getPropertyValue("translate").trim():"";return o==="none"?"":o}catch{return""}},A=new WeakMap;function H(t,e){let n=A.get(t);if(!e?.force&&n!==void 0&&t.style.getPropertyValue("translate")!==n){h("position_edit_fold_skipped",{hfId:t.getAttribute("data-hf-id")});return}let o=u(t.getAttribute("data-x"))-u(t.getAttribute(l)),r=u(t.getAttribute("data-y"))-u(t.getAttribute(p));t.hasAttribute(d)||t.setAttribute(d,M(t)),n===void 0&&L(t);let i=t.getAttribute(d)??"",a=D(i,`${o}px`,`${r}px`);t.style.setProperty("translate",a),A.set(t,t.style.getPropertyValue("translate"))}function _(t){let e=t.querySelectorAll(`[${l}], [${p}]`),n=t.defaultView?.HTMLElement,o=0;for(let r=0;r{try{_(e.document)}catch{}},o=s=>typeof s=="function"&&!!s[T],r=s=>{try{Object.defineProperty(s,T,{value:!0})}catch{}},i=(s,P)=>{let c=s();if(typeof c!="function"||o(c))return;let m=function(...R){let x=c.apply(this,R);return n(),x};r(m),P(m),n()},a=()=>{i(()=>e.__hf?.seek,s=>{e.__hf&&(e.__hf.seek=s)}),i(()=>e.__player?.renderSeek,s=>{e.__player&&(e.__player.renderSeek=s)})};a();let y=120,S=e.setInterval(()=>{a(),y-=1,y<=0&&e.clearInterval(S)},50)}function b(){document.querySelector(`[${l}], [${p}]`)&&(_(document),v(window))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",b,{once:!0}):b();})();\n'; + '"use strict";(()=>{function v(){return globalThis}function A(e,t){if(typeof window>"u")return;let o=v(),r=o.__hf?.onSwallowed;if(r)try{r({label:e,error:t})}catch(i){}(o.__hfDebug||o.__HYPERFRAMES_DEBUG)&&console.debug(`[hyperframes] ${e} swallowed:`,t)}var P=null;function T(e,t){if(P)try{P({source:"hf-preview",type:"analytics",event:e,properties:t??{}})}catch(o){A("runtime.analytics.site1",o)}}var w="data-hf-edit-base-x",y="data-hf-edit-base-y",_="data-hf-edit-original-translate",g=e=>{let t=parseFloat(e??"");return Number.isFinite(t)?t:0},H=e=>{let t=[],o=0,r="";for(let i of e.trim())i==="("&&(o+=1),i===")"&&(o=Math.max(0,o-1)),/\\s/.test(i)&&o===0?(r&&t.push(r),r=""):r+=i;return r&&t.push(r),t},R=/^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)px$/,b=(e,t)=>R.test(e)&&R.test(t)?`${parseFloat(e)+parseFloat(t)}px`:`calc(${e} + ${t})`,I=(e,t,o)=>{if(!e||e==="none")return`${t} ${o}`;let[r,i,a]=H(e);if(r===void 0)return`${t} ${o}`;if(i===void 0)return`${b(r,t)} ${o}`;let c=a===void 0?"":` ${a}`;return`${b(r,t)} ${b(i,o)}${c}`},O=e=>{try{e.ownerDocument.defaultView?.gsap?.getProperty?.(e,"x")}catch{}},j=e=>{let t=e.style.getPropertyValue("translate").trim();if(t)return t==="none"?"":t;try{let o=e.ownerDocument.defaultView,r=o?o.getComputedStyle(e).getPropertyValue("translate").trim():"";return r==="none"?"":r}catch{return""}},x=new WeakMap;function V(e,t){let o=x.get(e);if(!t?.force&&o!==void 0&&e.style.getPropertyValue("translate")!==o){T("position_edit_fold_skipped",{hfId:e.getAttribute("data-hf-id")});return}let r=g(e.getAttribute("data-x"))-g(e.getAttribute(w)),i=g(e.getAttribute("data-y"))-g(e.getAttribute(y));e.hasAttribute(_)||e.setAttribute(_,j(e)),o===void 0&&O(e);let a=e.getAttribute(_)??"",c=I(a,`${r}px`,`${i}px`);e.style.setProperty("translate",c),x.set(e,e.style.getPropertyValue("translate"))}function m(e){let t=e.querySelectorAll(`[${w}], [${y}]`),o=e.defaultView?.HTMLElement,r=0;for(let i=0;i{try{m(t.document)}catch{}},r=n=>typeof n=="function"&&($.has(n)||!!n[D]),i=n=>{$.add(n);try{Object.defineProperty(n,D,{value:!0})}catch{}},a=n=>{if(typeof n!="function"||r(n))return n;let s=function(...u){let d=n.apply(this,u);return o(),d};return i(s),s},c=(n,s)=>{let u=L.get(n);if(u?.has(s))return!0;let d=Object.getOwnPropertyDescriptor(n,s);if(d?.configurable===!1){let l=n[s];return typeof l=="function"&&(n[s]=a(l),o()),!1}let p=n[s],f=d?.set;return Object.defineProperty(n,s,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>p,set:l=>{p=a(l),f?.call(n,l)}}),p=a(p),u??(u=new Set),u.add(s),L.set(n,u),o(),!0},k=(n,s)=>{let u=F.get(t),d=Object.getOwnPropertyDescriptor(t,n);if(!u?.has(n)){if(d?.configurable===!1){let l=t[n];return l?c(l,s):!1}let f=t[n];Object.defineProperty(t,n,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>f,set:l=>{f=l,f&&c(f,s)}}),u??(u=new Set),u.add(n),F.set(t,u)}let p=t[n];return p?c(p,s):!1},E=()=>{let n=k("__hf","seek"),s=k("__player","renderSeek");return n&&s};if(E())return;let S=120,h=t.setInterval(()=>{if(E()){t.clearInterval(h);return}S-=1,S<=0&&t.clearInterval(h)},50)}function W(){document.querySelector(`[${w}], [${y}]`)&&(m(document),M(window))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",W,{once:!0}):W();})();\n'; /** Returns the pre-built position-edits render IIFE as a string constant. */ export function getPositionEditsRenderScript(): string { diff --git a/packages/core/src/runtime/positionEdits.ts b/packages/core/src/runtime/positionEdits.ts index 4afb623a9..601ff9aa5 100644 --- a/packages/core/src/runtime/positionEdits.ts +++ b/packages/core/src/runtime/positionEdits.ts @@ -177,6 +177,10 @@ export function applyPositionEdits(doc: Document): number { } const SEEK_REAPPLY_WRAPPED = "__hfPositionEditsSeekReapplyWrapped"; +type SeekFunction = (...args: unknown[]) => unknown; +const wrappedSeekFunctions = new WeakSet(); +const observedSeekProperties = new WeakMap>(); +const observedGlobalProperties = new WeakMap>(); type SeekWindow = Window & typeof globalThis & { @@ -195,54 +199,105 @@ export function installPositionEditsSeekReapply(win: Window & typeof globalThis) } }; - const isWrapped = (fn: unknown): fn is (...args: unknown[]) => unknown => + const isWrapped = (fn: unknown): fn is SeekFunction => typeof fn === "function" && - Boolean((fn as { [SEEK_REAPPLY_WRAPPED]?: boolean })[SEEK_REAPPLY_WRAPPED]); + (wrappedSeekFunctions.has(fn) || + Boolean((fn as { [SEEK_REAPPLY_WRAPPED]?: boolean })[SEEK_REAPPLY_WRAPPED])); - const markWrapped = (fn: (...args: unknown[]) => unknown): void => { + const markWrapped = (fn: SeekFunction): void => { + wrappedSeekFunctions.add(fn); try { Object.defineProperty(fn, SEEK_REAPPLY_WRAPPED, { value: true }); } catch { - // Frozen functions cannot be marked; the wrapper itself is still valid. + // The WeakSet keeps frozen functions from being wrapped repeatedly. } }; - const wrapOne = ( - get: () => unknown, - set: (fn: (...args: unknown[]) => unknown) => void, - ): void => { - const fn = get(); - if (typeof fn !== "function") return; - if (isWrapped(fn)) return; - const wrapped = function (this: unknown, ...args: unknown[]): unknown { + const wrapFunction = (fn: unknown): unknown => { + if (typeof fn !== "function" || isWrapped(fn)) return fn; + const wrapped: SeekFunction = function (this: unknown, ...args: unknown[]): unknown { const result = fn.apply(this, args); reapply(); return result; }; markWrapped(wrapped); - set(wrapped); + return wrapped; + }; + + const observeSeekProperty = (container: object, property: string): boolean => { + let observed = observedSeekProperties.get(container); + if (observed?.has(property)) return true; + const descriptor = Object.getOwnPropertyDescriptor(container, property); + if (descriptor?.configurable === false) { + const current = (container as Record)[property]; + if (typeof current === "function") { + (container as Record)[property] = wrapFunction(current); + reapply(); + } + return false; + } + + let current = (container as Record)[property]; + const originalSetter = descriptor?.set; + Object.defineProperty(container, property, { + configurable: true, + enumerable: descriptor?.enumerable ?? true, + get: () => current, + set: (value: unknown) => { + current = wrapFunction(value); + originalSetter?.call(container, value); + }, + }); + current = wrapFunction(current); + observed ??= new Set(); + observed.add(property); + observedSeekProperties.set(container, observed); reapply(); + return true; }; - const wrapAll = (): void => { - wrapOne( - () => target.__hf?.seek, - (fn) => { - if (target.__hf) target.__hf.seek = fn; - }, - ); - wrapOne( - () => target.__player?.renderSeek, - (fn) => { - if (target.__player) target.__player.renderSeek = fn; - }, - ); + const observeGlobalContainer = ( + name: "__hf" | "__player", + property: "seek" | "renderSeek", + ): boolean => { + let globals = observedGlobalProperties.get(target); + const descriptor = Object.getOwnPropertyDescriptor(target, name); + if (!globals?.has(name)) { + if (descriptor?.configurable === false) { + const current = target[name]; + return current ? observeSeekProperty(current, property) : false; + } + let value = target[name]; + Object.defineProperty(target, name, { + configurable: true, + enumerable: descriptor?.enumerable ?? true, + get: () => value, + set: (next: unknown) => { + value = next as typeof value; + if (value) observeSeekProperty(value, property); + }, + }); + globals ??= new Set(); + globals.add(name); + observedGlobalProperties.set(target, globals); + } + const current = target[name]; + return current ? observeSeekProperty(current, property) : false; }; - wrapAll(); + const wrapAll = (): boolean => { + const hfObserved = observeGlobalContainer("__hf", "seek"); + const playerObserved = observeGlobalContainer("__player", "renderSeek"); + return hfObserved && playerObserved; + }; + + if (wrapAll()) return; let remaining = 120; const interval = target.setInterval(() => { - wrapAll(); + if (wrapAll()) { + target.clearInterval(interval); + return; + } remaining -= 1; if (remaining <= 0) target.clearInterval(interval); }, 50); diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts index 08b623366..37dd29cf8 100644 --- a/packages/producer/src/services/htmlCompiler.ts +++ b/packages/producer/src/services/htmlCompiler.ts @@ -89,7 +89,8 @@ export function injectSdkPositionEditsRenderScript(html: string): string { if (!html.includes("data-hf-edit-base-x") && !html.includes("data-hf-edit-base-y")) { return html; } - const script = ``; + const scriptBody = getPositionEditsRenderScript().replace(/<\/script/gi, "<\\/script"); + const script = ``; const bodyClose = html.search(/<\/body\s*>/i); if (bodyClose < 0) return `${html}${script}`; return `${html.slice(0, bodyClose)}${script}${html.slice(bodyClose)}`;