fix(engine): inject sub-composition variables on the render path (#2066)

render left window.__hyperframes.getVariables() empty inside every
sub-composition mounted via data-composition-src, so each instance rendered
its declared JS defaults instead of the per-instance data-variable-values.
preview/snapshot injected them correctly, so the composition looked right in
every authoring/QA surface and then rendered wrong content silently (exit 0).
Any template-library workflow (reusable sub-comp scenes parametrized per
video) shipped placeholder/default text in the final MP4.

The plumbing already existed on main: htmlCompiler passes
readVariableDefaults/parseHostVariables and populates result.variablesByComp,
and the CSS-custom-property path (emitRootCompositionVariableStyles) reaches
the render. But the render compiler emitted only the CSS vars and never the
JS table window.__hfVariablesByComp that the scoped getVariables reads, while
the preview bundler (htmlBundler) did -- so getVariables() returned {} only
during render.

Fix, so the paths cannot drift again: buildVariablesByCompScript, colocated
with the reader in compositionScoping.ts and shared by both compile paths.
htmlBundler now calls it instead of an inline string; htmlCompiler injects it
before the inlined sub-comp scripts, using the already-populated
result.variablesByComp.

Verified end-to-end: a sub-comp painting its background from a color variable
now renders the injected value under render, matching snapshot; previously it
rendered the default. 3 new producer tests; 89 htmlCompiler + core-compiler
tests pass.

Closes #2064.
This commit is contained in:
Miguel Ángel
2026-07-08 14:23:40 -04:00
committed by GitHub
parent 17b852784b
commit 41ad5b4690
5 changed files with 112 additions and 8 deletions
@@ -523,3 +523,24 @@ ${source.replace(/<\/(script)/gi, "<\\/$1")}
export function wrapInlineScriptWithErrorBoundary(source: string, errorLabel: string): string {
return `(function(){ try { Function(${JSON.stringify(source)}).call(window); } catch (_err) { console.error(${JSON.stringify(errorLabel)}, _err); } })();`;
}
/**
* Build the statement that populates `window.__hfVariablesByComp` — the table
* the scoped `getVariables` above reads. Returns `null` when there are no
* per-instance values.
*
* The WRITER lives next to the READER (the scoped `getVariables` in
* `wrapScopedCompositionScript`) on purpose: every compile path that wraps the
* reader MUST also emit this writer before the sub-comp scripts run. The
* render compiler (`htmlCompiler`) inlined the reader scripts but never emitted
* the writer while the preview bundler (`htmlBundler`) did, so
* `getVariables()` returned `{}` only during render — parametrized sub-comps
* silently shipped blank/default text in the final MP4 while snapshot QA passed
* (issue #2064). Both callers now share this one builder so they can't drift.
*/
export function buildVariablesByCompScript(
variablesByComp: Record<string, Record<string, unknown>>,
): string | null {
if (!variablesByComp || Object.keys(variablesByComp).length === 0) return null;
return `window.__hfVariablesByComp = Object.assign({}, window.__hfVariablesByComp || {}, ${JSON.stringify(variablesByComp)});`;
}
+4 -4
View File
@@ -14,6 +14,7 @@ import {
} from "./htmlDocument";
// rewriteSubCompPaths functions are used by inlineSubCompositions (shared module)
import {
buildVariablesByCompScript,
scopeCssToComposition,
wrapInlineScriptWithErrorBoundary,
wrapScopedCompositionScript,
@@ -939,10 +940,9 @@ export async function bundleToSingleHtml(
style.textContent = compStyleChunks.join("\n\n");
document.head.appendChild(style);
}
if (Object.keys(compVariablesByComp).length > 0) {
compScriptChunks.unshift(
`window.__hfVariablesByComp = Object.assign({}, window.__hfVariablesByComp || {}, ${JSON.stringify(compVariablesByComp)});`,
);
const variablesByCompScript = buildVariablesByCompScript(compVariablesByComp);
if (variablesByCompScript) {
compScriptChunks.unshift(variablesByCompScript);
}
if (compScriptChunks.length) {
const compScript = document.createElement("script");
+5 -1
View File
@@ -52,7 +52,11 @@ export {
} from "./staticGuard";
// Composition isolation helpers
export { scopeCssToComposition, wrapScopedCompositionScript } from "./compositionScoping";
export {
buildVariablesByCompScript,
scopeCssToComposition,
wrapScopedCompositionScript,
} from "./compositionScoping";
// Sub-composition inlining (shared between bundler and producer)
export {