diff --git a/.filesize-allowlist b/.filesize-allowlist index b0cfc09e1..d2dff0d55 100644 --- a/.filesize-allowlist +++ b/.filesize-allowlist @@ -10,3 +10,4 @@ packages/studio/src/App.tsx packages/studio/src/player/components/Timeline.tsx packages/studio/src/player/components/timelineEditing.test.ts packages/studio/src/components/editor/domEditing.test.ts +packages/studio/src/components/editor/domEditingLayers.ts diff --git a/packages/core/src/compiler/htmlBundler.ts b/packages/core/src/compiler/htmlBundler.ts index 3bae137bf..1bcbfbbf6 100644 --- a/packages/core/src/compiler/htmlBundler.ts +++ b/packages/core/src/compiler/htmlBundler.ts @@ -685,6 +685,7 @@ export async function bundleToSingleHtml( const compStyleChunks: string[] = [...subCompResult.styles]; const compScriptChunks: string[] = [...subCompResult.scripts]; const compExternalScriptSrcs: string[] = [...subCompResult.externalScriptSrcs]; + const compExternalLinks = [...subCompResult.externalLinks]; const compVariablesByComp: Record> = { ...subCompResult.variablesByComp, }; @@ -811,6 +812,17 @@ export async function bundleToSingleHtml( } } + for (const link of compExternalLinks) { + const escapedHref = link.href.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + if (!document.querySelector(`link[href="${escapedHref}"]`)) { + const linkEl = document.createElement("link"); + linkEl.setAttribute("rel", link.rel); + linkEl.setAttribute("href", link.href); + if (link.crossorigin != null) linkEl.setAttribute("crossorigin", link.crossorigin); + document.head.appendChild(linkEl); + } + } + if (compStyleChunks.length) { const style = document.createElement("style"); style.textContent = compStyleChunks.join("\n\n"); diff --git a/packages/core/src/compiler/inlineSubCompositions.test.ts b/packages/core/src/compiler/inlineSubCompositions.test.ts index 7e91f165e..6e2470e65 100644 --- a/packages/core/src/compiler/inlineSubCompositions.test.ts +++ b/packages/core/src/compiler/inlineSubCompositions.test.ts @@ -131,6 +131,91 @@ describe("inlineSubCompositions – #ID selector scoping divergence", () => { expect(scopedCss).toContain('[data-hf-authored-id="intro"]'); }); + it("extracts elements from sub-composition with original rel and crossorigin", () => { + const subCompWithLinks = ` + + + + + +
+ Hello +
+`; + + const document = makeHostDocument("captions"); + const host = document.querySelector('[data-composition-src="intro.html"]')!; + + const result = inlineSubCompositions(document, [host], { + resolveHtml: () => subCompWithLinks, + parseHtml: (html) => parseHTML(html).document, + }); + + expect(result.externalLinks).toHaveLength(3); + expect(result.externalLinks[0]).toEqual({ + href: "https://fonts.googleapis.com", + rel: "preconnect", + crossorigin: undefined, + }); + expect(result.externalLinks[1]).toEqual({ + href: "https://fonts.gstatic.com", + rel: "preconnect", + crossorigin: "", + }); + expect(result.externalLinks[2]).toEqual({ + href: "https://fonts.googleapis.com/css2?family=Montserrat:wght@800&display=swap", + rel: "stylesheet", + crossorigin: undefined, + }); + }); + + it("deduplicates link hrefs across multiple sub-compositions", () => { + const subComp = ` + + + +
A
+`; + + const { document } = parseHTML(` + +
+
+
+
+`); + const hosts = Array.from(document.querySelectorAll("[data-composition-src]")); + + const result = inlineSubCompositions(document, hosts, { + resolveHtml: () => subComp, + parseHtml: (html) => parseHTML(html).document, + }); + + expect(result.externalLinks).toHaveLength(1); + expect(result.externalLinks[0]!.href).toBe( + "https://fonts.googleapis.com/css2?family=Montserrat:wght@800", + ); + }); + + it("propagates data-timeline-locked from inner root to host element", () => { + const lockedSubComp = ` + +
+ Hello +
+`; + + const document = makeHostDocument("captions"); + const host = document.querySelector('[data-composition-src="intro.html"]')!; + + inlineSubCompositions(document, [host], { + resolveHtml: () => lockedSubComp, + parseHtml: (html) => parseHTML(html).document, + }); + + expect(host.hasAttribute("data-timeline-locked")).toBe(true); + }); + it("producer path propagates data-hf-authored-id to host when inner root has id", () => { const document = makeHostDocument("intro"); const host = document.querySelector('[data-composition-src="intro.html"]')!; diff --git a/packages/core/src/compiler/inlineSubCompositions.ts b/packages/core/src/compiler/inlineSubCompositions.ts index 3ff1a437a..995c7019d 100644 --- a/packages/core/src/compiler/inlineSubCompositions.ts +++ b/packages/core/src/compiler/inlineSubCompositions.ts @@ -97,6 +97,7 @@ export interface InlineSubCompositionsResult { styles: string[]; scripts: string[]; externalScriptSrcs: string[]; + externalLinks: { href: string; rel: string; crossorigin?: string }[]; variablesByComp: Record>; } @@ -149,6 +150,8 @@ export function inlineSubCompositions( const styles: string[] = []; const scripts: string[] = []; const externalScriptSrcs: string[] = []; + const externalLinks: { href: string; rel: string; crossorigin?: string }[] = []; + const seenLinkHrefs = new Set(); const variablesByComp: Record> = {}; for (const hostEl of hosts) { @@ -221,6 +224,19 @@ export function inlineSubCompositions( externalScriptSrcs.push(externalSrc); } } + for (const link of [ + ...compDoc.head.querySelectorAll('link[rel="stylesheet"], link[rel="preconnect"]'), + ]) { + const href = (link.getAttribute("href") || "").trim(); + if (href && !seenLinkHrefs.has(href)) { + seenLinkHrefs.add(href); + const rel = (link.getAttribute("rel") || "").trim(); + const crossorigin = link.hasAttribute("crossorigin") + ? link.getAttribute("crossorigin") || "" + : undefined; + externalLinks.push({ href, rel, crossorigin }); + } + } } // Extract styles from content @@ -286,6 +302,10 @@ export function inlineSubCompositions( ); } + if (innerRoot?.hasAttribute("data-timeline-locked")) { + hostEl.setAttribute("data-timeline-locked", ""); + } + // Copy dimension attributes from inner root to host if missing if (innerRoot) { const innerW = innerRoot.getAttribute("data-width"); @@ -325,5 +345,5 @@ export function inlineSubCompositions( hostEl.removeAttribute("data-composition-src"); } - return { styles, scripts, externalScriptSrcs, variablesByComp }; + return { styles, scripts, externalScriptSrcs, externalLinks, variablesByComp }; } diff --git a/packages/core/src/runtime/compositionLoader.ts b/packages/core/src/runtime/compositionLoader.ts index abc0e3b33..bb335694a 100644 --- a/packages/core/src/runtime/compositionLoader.ts +++ b/packages/core/src/runtime/compositionLoader.ts @@ -262,6 +262,8 @@ async function mountCompositionContent(params: { headStyles?: HTMLStyleElement[]; /** Extra