fix(producer): reuse index shell for entry composition renders

This commit is contained in:
Miguel Ángel
2026-03-26 22:32:08 -04:00
parent ab54952c26
commit 8d009f9f4a
2 changed files with 109 additions and 2 deletions
@@ -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 ─────────────────────────────────────────────────