Skip to content

Commit 7a1e876

Browse files
Merge pull request #918 from heygen-com/refactor/unify-subcomp-inlining
fix: hold external sub-compositions in render mode + regenerate baseline
2 parents b6b6b8e + 54afecc commit 7a1e876

24 files changed

Lines changed: 10805 additions & 4578 deletions

File tree

packages/core/src/compiler/htmlBundler.ts

Lines changed: 27 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,12 @@ import {
77
parseHTMLContent,
88
stripEmbeddedRuntimeScripts,
99
} from "./htmlDocument";
10-
import {
11-
rewriteAssetPaths,
12-
rewriteCssAssetUrls,
13-
rewriteInlineStyleAssetUrls,
14-
} from "./rewriteSubCompPaths";
10+
// rewriteSubCompPaths functions are used by inlineSubCompositions (shared module)
1511
import { scopeCssToComposition, wrapScopedCompositionScript } from "./compositionScoping";
1612
import { validateHyperframeHtmlContract } from "./staticGuard";
1713
import { getHyperframeRuntimeScript } from "../generated/runtime-inline";
1814
import { readDeclaredDefaults } from "../runtime/getVariables";
15+
import { inlineSubCompositions } from "./inlineSubCompositions";
1916

2017
/** Resolve a relative path within projectDir, rejecting traversal outside it. */
2118
function safePath(projectDir: string, relativePath: string): string | null {
@@ -581,144 +578,36 @@ export async function bundleToSingleHtml(
581578
}
582579
}
583580

584-
// Inline sub-compositions
585-
const compStyleChunks: string[] = [];
586-
const compScriptChunks: string[] = [];
587-
const compExternalScriptSrcs: string[] = [];
588-
const compVariablesByComp: Record<string, Record<string, unknown>> = {};
581+
// Inline sub-compositions (via shared function)
589582
const trackedCompositionHosts = getBundledTrackedCompositionHosts(document);
590583
const hostIdentityByElement = assignBundledRuntimeCompositionIds(trackedCompositionHosts);
591584
const subCompositionHosts = trackedCompositionHosts.filter((host) =>
592585
host.hasAttribute("data-composition-src"),
593586
);
594-
for (const hostEl of subCompositionHosts) {
595-
const src = hostEl.getAttribute("data-composition-src");
596-
if (!src || !isRelativeUrl(src)) continue;
597-
const compPath = safePath(projectDir, src);
598-
const compHtml = compPath ? safeReadFile(compPath) : null;
599-
if (compHtml == null) {
600-
console.warn(`[Bundler] Composition file not found: ${src}`);
601-
continue;
602-
}
603-
604-
const compDoc = parseHTMLContent(compHtml);
605-
const hostIdentity = hostIdentityByElement.get(hostEl);
606-
const compId = hostIdentity?.authoredCompositionId || null;
607-
const runtimeCompId = hostIdentity?.runtimeCompositionId || compId || "";
608-
const contentRoot = compDoc.querySelector("template");
609-
const contentHtml = contentRoot ? contentRoot.innerHTML || "" : compDoc.body.innerHTML || "";
610-
const contentDoc = parseHTMLContent(contentHtml);
611-
const innerRoot = compId
612-
? contentDoc.querySelector(`[data-composition-id="${compId}"]`)
613-
: contentDoc.querySelector("[data-composition-id]");
614-
const inferredCompId = innerRoot?.getAttribute("data-composition-id")?.trim() || "";
615-
const authoredRootId = innerRoot?.getAttribute("id")?.trim() || null;
616-
const scopeCompId = compId || inferredCompId;
617-
const runtimeScope = runtimeCompId
618-
? cssAttributeSelector("data-composition-id", runtimeCompId)
619-
: "";
620-
const mergedVariables = runtimeCompId
621-
? {
622-
...readDeclaredDefaults(compDoc.documentElement),
623-
...parseHostVariableValues(hostEl),
624-
}
625-
: {};
626-
if (runtimeCompId && Object.keys(mergedVariables).length > 0) {
627-
compVariablesByComp[runtimeCompId] = mergedVariables;
628-
}
629-
630-
// When a sub-composition is a full HTML document (no <template>), styles
631-
// and scripts in <head> are not part of contentDoc (which only has body
632-
// content). Extract them so backgrounds, positioning, fonts, and library
633-
// scripts (e.g. GSAP CDN) are not silently dropped.
634-
if (!contentRoot && compDoc.head) {
635-
for (const s of [...compDoc.head.querySelectorAll("style")]) {
636-
const css = rewriteCssAssetUrls(s.textContent || "", src);
637-
compStyleChunks.push(
638-
scopeCompId ? scopeCssToComposition(css, scopeCompId, runtimeScope, authoredRootId) : css,
639-
);
640-
}
641-
for (const s of [...compDoc.head.querySelectorAll("script")]) {
642-
const externalSrc = (s.getAttribute("src") || "").trim();
643-
if (externalSrc && !compExternalScriptSrcs.includes(externalSrc)) {
644-
compExternalScriptSrcs.push(externalSrc);
645-
}
646-
}
647-
}
648-
649-
for (const s of [...contentDoc.querySelectorAll("style")]) {
650-
const css = rewriteCssAssetUrls(s.textContent || "", src);
651-
compStyleChunks.push(
652-
scopeCompId ? scopeCssToComposition(css, scopeCompId, runtimeScope, authoredRootId) : css,
653-
);
654-
s.remove();
655-
}
656-
for (const s of [...contentDoc.querySelectorAll("script")]) {
657-
const externalSrc = (s.getAttribute("src") || "").trim();
658-
if (externalSrc) {
659-
// External CDN/remote script — collect for deduped injection into the document.
660-
// Do NOT try to inline the content (external scripts have no innerHTML).
661-
if (!compExternalScriptSrcs.includes(externalSrc)) {
662-
compExternalScriptSrcs.push(externalSrc);
663-
}
664-
} else {
665-
compScriptChunks.push(
666-
scopeCompId
667-
? wrapScopedCompositionScript(
668-
s.textContent || "",
669-
scopeCompId,
670-
"[HyperFrames] composition script error:",
671-
runtimeScope,
672-
runtimeCompId || scopeCompId,
673-
authoredRootId,
674-
)
675-
: `(function(){ try { ${s.textContent || ""} } catch (_err) { console.error('[HyperFrames] composition script error:', _err); } })();`,
676-
);
677-
}
678-
s.remove();
679-
}
680-
681-
// Rewrite relative asset paths before inlining so ../foo.svg from
682-
// compositions/ resolves correctly when the content moves to root.
683-
const assetEls = innerRoot
684-
? innerRoot.querySelectorAll("[src], [href]")
685-
: contentDoc.querySelectorAll("[src], [href]");
686-
rewriteAssetPaths(
687-
assetEls,
688-
src,
689-
(el: Element, attr: string) => el.getAttribute(attr),
690-
(el: Element, attr: string, val: string) => {
691-
el.setAttribute(attr, val);
692-
},
693-
);
694-
const styledEls = innerRoot
695-
? innerRoot.querySelectorAll("[style]")
696-
: contentDoc.querySelectorAll("[style]");
697-
rewriteInlineStyleAssetUrls(
698-
styledEls,
699-
src,
700-
(el: Element) => el.getAttribute("style"),
701-
(el: Element, val: string) => {
702-
el.setAttribute("style", val);
703-
},
704-
);
705-
706-
if (innerRoot) {
707-
const innerW = innerRoot.getAttribute("data-width");
708-
const innerH = innerRoot.getAttribute("data-height");
709-
if (innerW && !hostEl.getAttribute("data-width")) hostEl.setAttribute("data-width", innerW);
710-
if (innerH && !hostEl.getAttribute("data-height")) hostEl.setAttribute("data-height", innerH);
711-
innerRoot.setAttribute("data-composition-file", src);
712-
for (const child of [...innerRoot.querySelectorAll("style, script")]) child.remove();
713-
const preparedInnerRoot = prepareFlattenedInnerRoot(innerRoot);
714-
hostEl.innerHTML = preparedInnerRoot.outerHTML || "";
715-
} else {
716-
for (const child of [...contentDoc.querySelectorAll("style, script")]) child.remove();
717-
hostEl.innerHTML = contentDoc.body.innerHTML || "";
718-
}
719-
hostEl.setAttribute("data-composition-file", src);
720-
hostEl.removeAttribute("data-composition-src");
721-
}
587+
const subCompResult = inlineSubCompositions(document, subCompositionHosts, {
588+
resolveHtml: (srcPath: string) => {
589+
if (!isRelativeUrl(srcPath)) return null;
590+
const compPath = safePath(projectDir, srcPath);
591+
return compPath ? safeReadFile(compPath) : null;
592+
},
593+
parseHtml: parseHTMLContent,
594+
hostIdentityMap: hostIdentityByElement,
595+
rewriteInlineStyles: true,
596+
flattenInnerRoot: prepareFlattenedInnerRoot,
597+
readVariableDefaults: readDeclaredDefaults,
598+
parseHostVariables: parseHostVariableValues,
599+
buildScopeSelector: (compId: string) => cssAttributeSelector("data-composition-id", compId),
600+
scriptErrorLabel: "[HyperFrames] composition script error:",
601+
onMissingComposition: (srcPath: string) => {
602+
console.warn(`[Bundler] Composition file not found: ${srcPath}`);
603+
},
604+
});
605+
const compStyleChunks: string[] = [...subCompResult.styles];
606+
const compScriptChunks: string[] = [...subCompResult.scripts];
607+
const compExternalScriptSrcs: string[] = [...subCompResult.externalScriptSrcs];
608+
const compVariablesByComp: Record<string, Record<string, unknown>> = {
609+
...subCompResult.variablesByComp,
610+
};
722611

723612
// Inline template compositions: inject <template id="X-template"> content into
724613
// matching empty host elements with data-composition-id="X" (no data-composition-src)

packages/core/src/compiler/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,10 @@ export {
3434

3535
// Composition isolation helpers
3636
export { scopeCssToComposition, wrapScopedCompositionScript } from "./compositionScoping";
37+
38+
// Sub-composition inlining (shared between bundler and producer)
39+
export {
40+
inlineSubCompositions,
41+
type InlineSubCompositionsOptions,
42+
type InlineSubCompositionsResult,
43+
} from "./inlineSubCompositions";

0 commit comments

Comments
 (0)