From ea3f7273e7976efc769304483c4abde828a4a596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 7 May 2026 02:49:30 +0200 Subject: [PATCH] fix(core): scope duplicate getElementById lookups (#655) --- .../src/compiler/compositionScoping.test.ts | 50 +++++++++++++++++++ .../core/src/compiler/compositionScoping.ts | 25 +++++++--- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/packages/core/src/compiler/compositionScoping.test.ts b/packages/core/src/compiler/compositionScoping.test.ts index 4c1a0189b..49208d30c 100644 --- a/packages/core/src/compiler/compositionScoping.test.ts +++ b/packages/core/src/compiler/compositionScoping.test.ts @@ -158,6 +158,56 @@ window.__timelines.scene = tl; expect(gsapTargets).toEqual([["Scene"], ["Scene"]]); }); + it("scopes getElementById when duplicate IDs exist across composition roots", () => { + const { document } = parseHTML(` +
+
+ `); + const fakeWindow = { + document, + __selectedComp: "", + __timelines: {}, + }; + const wrapped = wrapScopedCompositionScript( + ` +window.__selectedComp = + document.getElementById("gl-canvas") + ?.closest("[data-composition-id]") + ?.getAttribute("data-composition-id") || "null"; +`, + "scene-b", + ); + + new Function("window", wrapped)(fakeWindow); + + expect(fakeWindow.__selectedComp).toBe("scene-b"); + }); + + it("scopes getElementById for IDs that need CSS selector escaping", () => { + const { document } = parseHTML(` +
+
+ `); + const fakeWindow = { + document, + __selectedComp: "", + __timelines: {}, + }; + const wrapped = wrapScopedCompositionScript( + ` +window.__selectedComp = + document.getElementById("clip:1") + ?.closest("[data-composition-id]") + ?.getAttribute("data-composition-id") || "null"; +`, + "scene-b", + ); + + new Function("window", wrapped)(fakeWindow); + + expect(fakeWindow.__selectedComp).toBe("scene-b"); + }); + it("reads scoped proxy accessors with the original target receiver", () => { const root = { contains(node: unknown) { diff --git a/packages/core/src/compiler/compositionScoping.ts b/packages/core/src/compiler/compositionScoping.ts index bc7153011..5fec5b5c2 100644 --- a/packages/core/src/compiler/compositionScoping.ts +++ b/packages/core/src/compiler/compositionScoping.ts @@ -141,17 +141,30 @@ export function wrapScopedCompositionScript( var matches = __hfQueryAll(selector); return matches[0] || null; }; + var __hfGetElementById = function(id) { + var found = window.document.getElementById(id); + if (found && __hfContains(found)) return found; + var root = __hfFindRoot(); + if (!root) return found || null; + var idValue = id + ""; + if (root.id === idValue) return root; + if (typeof root.querySelector !== "function") return null; + if (typeof CSS !== "undefined" && CSS && typeof CSS.escape === "function") { + try { + return root.querySelector("#" + CSS.escape(idValue)) || null; + } catch {} + } + try { + return root.querySelector('[id="' + __hfEscapeAttr(idValue) + '"]') || null; + } catch {} + return null; + }; var __hfScopedDocument = typeof Proxy === "function" ? new Proxy(window.document, { get: function(target, prop, receiver) { if (prop === "querySelector") return __hfQueryOne; if (prop === "querySelectorAll") return __hfQueryAll; - if (prop === "getElementById") { - return function(id) { - var found = target.getElementById(id); - return found && __hfContains(found) ? found : null; - }; - } + if (prop === "getElementById") return __hfGetElementById; var value = Reflect.get(target, prop, target); return typeof value === "function" ? value.bind(target) : value; },