diff --git a/packages/core/src/studio-api/helpers/subComposition.test.ts b/packages/core/src/studio-api/helpers/subComposition.test.ts index e2121b12f..0c9abdd70 100644 --- a/packages/core/src/studio-api/helpers/subComposition.test.ts +++ b/packages/core/src/studio-api/helpers/subComposition.test.ts @@ -16,6 +16,91 @@ function makeTempProject(files: Record): string { } describe("buildSubCompositionHtml", () => { + it("handles full HTML document compositions without nesting in ", () => { + const dir = makeTempProject({ + "index.html": ` +Host`, + "compositions/map-block.html": ` + + + + + + + + + +
+ +
+ + +`, + }); + + const html = buildSubCompositionHtml( + dir, + "compositions/map-block.html", + "/api/runtime.js", + "/api/projects/demo/preview/", + ); + + expect(html).not.toBeNull(); + // Must not nest a full HTML document inside + const bodyStart = html!.indexOf(""); + const afterBody = html!.slice(bodyStart); + expect(afterBody).not.toContain(""); + // Composition styles must be in , not lost + expect(html).toContain(".map {"); + expect(html).toContain("#root {"); + // Image src preserved (no ../ rewrite needed for bare relative paths) + expect(html).toContain('src="assets/map.png"'); + // Base tag for asset resolution + expect(html).toContain(''); + // GSAP from the composition's own must be preserved + expect(html).toContain("gsap@3.14.2"); + // Body script content preserved + expect(html).toContain('__timelines["map-block"]'); + // and from composition head must not be dropped + expect(html).toContain('rel="stylesheet"'); + expect(html).toContain('href="styles/theme.css"'); + expect(html).toContain('name="viewport"'); + // attribute forwarded to the output + expect(html).toContain('lang="en"'); + }); + + it("handles raw fragment compositions (no template, no full document)", () => { + const dir = makeTempProject({ + "index.html": ` +Host`, + "compositions/card.html": `
+ +

Hello

+
`, + }); + + const html = buildSubCompositionHtml( + dir, + "compositions/card.html", + "/api/runtime.js", + "/api/projects/demo/preview/", + ); + + expect(html).not.toBeNull(); + expect(html).toContain(''); + // ../icon.svg from compositions/ rewrites to icon.svg at project root + expect(html).toContain('src="icon.svg"'); + expect(html).not.toContain('src="../icon.svg"'); + expect(html).toContain("

Hello

"); + }); + it("rewrites sub-composition asset paths against the project root preview base", () => { const dir = makeTempProject({ "index.html": ` diff --git a/packages/core/src/studio-api/helpers/subComposition.ts b/packages/core/src/studio-api/helpers/subComposition.ts index 859779a3d..30014d407 100644 --- a/packages/core/src/studio-api/helpers/subComposition.ts +++ b/packages/core/src/studio-api/helpers/subComposition.ts @@ -7,12 +7,108 @@ import { rewriteInlineStyleAssetUrls, } from "../../compiler/rewriteSubCompPaths.js"; +/** + * Detect whether `html` is a full document (has ``, ``, or + * ``-wrapped fragment. + * Anchored to start-of-string (ignoring leading whitespace) so stray + * occurrences inside script/template content don't false-positive. + */ +function isFullHtmlDocument(html: string): boolean { + return /^\s*(?:])/i.test(html); +} + +/** + * Rewrite relative asset paths in a parsed DOM tree. Shared across all + * three dispatch branches (template, full-doc, fragment) to avoid drift. + */ +function rewriteRelativePaths(root: ParentNode, compPath: string): void { + rewriteAssetPaths( + root.querySelectorAll("[src], [href]"), + compPath, + (el: Element, attr: string) => el.getAttribute(attr), + (el: Element, attr: string, value: string) => el.setAttribute(attr, value), + ); + rewriteInlineStyleAssetUrls( + root.querySelectorAll("[style]"), + compPath, + (el: Element) => el.getAttribute("style"), + (el: Element, value: string) => el.setAttribute("style", value), + ); + for (const styleEl of root.querySelectorAll("style")) { + styleEl.textContent = rewriteCssAssetUrls(styleEl.textContent || "", compPath); + } +} + +/** + * Parse a full HTML document and extract its head elements and body + * content separately, so they can be reassembled into a clean standalone + * page without nesting `` inside ``. + * + * Extracts the full innerHTML of `` — this preserves `