feat(core): sub-composition variable render path

This commit is contained in:
James
2026-07-09 13:31:04 -07:00
parent 8de80bf369
commit a9901cb661
5 changed files with 62 additions and 2 deletions
+4 -1
View File
@@ -81,7 +81,10 @@ docs/plans/
# Local proof / test artifacts
qa-artifacts/
my-video/
.hyperframes/backup/
# Studio project state (hf-id backups, per-file state) — always runtime-generated,
# never committed. Broadened from .hyperframes/backup/ after preview-generated state
# leaked into a fixture commit.
.hyperframes/
examples/*
# Tracked OSS examples — negations override the blanket `examples/*` ignore.
!examples/aws-lambda
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { parseHTML } from "linkedom";
import { inlineSubCompositions } from "./inlineSubCompositions";
import { readDeclaredDefaults, parseHostVariableValues } from "../runtime/getVariables";
// Fixtures reference GSAP CDN but are never loaded in a real browser — resolveHtml is mocked.
@@ -358,3 +359,46 @@ describe("inlineSubCompositions #ID selector scoping divergence", () => {
);
});
});
describe("inlineSubCompositions variable defaults on a template sub-comp root div", () => {
const SUB_COMP_WITH_VAR = `<template id="card-template">
<div id="card" data-composition-id="card" data-width="1920" data-height="1080"
data-composition-variables='[{"id":"headline","type":"string","label":"Headline","default":"Hi there"}]'>
<h1 class="title" data-var-text="headline">Hi there</h1>
</div>
</template>`;
function hostDoc() {
const { document } = parseHTML(`<!DOCTYPE html><html><body>
<div data-composition-id="main">
<div data-composition-id="card" data-composition-src="card.html"
data-start="0" data-duration="4" data-track-index="0"></div>
</div></body></html>`);
return document;
}
it("aggregates defaults declared on the inner root div (template comps have no <html> to hold them)", () => {
const document = hostDoc();
const host = document.querySelector('[data-composition-src="card.html"]')!;
const result = inlineSubCompositions(document, [host], {
resolveHtml: () => SUB_COMP_WITH_VAR,
parseHtml: (h) => parseHTML(h).document,
readVariableDefaults: readDeclaredDefaults,
parseHostVariables: parseHostVariableValues,
});
expect(result.variablesByComp["card"]).toMatchObject({ headline: "Hi there" });
});
it("lets a per-instance host value override the declared default", () => {
const document = hostDoc();
const host = document.querySelector('[data-composition-src="card.html"]')!;
host.setAttribute("data-variable-values", JSON.stringify({ headline: "Overridden" }));
const result = inlineSubCompositions(document, [host], {
resolveHtml: () => SUB_COMP_WITH_VAR,
parseHtml: (h) => parseHTML(h).document,
readVariableDefaults: readDeclaredDefaults,
parseHostVariables: parseHostVariableValues,
});
expect(result.variablesByComp["card"]).toMatchObject({ headline: "Overridden" });
});
});
@@ -149,6 +149,7 @@ function defaultBuildScopeSelector(compId: string): string {
* 10. Remove `data-composition-src` from host
* 11. Inject the content into the host element
*/
// fallow-ignore-next-line complexity
export function inlineSubCompositions(
document: Document,
hosts: Element[],
@@ -236,10 +237,14 @@ export function inlineSubCompositions(
const scopeCompId = compId || inferredCompId;
const runtimeScope = runtimeCompId ? buildScopeSelector(runtimeCompId) : "";
// Variable merging (bundler feature)
// Variable merging (bundler feature). Read declared defaults from the
// document element (full-document sub-comps) AND the inner composition root
// (template/fragment sub-comps store their schema on the root div, not a
// synthetic <html>), then let per-instance host values override.
if (readVariableDefaults && parseHostVariables && runtimeCompId) {
const mergedVariables = {
...readVariableDefaults(compDoc.documentElement),
...(innerRoot ? readVariableDefaults(innerRoot) : {}),
...parseHostVariables(hostEl),
};
if (Object.keys(mergedVariables).length > 0) {
@@ -698,6 +698,10 @@ export async function loadExternalCompositions(
headStyles,
headScripts,
headLinks,
// TODO(template-var-carriers): reads `<html>` only. A template/fragment
// sub-comp that declares on its `[data-composition-id]` root div (the
// dual-carrier contract from #2081) loses its defaults on this lazy
// external-load path — see inlineSubCompositions for the fixed path.
declaredVariableDefaults: readDeclaredDefaults(doc.documentElement),
onDiagnostic: params.onDiagnostic,
});
+4
View File
@@ -782,6 +782,10 @@ export function extractCompositionMetadata(html: string): CompositionMetadata {
const durationStr = htmlEl.getAttribute("data-composition-duration");
const compositionDuration = durationStr ? parseFloat(durationStr) : null;
// TODO(template-var-carriers): reads `<html>` only. A template/fragment comp
// that declares variables on its `[data-composition-id]` root div (the
// dual-carrier contract from #2081) reports no variables when its metadata is
// extracted standalone (e.g. CLI --variables validation of a sub-comp file).
const variables = parseCompositionVariables(htmlEl);
return {