fix(core): load head styles and scripts from non-template sub-compositions

When a sub-composition is a full HTML document (not wrapped in
<template>), the composition loader only extracted <style> and
<script> elements from within the [data-composition-id] root
element. Any CSS in <head> (backgrounds, positioning, fonts) or
scripts in <head> (GSAP CDN) were silently dropped.

This caused the iris-wipe eval (prompt 25, scored 1.0/5) to
render entirely black — the scene background colors and absolute
positioning were defined in <head><style> and never loaded.

Now extracts <head> styles and scripts from non-template sub-comp
documents and injects them before the content-level assets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Miguel Ángel
2026-04-07 14:54:26 +00:00
committed by Miguel Ángel
co-authored by Claude Opus 4.6
parent 0d33238381
commit df2cf47d20
+49 -1
View File
@@ -81,6 +81,10 @@ async function mountCompositionContent(params: {
injectedStyles: HTMLStyleElement[];
injectedScripts: HTMLScriptElement[];
parseDimensionPx: (value: string | null) => string | null;
/** Extra <style> elements from the parsed document <head> (non-template sub-compositions). */
headStyles?: HTMLStyleElement[];
/** Extra <script> elements from the parsed document <head> (non-template sub-compositions). */
headScripts?: HTMLScriptElement[];
onDiagnostic?: (payload: {
code: string;
details: Record<string, string | number | boolean | null | string[]>;
@@ -98,6 +102,17 @@ async function mountCompositionContent(params: {
}
const contentNode = innerRoot ?? params.sourceNode;
// Inject <head> styles from non-template sub-compositions first (they define
// element styles like backgrounds and positioning that the composition needs).
if (params.headStyles) {
for (const style of params.headStyles) {
const clonedStyle = style.cloneNode(true);
if (!(clonedStyle instanceof HTMLStyleElement)) continue;
document.head.appendChild(clonedStyle);
params.injectedStyles.push(clonedStyle);
}
}
const styles = Array.from(contentNode.querySelectorAll<HTMLStyleElement>("style"));
for (const style of styles) {
const clonedStyle = style.cloneNode(true);
@@ -106,8 +121,27 @@ async function mountCompositionContent(params: {
params.injectedStyles.push(clonedStyle);
}
// Collect head scripts first (e.g. GSAP CDN loaded in <head> of non-template sub-comps),
// then content scripts. Head scripts must execute before content scripts.
const headScriptPayloads: PendingScript[] = [];
if (params.headScripts) {
for (const script of params.headScripts) {
const scriptType = script.getAttribute("type")?.trim() ?? "";
const scriptSrc = script.getAttribute("src")?.trim() ?? "";
if (scriptSrc) {
const resolvedSrc = resolveScriptSourceUrl(scriptSrc, params.compositionUrl);
headScriptPayloads.push({ kind: "external", src: resolvedSrc, type: scriptType });
} else {
const scriptText = script.textContent?.trim() ?? "";
if (scriptText) {
headScriptPayloads.push({ kind: "inline", content: scriptText, type: scriptType });
}
}
}
}
const scripts = Array.from(contentNode.querySelectorAll<HTMLScriptElement>("script"));
const scriptPayloads: PendingScript[] = [];
const scriptPayloads: PendingScript[] = [...headScriptPayloads];
for (const script of scripts) {
const scriptType = script.getAttribute("type")?.trim() ?? "";
const scriptSrc = script.getAttribute("src")?.trim() ?? "";
@@ -287,6 +321,18 @@ export async function loadExternalCompositions(
)
: null) ?? doc.querySelector<HTMLTemplateElement>("template");
const sourceNode = template ? template.content : doc.body;
// When loading a non-template sub-composition (full HTML document),
// extract <style> and <script> elements from the parsed document's
// <head>. These contain critical CSS (backgrounds, positioning, fonts)
// and library scripts (e.g. GSAP CDN) that would otherwise be lost
// because mountCompositionContent only looks inside the composition
// root element.
const headStyles = !template
? Array.from(doc.head.querySelectorAll<HTMLStyleElement>("style"))
: undefined;
const headScripts = !template
? Array.from(doc.head.querySelectorAll<HTMLScriptElement>("script"))
: undefined;
await mountCompositionContent({
host,
hostCompositionId,
@@ -298,6 +344,8 @@ export async function loadExternalCompositions(
injectedStyles: params.injectedStyles,
injectedScripts: params.injectedScripts,
parseDimensionPx: params.parseDimensionPx,
headStyles,
headScripts,
onDiagnostic: params.onDiagnostic,
});
} catch (error) {