mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio,runtime,engine,compiler): 8 bug fixes — audio, render, timeline, Lottie, thumbnails, video render (#133)
## 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 `\<script src>` tags from sub-compositions (root cause: `$content(s).html()` returns `""` for external scripts).
**3 additional bugs fixed:**
- **Bug 6 — Blank thumbnails outside monorepo**: Implemented `generateThumbnail` in the CLI adapter using Puppeteer.
- **Bug 7 — Video empty in rendered sub-compositions**: Fixed `parseVideoElements` selector from `video[id][src]` to `video[src][data-start]` + auto-assign IDs.
- **Render errors**: Failed renders now show their error message in the renders panel.
## Commits
| Commit | Description |
| --- | --- |
| `3951c6f` | fix(studio): store render job reference instead of snapshot copy |
| `f331c30` | fix(studio): make previously-completed renders downloadable after restart |
| `a5e2d04` | fix(studio): add range request support for audio/video seeking in preview |
| `f24317a` | fix(runtime): pad GSAP timeline to data-duration when composition declares longer duration |
| `7cf38ca` | fix(runtime): fix Lottie adapter conflicting with GSAP-driven animations |
| `bc99209` | fix(studio): surface render error messages in the renders panel |
| `8fc9e8b` | fix(cli): implement generateThumbnail in studio adapter |
| `90277ea` | fix(engine): render videos inside sub-compositions that lack an explicit id |
| `f5bb579` | fix(compiler): preserve external CDN scripts from sub-compositions in bundle |
## Test plan
- [x] `golden-lyric-video`: seek → audio plays from seeked position
- [x] Any project: render → progress advances past 0%, reaches 100%
- [x] Any project: complete render, restart `hyperframes dev`, Download → works
- [x] `intro-vid`: play → runs full 5s (not stopping at 3s)
- [x] `hyperframe-build-up-demo`: play → rocket Lottie visible during 0-2s ✅ verified
- [x] Outside monorepo: Compositions sidebar shows thumbnail images (not blank)
- [x] `bug.zip` project: render → video in polaroid sub-composition appears in output
- [x] Trigger a failed render → error message shown
This commit is contained in:
@@ -111,6 +111,42 @@ describe("lintHyperframeHtml", () => {
|
||||
expect(codes.length).toBe(uniqueCodes.length);
|
||||
});
|
||||
|
||||
it("reports info for composition with external CDN script dependency", () => {
|
||||
const html = `<template id="rockets-template">
|
||||
<div data-composition-id="rockets" data-width="1920" data-height="1080">
|
||||
<div id="rocket-container"></div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/lottie.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["rockets"] = gsap.timeline({ paused: true });
|
||||
</script>
|
||||
</div>
|
||||
</template>`;
|
||||
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 = `
|
||||
<html><body>
|
||||
<div id="root" data-composition-id="main" data-width="1920" data-height="1080">
|
||||
<script>
|
||||
window.__timelines = {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</body></html>`;
|
||||
const result = lintHyperframeHtml(html);
|
||||
expect(result.findings.find((f) => f.code === "external_script_dependency")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("strips <template> wrapper before linting composition files", () => {
|
||||
const html = `<template id="my-comp-template">
|
||||
<div data-composition-id="my-comp" data-width="1920" data-height="1080"
|
||||
|
||||
@@ -675,6 +675,32 @@ export function lintHyperframeHtml(
|
||||
}
|
||||
}
|
||||
|
||||
// ── External CDN script dependency check ────────────────────────────────
|
||||
// Compositions that load CDN libraries via <script src="https://..."> work
|
||||
// correctly in bundled mode (bundleToSingleHtml auto-hoists them to the parent
|
||||
// document) and in runtime mode (loadExternalCompositions re-injects them).
|
||||
// But when a composition is used in a custom pipeline that bypasses both, the
|
||||
// scripts won't be available. Flag this as an info-level finding so developers
|
||||
// know the dependency exists.
|
||||
{
|
||||
const externalScriptRe = /<script\b[^>]*\bsrc=["'](https?:\/\/[^"']+)["'][^>]*>/gi;
|
||||
let match: RegExpExecArray | null;
|
||||
const seen = new Set<string>();
|
||||
while ((match = externalScriptRe.exec(source)) !== null) {
|
||||
const src = match[1] ?? "";
|
||||
if (seen.has(src)) continue;
|
||||
seen.add(src);
|
||||
pushFinding({
|
||||
code: "external_script_dependency",
|
||||
severity: "info",
|
||||
message: `This composition loads an external script from \`${src}\`. The HyperFrames bundler automatically hoists CDN scripts from sub-compositions into the parent document. In unbundled runtime mode, \`loadExternalCompositions\` re-injects them. If you're using a custom pipeline that bypasses both, you'll need to include this script manually.`,
|
||||
fixHint:
|
||||
"No action needed when using `hyperframes dev` or `hyperframes render`. If using a custom pipeline, add this script tag to your root composition or HTML page.",
|
||||
snippet: truncateSnippet(match[0] ?? ""),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const errorCount = findings.filter((finding) => finding.severity === "error").length;
|
||||
const warningCount = findings.length - errorCount;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user