mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(bundler): inline runtime body, drop bare-semi joins, drop empty catch binding
Three issues in `bundleToSingleHtml` reported via Abhay's LLM-based code-validity
eval against the bundled output. Each is independently small; they share a single
PR because they're all artifacts of the bundler-output shape.
1. Empty `src=""` runtime placeholder (real bug)
`htmlBundler.ts:injectInterceptor` emitted
`<script data-hyperframes-preview-runtime="1" src=""></script>`
when no `HYPERFRAME_RUNTIME_URL` was configured. Empty `src` resolves to the
page URL itself; Chrome flags this as an infinite-fetch hazard. Three other
consumers (studioServer, validate, snapshot) post-process the placeholder to
substitute either a real URL or an inlined body — `bundleToSingleHtml` did
not, so the bundle wasn't actually self-contained despite the function name.
Fix: when no URL is configured, inline the runtime IIFE directly via
`getHyperframeRuntimeScript()`. Otherwise emit `src=…` as before.
2. Bare-semicolon lines between joined JS chunks (cosmetic)
Three sites used `chunks.join("\n;\n")` (body-script coalesce, local JS,
composition scripts) which produced a lone `;` on its own line between
chunks. Valid JS but a code smell. Replace with a `joinJsChunks()` helper
that ensures each chunk ends in `;` and joins on `\n`.
3. Empty `catch (_err) {}` in compositionScoping.ts (lint-noisy)
The `_err` underscore prefix signals "intentionally swallowed" but bundle-time
linters often don't honor that convention. Replaced with `catch { /* ... */ }`
(no binding, explanatory comment) — same behavior, no rule fires.
Tests: 2 new regression guards (runtime-not-empty-src, no-bare-semi) plus
existing tests updated to reflect the new inlined-runtime shape (the previous
"runtime block must not contain getElementById" assertion no longer holds
because the inlined body itself uses getElementById; replaced with a more
specific "author script not merged into runtime tag" check).
Issue #4 from the original report (Unterminated string at line 1111 col 18,
char 65497) was not directly reproducible after applying these fixes — esbuild
parses all 4 inline scripts in the rebundled output cleanly. The unterminated-
string symptom was likely a downstream artifact of the bare-semicolon joining
or the empty-src placeholder confusing the lint tool. If the original symptom
persists on a clean re-run against the fixed bundle, will open a follow-up PR
with a focused repro.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
21ec5f800a
commit
af2f727b3f
@@ -38,10 +38,82 @@ describe("bundleToSingleHtml", () => {
|
||||
)?.[0];
|
||||
|
||||
expect(runtimeBlock).toBeDefined();
|
||||
expect(runtimeBlock).not.toContain("getElementById");
|
||||
// The runtime block must contain the inlined HF runtime IIFE — bundled
|
||||
// output is self-contained, so the bundle's runtime body is loaded inline,
|
||||
// not referenced via src.
|
||||
expect(runtimeBlock).toMatch(/data-hyperframes-preview-runtime="1">/);
|
||||
expect(runtimeBlock).not.toMatch(/src=""/);
|
||||
// The author's specific composition script must NOT be merged INTO the
|
||||
// runtime tag — it stays as its own <script> elsewhere in the document.
|
||||
expect(runtimeBlock).not.toContain("window.__timelines.main = { duration:");
|
||||
expect(bundled).toContain('document.getElementById("scene")');
|
||||
});
|
||||
|
||||
it("produces a self-contained runtime script when no HYPERFRAME_RUNTIME_URL is set", async () => {
|
||||
// Regression guard: hf#XXX. The bundler used to emit
|
||||
// <script ... src=""></script> when no runtime URL was configured. An
|
||||
// empty src resolves to the page URL itself, which Chrome flags as an
|
||||
// infinite-fetch hazard. Verify that bundleToSingleHtml inlines the
|
||||
// runtime body so the bundle is genuinely self-contained.
|
||||
const dir = makeTempProject({
|
||||
"index.html": `<!doctype html>
|
||||
<html><body>
|
||||
<div data-composition-id="root" data-width="320" data-height="180"></div>
|
||||
</body></html>`,
|
||||
});
|
||||
|
||||
const previousUrl = process.env.HYPERFRAME_RUNTIME_URL;
|
||||
delete process.env.HYPERFRAME_RUNTIME_URL;
|
||||
let bundled: string;
|
||||
try {
|
||||
bundled = await bundleToSingleHtml(dir);
|
||||
} finally {
|
||||
if (previousUrl !== undefined) process.env.HYPERFRAME_RUNTIME_URL = previousUrl;
|
||||
}
|
||||
|
||||
const runtimeBlock = bundled.match(
|
||||
/<script\b[^>]*data-hyperframes-preview-runtime[^>]*>[\s\S]*?<\/script>/i,
|
||||
)?.[0];
|
||||
expect(runtimeBlock).toBeDefined();
|
||||
// Must NOT have an empty src attribute (would self-fetch).
|
||||
expect(runtimeBlock).not.toMatch(/src=""/);
|
||||
// Must have a non-trivial inlined body (the runtime IIFE is ~150KB).
|
||||
const innerLength = (runtimeBlock!.match(/>([\s\S]*?)<\/script>/)?.[1] ?? "").length;
|
||||
expect(innerLength).toBeGreaterThan(1000);
|
||||
});
|
||||
|
||||
it("does not produce stray bare-semicolon lines between concatenated JS chunks", async () => {
|
||||
// Regression guard: hf#XXX. Earlier the bundler joined script chunks with
|
||||
// `\n;\n`, which produces a lone `;` on its own line between chunks. Valid
|
||||
// JS but reads as a code smell. Each chunk should end in `;` and chunks
|
||||
// should join with `\n`.
|
||||
const dir = makeTempProject({
|
||||
"index.html": `<!doctype html>
|
||||
<html><body>
|
||||
<div data-composition-id="root" data-width="320" data-height="180">
|
||||
<div id="child-host"
|
||||
data-composition-id="child"
|
||||
data-composition-src="compositions/child.html"
|
||||
data-start="0" data-duration="2"></div>
|
||||
</div>
|
||||
<script src="local-a.js"></script>
|
||||
<script src="local-b.js"></script>
|
||||
<script>window.__timelines = window.__timelines || {}; window.__timelines.root = {}</script>
|
||||
</body></html>`,
|
||||
"local-a.js": "window.__a = 1",
|
||||
"local-b.js": "window.__b = 2",
|
||||
"compositions/child.html": `<template id="child-template">
|
||||
<div data-composition-id="child" data-width="320" data-height="180">
|
||||
<script>window.__c = 3</script>
|
||||
</div>
|
||||
</template>`,
|
||||
});
|
||||
|
||||
const bundled = await bundleToSingleHtml(dir);
|
||||
// No line is JUST a bare semicolon (with optional surrounding whitespace).
|
||||
expect(bundled).not.toMatch(/\n\s*;\s*\n/);
|
||||
});
|
||||
|
||||
it("hoists external CDN scripts from sub-compositions into the bundle", async () => {
|
||||
const dir = makeTempProject({
|
||||
"index.html": `<!doctype html>
|
||||
@@ -84,8 +156,14 @@ describe("bundleToSingleHtml", () => {
|
||||
// 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");
|
||||
// data-composition-src should be stripped from the host element (composition
|
||||
// was inlined). The literal string may still appear inside the inlined
|
||||
// runtime IIFE that knows how to look up that attribute — so check the DOM,
|
||||
// not the raw text.
|
||||
const { document: doc } = parseHTML(bundled);
|
||||
const hostEl = doc.getElementById("rockets-host");
|
||||
expect(hostEl).toBeTruthy();
|
||||
expect(hostEl?.hasAttribute("data-composition-src")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not duplicate CDN scripts already present in the main document", async () => {
|
||||
|
||||
Reference in New Issue
Block a user