From 1230657ed0cb7b72e2daae6b93308e664f3c8236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 30 Mar 2026 23:58:08 +0200 Subject: [PATCH] =?UTF-8?q?fix(studio,runtime,engine,compiler):=208=20bug?= =?UTF-8?q?=20fixes=20=E2=80=94=20audio,=20render,=20timeline,=20Lottie,?= =?UTF-8?q?=20thumbnails,=20video=20render=20(#133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary **Original 5 bugs fixed:** - **Bug 1 — Audio silent after seek**: Added `Accept-Ranges` / `Content-Length` + `206 Partial Content` to the static asset server for byte-range seeking. - **Bug 2 — Download 404 after restart**: Render list endpoint now registers on-disk renders into the in-memory job map. - **Bug 3 — Timeline stops at GSAP end**: `resolveRootTimelineFromDocument` pads the GSAP timeline to match `data-duration` when the composition declares longer. - **Bug 4 — Render stuck at 0%**: Store `jobState` reference (not spread copy) so async progress mutations reach the SSE stream. - **Bug 5 — Lottie missing in preview/render**: Two fixes — (a) moved Lottie adapter before GSAP so `onUpdate` wins; (b) fixed bundler silently dropping external CDN `\ + +
+
+
+ +`, + "compositions/rockets.html": ``, + }); + + const bundled = await bundleToSingleHtml(dir); + + // Lottie CDN script from sub-composition must be present in the bundle + expect(bundled).toContain( + "https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/lottie.min.js", + ); + + // Should only appear once (deduped) + const occurrences = (bundled.match(/cdnjs\.cloudflare\.com\/ajax\/libs\/lottie-web/g) ?? []) + .length; + expect(occurrences).toBe(1); + + // GSAP CDN from main doc should still be present + expect(bundled).toContain("cdn.jsdelivr.net/npm/gsap"); + + // data-composition-src should be stripped (composition was inlined) + expect(bundled).not.toContain("data-composition-src"); + }); + + it("does not duplicate CDN scripts already present in the main document", async () => { + const dir = makeTempProject({ + "index.html": ` + + + +
+
+
+ +`, + "compositions/child.html": ``, + }); + + const bundled = await bundleToSingleHtml(dir); + + // GSAP CDN should appear exactly once (deduped) + const gsapOccurrences = ( + bundled.match(/cdn\.jsdelivr\.net\/npm\/gsap@3\.14\.2\/dist\/gsap\.min\.js/g) ?? [] + ).length; + expect(gsapOccurrences).toBe(1); + }); +}); diff --git a/packages/core/src/compiler/htmlBundler.ts b/packages/core/src/compiler/htmlBundler.ts index f9a2035a3..54b3250c7 100644 --- a/packages/core/src/compiler/htmlBundler.ts +++ b/packages/core/src/compiler/htmlBundler.ts @@ -390,6 +390,7 @@ export async function bundleToSingleHtml( // Inline sub-compositions const compStyleChunks: string[] = []; const compScriptChunks: string[] = []; + const compExternalScriptSrcs: string[] = []; $("[data-composition-src]").each((_, hostEl) => { const src = $(hostEl).attr("data-composition-src"); if (!src || !isRelativeUrl(src)) return; @@ -416,9 +417,18 @@ export async function bundleToSingleHtml( $content(s).remove(); }); $content("script").each((_, s) => { - compScriptChunks.push( - `(function(){ try { ${$content(s).html() || ""} } catch (_err) { console.error('[HyperFrames] composition script error:', _err); } })();`, - ); + const externalSrc = ($content(s).attr("src") || "").trim(); + if (externalSrc) { + // External CDN/remote script — collect for deduped injection into the document. + // Do NOT try to inline the content (external scripts have no innerHTML). + if (!compExternalScriptSrcs.includes(externalSrc)) { + compExternalScriptSrcs.push(externalSrc); + } + } else { + compScriptChunks.push( + `(function(){ try { ${$content(s).html() || ""} } catch (_err) { console.error('[HyperFrames] composition script error:', _err); } })();`, + ); + } $content(s).remove(); }); @@ -439,6 +449,14 @@ export async function bundleToSingleHtml( $(hostEl).removeAttr("data-composition-src"); }); + // Inject external scripts from sub-compositions (e.g., Lottie CDN) + // that aren't already present in the main document. + for (const extSrc of compExternalScriptSrcs) { + if (!$(`script[src="${extSrc}"]`).length) { + $("body").append(``); + } + } + if (compStyleChunks.length) $("head").append(``); if (compScriptChunks.length) $("body").append(``); diff --git a/packages/core/src/lint/hyperframeLinter.test.ts b/packages/core/src/lint/hyperframeLinter.test.ts index 1a5d31758..a5ef30ce4 100644 --- a/packages/core/src/lint/hyperframeLinter.test.ts +++ b/packages/core/src/lint/hyperframeLinter.test.ts @@ -111,6 +111,42 @@ describe("lintHyperframeHtml", () => { expect(codes.length).toBe(uniqueCodes.length); }); + it("reports info for composition with external CDN script dependency", () => { + const html = ``; + const result = lintHyperframeHtml(html, { filePath: "compositions/rockets.html" }); + const finding = result.findings.find((f) => f.code === "external_script_dependency"); + expect(finding).toBeDefined(); + expect(finding?.severity).toBe("info"); + expect(finding?.message).toContain("cdnjs.cloudflare.com"); + // info findings do not count as errors — ok should still be true + expect(result.ok).toBe(true); + expect(result.errorCount).toBe(0); + }); + + it("does not report external_script_dependency for inline scripts", () => { + const html = ` + +
+ +
+`; + const result = lintHyperframeHtml(html); + expect(result.findings.find((f) => f.code === "external_script_dependency")).toBeUndefined(); + }); + it("strips