diff --git a/packages/producer/src/services/renderOrchestrator.test.ts b/packages/producer/src/services/renderOrchestrator.test.ts new file mode 100644 index 000000000..ac1554390 --- /dev/null +++ b/packages/producer/src/services/renderOrchestrator.test.ts @@ -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 = ` + + + + + + +
+
+
+
+ +`; + + 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(""); + }); + + it("matches normalized data-composition-src paths", () => { + const indexHtml = ` + + +
+
+
+ +`; + + 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"'); + }); +}); diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index de5e90187..1b3ecac01 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -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")} `; } +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("