diff --git a/packages/core/src/compiler/compositionScoping.test.ts b/packages/core/src/compiler/compositionScoping.test.ts
index df3ce1d595..19c7b019d7 100644
--- a/packages/core/src/compiler/compositionScoping.test.ts
+++ b/packages/core/src/compiler/compositionScoping.test.ts
@@ -79,6 +79,59 @@ body { margin: 0; }
expect(fakeWindow.__captured).toEqual({ title: "Pro", price: "$29" });
});
+ it("routes the documented window.__hyperframes.getVariables() to the scoped variant too", () => {
+ // Regression: the docs (variables-and-media.md) show `window.__hyperframes.
+ // getVariables()`, but inside a sub-comp the scoped `window` proxy used to
+ // fall through to the HOST page's base __hyperframes, returning the wrong
+ // (or empty) variables — the bare `__hyperframes` param was the only form
+ // that worked. Both spellings must now resolve to this comp's variables.
+ const { document } = parseHTML(`
`);
+ const fakeWindow: Record = {
+ document,
+ __timelines: {},
+ __hfVariablesByComp: {
+ "card-1": { title: "Pro", price: "$29" },
+ "card-2": { title: "Enterprise", price: "Custom" },
+ },
+ __hyperframes: {
+ getVariables: () => ({ title: "TOP-LEVEL-LEAK" }),
+ fitTextFontSize: () => undefined,
+ },
+ };
+ const wrapped = wrapScopedCompositionScript(
+ `window.__captured = window.__hyperframes.getVariables();`,
+ "card-1",
+ );
+
+ new Function("window", wrapped)(fakeWindow);
+
+ expect(fakeWindow.__captured).toEqual({ title: "Pro", price: "$29" });
+ });
+
+ it("preserves non-getVariables members on window.__hyperframes (only getVariables is rescoped)", () => {
+ const { document } = parseHTML(``);
+ let fitCalled = false;
+ const fakeWindow: Record = {
+ document,
+ __timelines: {},
+ __hfVariablesByComp: { "card-1": { title: "Pro" } },
+ __hyperframes: {
+ getVariables: () => ({ title: "TOP-LEVEL-LEAK" }),
+ fitTextFontSize: () => {
+ fitCalled = true;
+ },
+ },
+ };
+ const wrapped = wrapScopedCompositionScript(
+ `window.__hyperframes.fitTextFontSize();`,
+ "card-1",
+ );
+
+ new Function("window", wrapped)(fakeWindow);
+
+ expect(fitCalled).toBe(true);
+ });
+
it("scoped getVariables reads from the runtime composition id when it differs", () => {
const { document } = parseHTML(``);
const fakeWindow: Record = {
diff --git a/packages/core/src/compiler/compositionScoping.ts b/packages/core/src/compiler/compositionScoping.ts
index be49f149a0..4315d770a9 100644
--- a/packages/core/src/compiler/compositionScoping.ts
+++ b/packages/core/src/compiler/compositionScoping.ts
@@ -396,6 +396,16 @@ export function wrapScopedCompositionScript(
? new Proxy(window, {
get: function(target, prop, receiver) {
if (prop === "__timelines") return __hfGetTimelineRegistry();
+ // Inside a sub-composition, __hyperframes is passed as a bare script
+ // param bound to the SCOPED variant (per-comp getVariables). But
+ // authors routinely write the documented window.__hyperframes.
+ // getVariables() form, which would otherwise fall through to the host
+ // page's base __hyperframes and return the WRONG (or empty) variables
+ // for this instance. Route it to the scoped variant too so both
+ // spellings resolve to this composition's own variables.
+ // (__hfScopedHyperframes is a hoisted var assigned below, before any
+ // sub-comp script -- the only code that reads this -- runs.)
+ if (prop === "__hyperframes") return __hfScopedHyperframes;
return Reflect.get(target, prop, target);
},
set: function(target, prop, value, receiver) {