fix(cli): surface keyframes from scripts and styles inside <template> (#2374)

Sub-compositions are required to wrap markup, script, and style in <template>, but template content is an inert DocumentFragment that document-level querySelectorAll does not traverse — so 'hyperframes keyframes' silently surfaced zero tweens and zero CSS keyframes for every spec-conformant sub-composition. Extract from the document and every template content fragment (nested templates included, walked iteratively). Documents the extraction ordering contract and the deliberately document-root-only sub-composition discovery scan; pins template, multi-template, nested-template, and mixed-script cases with tests.
This commit is contained in:
Miguel Ángel
2026-07-13 20:47:50 -04:00
committed by GitHub
parent 9639824f4e
commit b231b6f963
2 changed files with 140 additions and 4 deletions
+28 -4
View File
@@ -93,17 +93,37 @@ interface SurfacedComposition {
// ── GSAP extraction ──────────────────────────────────────────────────────────
function inlineScriptText(html: string): string {
// <template> content lives in an inert DocumentFragment that document-level
// querySelectorAll does not traverse — and sub-compositions are REQUIRED to wrap
// markup + script in <template>. Query the document and every template fragment
// (including templates nested inside another template's fragment, walked
// iteratively), or template-wrapped compositions surface zero tweens/keyframes.
// Ordering contract: document-level matches come first, then template contents
// in discovery order — NOT strict source order when a file mixes top-level and
// template scripts. Spec-conformant sub-compositions keep everything in one
// template, so mixed files only need to parse, not preserve interleaving.
function queryIncludingTemplates(html: string, selector: string): Element[] {
const doc = new DOMParser().parseFromString(html, "text/html");
return Array.from(doc.querySelectorAll("script"))
const roots: Array<{ querySelectorAll(s: string): Iterable<Element> }> = [doc];
const queue = Array.from(doc.querySelectorAll("template")) as HTMLTemplateElement[];
while (queue.length > 0) {
const content = queue.shift()!.content;
if (!content) continue;
roots.push(content);
queue.push(...(Array.from(content.querySelectorAll("template")) as HTMLTemplateElement[]));
}
return roots.flatMap((root) => Array.from(root.querySelectorAll(selector)));
}
function inlineScriptText(html: string): string {
return queryIncludingTemplates(html, "script")
.filter((s) => !s.getAttribute("src"))
.map((s) => s.textContent ?? "")
.join("\n");
}
function inlineStyleText(html: string): string {
const doc = new DOMParser().parseFromString(html, "text/html");
return Array.from(doc.querySelectorAll("style"))
return queryIncludingTemplates(html, "style")
.map((s) => s.textContent ?? "")
.join("\n");
}
@@ -560,6 +580,10 @@ function collectCompositions(indexPath: string): SurfacedComposition[] {
surfaceComposition(html, basename(indexPath), basename(indexPath)),
];
// Deliberately document-root only (NOT queryIncludingTemplates): host divs
// with [data-composition-src] live in the orchestrating index.html light
// tree, never inside <template>. Widening this scan would change discovery
// semantics, not fix a gap.
const doc = new DOMParser().parseFromString(html, "text/html");
for (const div of Array.from(doc.querySelectorAll("[data-composition-src]"))) {
const src = div.getAttribute("data-composition-src");