From 46602d75f4a3f70803b5d1a89cded3f72c4a2915 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Fri, 10 Jul 2026 16:55:19 -0700 Subject: [PATCH 1/2] fix(sdk): include template GSAP scripts in resolver parity --- packages/sdk/src/document.ts | 44 ++++++++++------------- packages/sdk/src/engine/model.ts | 34 ++++++++++++++++-- packages/sdk/src/session.template.test.ts | 16 +++++++++ packages/sdk/src/session.ts | 14 ++++++-- 4 files changed, 76 insertions(+), 32 deletions(-) diff --git a/packages/sdk/src/document.ts b/packages/sdk/src/document.ts index 97c6a4a88..24da6051b 100644 --- a/packages/sdk/src/document.ts +++ b/packages/sdk/src/document.ts @@ -14,6 +14,7 @@ import { parseGsapScriptAcornForWrite } from "@hyperframes/core/gsap-parser-acor import { findRoot, getElementStyles, + getGsapScripts, getOwnText, isNewHostBoundary, querySelectorAllDeep, @@ -67,22 +68,22 @@ function parseLocatedCached(script: string): Array<{ id: string; selector: strin */ function buildAnimationIdMap(document: Document): Map { const map = new Map(); - const script = extractGsapScript(document); - if (!script) return map; - for (const { id, selector } of parseLocatedCached(script)) { - if (!selector) continue; - let matches: Element[] = []; - try { - matches = querySelectorAllDeep(document, selector); - } catch { - continue; // selector not valid for querySelectorAll — skip - } - for (const el of matches) { - const hfId = el.getAttribute("data-hf-id"); - if (!hfId) continue; - const list = map.get(hfId); - if (list) list.push(id); - else map.set(hfId, [id]); + for (const script of getGsapScripts(document)) { + for (const { id, selector } of parseLocatedCached(script)) { + if (!selector) continue; + let matches: Element[] = []; + try { + matches = querySelectorAllDeep(document, selector); + } catch { + continue; // selector not valid for querySelectorAll — skip + } + for (const el of matches) { + const hfId = el.getAttribute("data-hf-id"); + if (!hfId) continue; + const list = map.get(hfId); + if (list) list.push(id); + else map.set(hfId, [id]); + } } } return map; @@ -198,16 +199,7 @@ function buildElement( // fallow-ignore-next-line complexity function extractGsapScript(doc: Document): string | null { - // GSAP script is the first + + `); + + const animationIds = comp.getElement("hf-line")?.animationIds ?? []; + expect(animationIds).toHaveLength(1); + expect(comp.getAllAnimationIds()).toEqual(new Set(animationIds)); + }); }); // The authored sub-comp form `hyperframes add` scaffolds: the composition id is diff --git a/packages/sdk/src/session.ts b/packages/sdk/src/session.ts index d84d9c07c..3a98ae124 100644 --- a/packages/sdk/src/session.ts +++ b/packages/sdk/src/session.ts @@ -35,7 +35,12 @@ import type { PersistAdapter, PreviewAdapter } from "./adapters/types.js"; import { parseMutable } from "./engine/model.js"; import type { ParsedDocument } from "./engine/model.js"; import { applyOp, validateOp, type MutationResult } from "./engine/mutate.js"; -import { getGsapScript, resolveScoped, declarationElement } from "./engine/model.js"; +import { + getGsapScript, + getGsapScripts, + resolveScoped, + declarationElement, +} from "./engine/model.js"; import { extractGsapLabels } from "@hyperframes/core/gsap-parser-acorn"; import { stripEmbeddedRuntimeScripts } from "@hyperframes/core/compiler/html-document"; import { parseStartExpression } from "@hyperframes/core/runtime/start-expression"; @@ -555,8 +560,11 @@ class CompositionImpl implements Composition { } getAllAnimationIds(): Set { - const script = getGsapScript(this.parsed.document); - return script ? parsedAnimationIds(script) : new Set(); + const ids = new Set(); + for (const script of getGsapScripts(this.parsed.document)) { + for (const id of parsedAnimationIds(script)) ids.add(id); + } + return ids; } // ── Selection API ──────────────────────────────────────────────────────────── From a61f7de8d79255da5a79730959a8ac5614415db9 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Fri, 10 Jul 2026 17:36:09 -0700 Subject: [PATCH 2/2] fix(sdk): align template script traversal --- packages/parsers/src/hfIds.ts | 43 +++++++++++++++--- packages/parsers/src/htmlParser.test.ts | 55 ++++++++++++++++++++++++ packages/parsers/src/htmlParser.ts | 28 +++++++----- packages/sdk/src/document.ts | 39 +++++++++++------ packages/sdk/src/engine/model.ts | 36 ++++++---------- packages/sdk/src/session.timings.test.ts | 20 +++++++++ packages/sdk/src/session.ts | 37 ++++++++-------- 7 files changed, 186 insertions(+), 72 deletions(-) diff --git a/packages/parsers/src/hfIds.ts b/packages/parsers/src/hfIds.ts index f2020c61d..0e4329f3f 100644 --- a/packages/parsers/src/hfIds.ts +++ b/packages/parsers/src/hfIds.ts @@ -114,15 +114,49 @@ export function mintHfId(el: Element, assigned: Set): string { * inner id would be duplicated across every clone. Form B is distinguished from * a clone-source by the presence of a direct `[data-composition-id]` child. */ +function getChildElements(parent: Element): Element[] { + const directChildren = Array.from(parent.children); + if (directChildren.length || parent.tagName.toLowerCase() !== "template") return directChildren; + const content = (parent as HTMLTemplateElement).content; + if (content?.children.length) return Array.from(content.children); + return directChildren; +} + export function isCompositionTemplate(el: Element): boolean { if (el.tagName.toLowerCase() !== "template") return false; if (el.getAttribute("data-composition-id") !== null) return true; - for (const child of Array.from(el.children)) { + for (const child of getChildElements(el)) { if (child.getAttribute("data-composition-id") !== null) return true; } return false; } +/** + * Walk document-order descendants, descending through composition templates + * while keeping plain templates inert. linkedom's querySelectorAll does not + * expose template contents, so callers that model the served composition use + * this traversal instead. + */ +export function walkCompositionDescendants( + root: Document | Element, + visit: (el: Element) => void, +): void { + const rootElement: Element | null = + root.nodeType === 9 ? (root as Document).documentElement : (root as Element); + if (!rootElement) return; + + const walk = (parent: Element): void => { + for (const child of getChildElements(parent)) { + const isTemplate = child.tagName.toLowerCase() === "template"; + if (isTemplate && !isCompositionTemplate(child)) continue; + visit(child); + walk(child); + } + }; + + walk(rootElement); +} + /** * Document-order walk of every element under `root`, descending into * composition `