mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix: isolate studio sub-composition previews
This commit is contained in:
@@ -473,6 +473,7 @@ describe("template-wrapped sub-composition media offsets", () => {
|
||||
join(projectDir, "index.html"),
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
@@ -506,6 +507,8 @@ describe("template-wrapped sub-composition media offsets", () => {
|
||||
data-height="360"
|
||||
data-duration="4"
|
||||
>
|
||||
<style>.title { opacity: 0; }</style>
|
||||
<h1 class="title">Scene</h1>
|
||||
<video
|
||||
id="scene-video"
|
||||
src="../assets/clip.mp4"
|
||||
@@ -594,4 +597,18 @@ describe("template-wrapped sub-composition media offsets", () => {
|
||||
end: 25.5,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the sub-composition root in compiled render HTML", async () => {
|
||||
const { projectDir, indexPath } = writeTemplateWrappedProject(
|
||||
'data-start="20" data-duration="6" data-width="640" data-height="360"',
|
||||
'data-start="1.5" data-duration="4"',
|
||||
);
|
||||
|
||||
const compiled = await compileForRender(projectDir, indexPath, projectDir);
|
||||
|
||||
expect(compiled.html).toContain('id="scene-host"');
|
||||
expect(compiled.html).toContain('data-composition-id="scene" data-start="0"');
|
||||
expect(compiled.html).toContain('[data-composition-id="scene"] .title');
|
||||
expect(compiled.html).toContain("new Proxy(window.document");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
import { readFileSync, existsSync, mkdirSync } from "fs";
|
||||
import { join, dirname, resolve } from "path";
|
||||
import { parseHTML } from "linkedom";
|
||||
import postcss from "postcss";
|
||||
import {
|
||||
compileTimingAttrs,
|
||||
injectDurations,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
rewriteAssetPaths,
|
||||
rewriteCssAssetUrls,
|
||||
} from "@hyperframes/core";
|
||||
import { scopeCssToComposition, wrapScopedCompositionScript } from "@hyperframes/core/compiler";
|
||||
import { extractMediaMetadata, extractAudioMetadata } from "../utils/ffprobe.js";
|
||||
import { isPathInside, toExternalAssetKey } from "../utils/paths.js";
|
||||
import {
|
||||
@@ -448,45 +448,6 @@ function promoteCssImportsToLinkTags(html: string): string {
|
||||
* export, preventing font-loading and animation-ordering regressions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Scope CSS rules to a specific composition by prepending each selector
|
||||
* with `[data-composition-id="<id>"]`. This prevents class name collisions
|
||||
* when multiple sub-compositions use the same class names (e.g. ".content").
|
||||
*
|
||||
* Handles:
|
||||
* - Regular rules: `.foo { }` → `[data-composition-id="x"] .foo { }`
|
||||
* - @media and other at-rules: preserved, inner selectors are scoped
|
||||
* - @import, @font-face, @keyframes: left unscoped (global by nature)
|
||||
*/
|
||||
function scopeCssToComposition(css: string, compositionId: string): string {
|
||||
const scope = `[data-composition-id="${compositionId}"]`;
|
||||
const globalAtRules = new Set(["keyframes", "-webkit-keyframes", "font-face"]);
|
||||
const root = postcss.parse(css);
|
||||
|
||||
root.walkRules((rule) => {
|
||||
// Skip rules nested inside @keyframes or @font-face — they're global
|
||||
let node: postcss.Node | undefined = rule.parent;
|
||||
while (node) {
|
||||
if (
|
||||
node.type === "atrule" &&
|
||||
globalAtRules.has((node as postcss.AtRule).name.toLowerCase())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
node = (node as postcss.ChildNode).parent;
|
||||
}
|
||||
|
||||
rule.selectors = rule.selectors.map((sel) => {
|
||||
if (!sel.trim()) return sel;
|
||||
if (/^(html|body|:root|\*)$/i.test(sel.trim())) return sel;
|
||||
if (sel.includes(`data-composition-id="${compositionId}"`)) return sel;
|
||||
return `${scope} ${sel}`;
|
||||
});
|
||||
});
|
||||
|
||||
return root.toResult().css;
|
||||
}
|
||||
|
||||
function coalesceHeadStylesAndBodyScripts(html: string): string {
|
||||
const { document } = parseHTML(html);
|
||||
const head = document.querySelector("head");
|
||||
@@ -665,28 +626,15 @@ function inlineSubCompositions(
|
||||
const content = (scriptEl.textContent || "").trim();
|
||||
if (content) {
|
||||
const scriptMountCompId = compId || inferredCompId || "";
|
||||
const compIdLiteral = JSON.stringify(scriptMountCompId);
|
||||
collectedScripts.push(`(function(){
|
||||
var __compId = ${compIdLiteral};
|
||||
var __run = function() {
|
||||
try {
|
||||
${content}
|
||||
} catch (_err) {
|
||||
console.error("[Compiler] Composition script failed", __compId, _err);
|
||||
}
|
||||
};
|
||||
if (!__compId) { __run(); return; }
|
||||
${COMPILER_MOUNT_BLOCK_START}
|
||||
var __selector = '[data-composition-id="' + (__compId + '').replace(/"/g, '\\\\"') + '"]';
|
||||
var __attempt = 0;
|
||||
var __tryRun = function() {
|
||||
if (document.querySelector(__selector)) { __run(); return; }
|
||||
if (++__attempt >= 8) { __run(); return; }
|
||||
requestAnimationFrame(__tryRun);
|
||||
};
|
||||
__tryRun();
|
||||
${COMPILER_MOUNT_BLOCK_END}
|
||||
})()`);
|
||||
collectedScripts.push(
|
||||
scriptMountCompId
|
||||
? wrapScopedCompositionScript(
|
||||
content,
|
||||
scriptMountCompId,
|
||||
"[Compiler] Composition script failed",
|
||||
)
|
||||
: `(function(){ try { ${content} } catch (_err) { console.error("[Compiler] Composition script failed", _err); } })()`,
|
||||
);
|
||||
}
|
||||
scriptEl.remove();
|
||||
}
|
||||
@@ -707,11 +655,7 @@ function inlineSubCompositions(
|
||||
if (innerW && !host.getAttribute("data-width")) host.setAttribute("data-width", innerW);
|
||||
if (innerH && !host.getAttribute("data-height")) host.setAttribute("data-height", innerH);
|
||||
innerRoot.querySelectorAll("style, script").forEach((el) => el.remove());
|
||||
if (!compId && inferredCompId) {
|
||||
host.innerHTML = innerRoot.outerHTML || "";
|
||||
} else {
|
||||
host.innerHTML = innerRoot.innerHTML || "";
|
||||
}
|
||||
host.innerHTML = innerRoot.outerHTML || "";
|
||||
} else {
|
||||
contentDoc.querySelectorAll("style, script").forEach((el) => el.remove());
|
||||
host.innerHTML = contentDoc.toString();
|
||||
|
||||
Reference in New Issue
Block a user