mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix: producer render diverges from preview for sub-composition root styling (#1886)
Fixes #1847 The producer's render path stripped a sub-composition's authored root element and inlined only its children, so any CSS anchored on that root (its id or classes) matched nothing in the compiled HTML even though it resolved fine in Studio preview. Changes: - Wire flattenInnerRoot into the producer's sub-composition inliner (packages/producer/src/services/htmlCompiler.ts) so its render-time DOM shape matches the preview bundler's. - Rewrite a bare root [data-composition-id="X"] box selector to a :has()/:not() pair that lands on exactly one of the host or the flattened wrapper (packages/core/src/compiler/compositionScoping.ts), avoiding double-applying additive properties like padding. - Restore the composition's own id onto the flattened wrapper when the host has no id of its own, an "anonymous" host (packages/core/src/compiler/inlineSubCompositions.ts). - Fix the runtime's startResolver to find a composition's start time through the post-inlining data-composition-file marker, not just data-composition-src or data-composition-id (packages/core/src/runtime/startResolver.ts). Also adds regression coverage for the literal issue #1847 repro (a class, not just an id, on the authored root, styled via a descendant selector), a test proving the runtime compositionLoader's anonymous-host path doesn't share this bug, and fixes stale test documentation and a misattributed code comment surfaced during review. Verified: 29-fixture Docker regression sweep on linux/amd64 (matching CI) run 3x clean, 967/967 core unit tests, full CI green.
This commit is contained in:
@@ -920,6 +920,68 @@ describe("template-wrapped sub-composition media offsets", () => {
|
||||
expect(compiled.html).toContain("__hfNormalizeSelector");
|
||||
});
|
||||
|
||||
it("resolves a class selector on the authored root wrapper itself (issue #1847 repro)", async () => {
|
||||
// The original bug report: a sub-composition root authored as
|
||||
// `<div id="scene-root" class="scene-wrapper">` styled via
|
||||
// `.scene-wrapper .title { color: red }`. Class-based descendant
|
||||
// selectors anchored on the authored root's own class only resolve if
|
||||
// the root survives as a real element in the render DOM, not just via
|
||||
// id-selector rewriting to [data-hf-authored-id].
|
||||
const projectDir = mkdtempSync(join(tmpdir(), "hf-class-wrapper-"));
|
||||
const compositionsDir = join(projectDir, "compositions");
|
||||
mkdirSync(compositionsDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(projectDir, "index.html"),
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<div id="root" data-composition-id="root" data-start="0" data-width="1920" data-height="1080" data-duration="3">
|
||||
<div
|
||||
id="scene-host"
|
||||
data-composition-id="scene"
|
||||
data-composition-src="compositions/scene.html"
|
||||
data-start="0"
|
||||
data-duration="3"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["root"] = { duration: () => 3 };
|
||||
</script>
|
||||
</body>
|
||||
</html>`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(compositionsDir, "scene.html"),
|
||||
`<template id="scene-template">
|
||||
<div id="scene-root" class="scene-wrapper" data-composition-id="scene" data-width="1920" data-height="1080" data-duration="3">
|
||||
<div class="title">ISSUE 1847 REPRO</div>
|
||||
<style>
|
||||
.scene-wrapper { background: #111; }
|
||||
.scene-wrapper .title { color: red; }
|
||||
</style>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["scene"] = { duration: () => 3 };
|
||||
</script>
|
||||
</div>
|
||||
</template>`,
|
||||
);
|
||||
|
||||
const compiled = await compileForRender(projectDir, join(projectDir, "index.html"), projectDir);
|
||||
const { document } = parseHTML(compiled.html);
|
||||
const host = document.querySelector("#scene-host");
|
||||
|
||||
const wrapper = host?.querySelector(".scene-wrapper");
|
||||
expect(wrapper).not.toBeNull();
|
||||
expect(wrapper?.getAttribute("data-hf-authored-id")).toBe("scene-root");
|
||||
expect(wrapper?.querySelector(".title")?.textContent).toBe("ISSUE 1847 REPRO");
|
||||
// The authored class selector round-trips unmodified: no id rewriting
|
||||
// is needed for a class selector, only the wrapper element surviving.
|
||||
expect(compiled.html).toContain(".scene-wrapper .title");
|
||||
});
|
||||
|
||||
it("preserves the inferred composition boundary when the host has no composition id", async () => {
|
||||
const projectDir = mkdtempSync(join(tmpdir(), "hf-anonymous-host-"));
|
||||
const compositionsDir = join(projectDir, "compositions");
|
||||
@@ -954,7 +1016,12 @@ describe("template-wrapped sub-composition media offsets", () => {
|
||||
const host = document.querySelector("#scene-host");
|
||||
|
||||
expect(host?.getAttribute("data-composition-id")).toBeNull();
|
||||
expect(host?.querySelector('[data-composition-id="scene"] .title')?.textContent).toBe("Scene");
|
||||
// The host has no data-composition-id of its own, but the composition's
|
||||
// own id is restored onto the flattened wrapper, so root-scoped
|
||||
// selectors and self-referencing scripts still resolve.
|
||||
const wrapper = host?.querySelector("[data-hf-inner-root]");
|
||||
expect(wrapper?.getAttribute("data-composition-id")).toBe("scene");
|
||||
expect(wrapper?.querySelector(".title")?.textContent).toBe("Scene");
|
||||
expect(compiled.html).toContain('var __hfCompId = "scene";');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,10 @@ import {
|
||||
type ResolvedDuration,
|
||||
type UnresolvedElement,
|
||||
} from "@hyperframes/core";
|
||||
import { inlineSubCompositions as inlineSubCompositionsShared } from "@hyperframes/core/compiler";
|
||||
import {
|
||||
inlineSubCompositions as inlineSubCompositionsShared,
|
||||
prepareFlattenedInnerRoot,
|
||||
} from "@hyperframes/core/compiler";
|
||||
import {
|
||||
checkSubCompositionUsability,
|
||||
type ParsableDocumentLike,
|
||||
@@ -748,7 +751,14 @@ function inlineSubCompositions(
|
||||
},
|
||||
parseHtml: (htmlStr: string) => parseHTML(htmlStr).document as unknown as Document,
|
||||
scriptErrorLabel: "[Compiler] Composition script failed",
|
||||
compoundAuthoredRoot: true,
|
||||
// Preserve the authored root wrapper as a child of the host, matching
|
||||
// the preview bundler's shape (htmlBundler.ts's prepareFlattenedInnerRoot,
|
||||
// which the runtime compositionLoader mirrors with its own copy for the
|
||||
// live-loaded case). Without this, the wrapper element (and its
|
||||
// class/id) is discarded and any CSS anchored on it —
|
||||
// `.wrapper-class .title`, `#wrapper-id` — is dead at render time even
|
||||
// though it works in preview.
|
||||
flattenInnerRoot: prepareFlattenedInnerRoot as (innerRoot: Element) => Element,
|
||||
onMissingComposition: (srcPath: string, reason?: string) => {
|
||||
// In the render path this is normally unreachable — compileForRender
|
||||
// calls assertSubCompositionsUsable() before any of this runs, so a
|
||||
@@ -761,18 +771,6 @@ function inlineSubCompositions(
|
||||
},
|
||||
);
|
||||
|
||||
// Set data-hf-authored-id on host elements so the scoped script proxy
|
||||
// can rewrite #id selectors (e.g. #us-map → [data-hf-authored-id="us-map"]).
|
||||
// Unlike flattenInnerRoot (which changes DOM structure and breaks baselines),
|
||||
// this preserves the existing innerHTML-based inlining while enabling the
|
||||
// authored-id selector contract.
|
||||
for (const hostEl of hosts) {
|
||||
const compId = hostEl.getAttribute("data-composition-id");
|
||||
if (compId && !hostEl.getAttribute("data-hf-authored-id")) {
|
||||
hostEl.setAttribute("data-hf-authored-id", compId);
|
||||
}
|
||||
}
|
||||
|
||||
// Producer-specific: set explicit pixel dimensions on host elements so
|
||||
// children using width/height: 100% resolve correctly. The runtime does
|
||||
// this automatically but compiled HTML needs it inline.
|
||||
|
||||
Reference in New Issue
Block a user