mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(producer): reuse index shell for entry composition renders
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractStandaloneEntryFromIndex } from "./renderOrchestrator.js";
|
||||
|
||||
describe("extractStandaloneEntryFromIndex", () => {
|
||||
it("reuses the index wrapper and keeps only the requested composition host", () => {
|
||||
const indexHtml = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>body { background: #111; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main" data-composition-id="root" data-width="1920" data-height="1080">
|
||||
<div id="intro" data-composition-id="intro" data-composition-src="compositions/intro.html" data-start="5"></div>
|
||||
<div id="outro" data-composition-id="outro" data-composition-src="compositions/outro.html" data-start="12"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const extracted = extractStandaloneEntryFromIndex(indexHtml, "compositions/outro.html");
|
||||
|
||||
expect(extracted).toContain('data-composition-id="root"');
|
||||
expect(extracted).toContain('id="outro"');
|
||||
expect(extracted).toContain('data-composition-src="compositions/outro.html"');
|
||||
expect(extracted).toContain('data-start="0"');
|
||||
expect(extracted).not.toContain('id="intro"');
|
||||
expect(extracted).toContain("<style>body { background: #111; }</style>");
|
||||
});
|
||||
|
||||
it("matches normalized data-composition-src paths", () => {
|
||||
const indexHtml = `<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div data-composition-id="root" data-width="1920" data-height="1080">
|
||||
<div id="intro" data-composition-id="intro" data-composition-src="./compositions/intro.html" data-start="3"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const extracted = extractStandaloneEntryFromIndex(indexHtml, "compositions/intro.html");
|
||||
|
||||
expect(extracted).not.toBeNull();
|
||||
expect(extracted).toContain('data-start="0"');
|
||||
expect(extracted).toContain('data-composition-src="./compositions/intro.html"');
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
copyFileSync,
|
||||
appendFileSync,
|
||||
} from "fs";
|
||||
import { parseHTML } from "linkedom";
|
||||
import {
|
||||
type EngineConfig,
|
||||
resolveConfig,
|
||||
@@ -282,6 +283,10 @@ export function createRenderJob(config: RenderConfig): RenderJob {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCompositionSrcPath(srcPath: string): string {
|
||||
return srcPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Main render pipeline
|
||||
*/
|
||||
@@ -337,6 +342,46 @@ ${scripts.map((s) => s.replace(/<\/?script[^>]*>/gi, "")).join("\n")}
|
||||
</html>`;
|
||||
}
|
||||
|
||||
export function extractStandaloneEntryFromIndex(
|
||||
indexHtml: string,
|
||||
entryFile: string,
|
||||
): string | null {
|
||||
const normalizedEntryFile = normalizeCompositionSrcPath(entryFile);
|
||||
const { document } = parseHTML(indexHtml);
|
||||
const body = document.querySelector("body");
|
||||
if (!body) return null;
|
||||
|
||||
const hosts = Array.from(document.querySelectorAll("[data-composition-src]")) as Element[];
|
||||
const host = hosts.find(
|
||||
(candidate) =>
|
||||
normalizeCompositionSrcPath(candidate.getAttribute("data-composition-src") || "") ===
|
||||
normalizedEntryFile,
|
||||
);
|
||||
if (!host) return null;
|
||||
|
||||
const root =
|
||||
(Array.from(body.children) as Element[]).find((candidate) =>
|
||||
candidate.hasAttribute("data-composition-id"),
|
||||
) ?? null;
|
||||
if (!root) return null;
|
||||
|
||||
const hostClone = host.cloneNode(true) as Element;
|
||||
hostClone.setAttribute("data-start", "0");
|
||||
|
||||
body.innerHTML = "";
|
||||
|
||||
if (root === host) {
|
||||
body.appendChild(hostClone);
|
||||
return document.toString();
|
||||
}
|
||||
|
||||
const rootClone = root.cloneNode(false) as Element;
|
||||
rootClone.appendChild(hostClone);
|
||||
body.appendChild(rootClone);
|
||||
|
||||
return document.toString();
|
||||
}
|
||||
|
||||
export async function executeRenderJob(
|
||||
job: RenderJob,
|
||||
projectDir: string,
|
||||
@@ -394,10 +439,26 @@ export async function executeRenderJob(
|
||||
if (entryFile !== "index.html" && rawEntry.trimStart().startsWith("<template")) {
|
||||
const wrapperPath = join(workDir, "standalone-entry.html");
|
||||
const compositionId = entryFile.replace(/^compositions\//, "").replace(/\.html$/, "");
|
||||
const standaloneHtml = wrapSubCompositionAsStandalone(rawEntry, compositionId, entryFile);
|
||||
const projectIndexPath = join(projectDir, "index.html");
|
||||
const extractedHtml = existsSync(projectIndexPath)
|
||||
? extractStandaloneEntryFromIndex(readFileSync(projectIndexPath, "utf-8"), entryFile)
|
||||
: null;
|
||||
const standaloneHtml =
|
||||
extractedHtml ?? wrapSubCompositionAsStandalone(rawEntry, compositionId, entryFile);
|
||||
writeFileSync(wrapperPath, standaloneHtml, "utf-8");
|
||||
htmlPath = wrapperPath;
|
||||
log.info("Wrapped sub-composition as standalone document", { entryFile, compositionId });
|
||||
if (extractedHtml) {
|
||||
log.info("Extracted standalone entry from index.html host context", {
|
||||
entryFile,
|
||||
compositionId,
|
||||
});
|
||||
} else {
|
||||
log.info("Wrapped sub-composition as standalone document", {
|
||||
entryFile,
|
||||
compositionId,
|
||||
fallback: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stage 1: Compile ─────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user