diff --git a/packages/core/src/compiler/compositionScoping.test.ts b/packages/core/src/compiler/compositionScoping.test.ts index 07c7faf01..5a0a0cd3a 100644 --- a/packages/core/src/compiler/compositionScoping.test.ts +++ b/packages/core/src/compiler/compositionScoping.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it, vi } from "vitest"; import { parseHTML } from "linkedom"; -import { scopeCssToComposition, wrapScopedCompositionScript } from "./compositionScoping"; +import { + scopeCssToComposition, + wrapInlineScriptWithErrorBoundary, + wrapScopedCompositionScript, +} from "./compositionScoping"; describe("composition scoping", () => { it("scopes regular selectors while preserving global at-rules", () => { @@ -568,6 +572,26 @@ window.__afterTimeline = window.__timelines.scene; expect(scoped).toContain('[data-composition-id="chrome-overlay"] .child-element'); }); + it("wraps scoped composition script source as a string literal", () => { + const wrapped = wrapScopedCompositionScript( + 'window.payload = "";', + "scene", + ); + + expect(wrapped).toContain('Function("document", "gsap", "window", "__hyperframes", '); + expect(wrapped).toContain('\\"\\"'); + }); + + it("wraps unscoped composition script source as a string literal", () => { + const wrapped = wrapInlineScriptWithErrorBoundary( + 'window.payload = "";', + "[HyperFrames] composition script error:", + ); + + expect(wrapped).toContain("Function("); + expect(wrapped).toContain('\\"\\"'); + }); + it("rewrites #id CSS selectors to [data-hf-authored-id] when authoredRootId is provided", () => { const scoped = scopeCssToComposition( `#intro { background: #111; } diff --git a/packages/core/src/compiler/compositionScoping.ts b/packages/core/src/compiler/compositionScoping.ts index 280fe5df8..72e9cbbf8 100644 --- a/packages/core/src/compiler/compositionScoping.ts +++ b/packages/core/src/compiler/compositionScoping.ts @@ -216,6 +216,7 @@ export function wrapScopedCompositionScript( const authoredRootIdFormsLiteral = JSON.stringify( getAuthoredRootIdSelectorForms(authoredRootId?.trim() || ""), ); + const sourceLiteral = JSON.stringify(source); return `(function(){ var __hfCompId = ${compositionIdLiteral}; var __hfTimelineCompId = ${timelineCompositionIdLiteral}; @@ -485,9 +486,8 @@ export function wrapScopedCompositionScript( }); var __hfRun = function() { try { - (function(document, gsap, window, __hyperframes) { -${source} - }).call(window, __hfScopedDocument, __hfScopedGsap, __hfScopedWindow, __hfScopedHyperframes); + var __hfScript = Function("document", "gsap", "window", "__hyperframes", ${sourceLiteral}); + __hfScript.call(window, __hfScopedDocument, __hfScopedGsap, __hfScopedWindow, __hfScopedHyperframes); } catch (_err) { console.error(__hfErrorLabel, __hfCompId, _err); } @@ -496,3 +496,7 @@ ${source} __hfRun(); })();`; } + +export function wrapInlineScriptWithErrorBoundary(source: string, errorLabel: string): string { + return `(function(){ try { Function(${JSON.stringify(source)}).call(window); } catch (_err) { console.error(${JSON.stringify(errorLabel)}, _err); } })();`; +} diff --git a/packages/core/src/compiler/htmlBundler.test.ts b/packages/core/src/compiler/htmlBundler.test.ts index 8981c1ea5..f955ef5ea 100644 --- a/packages/core/src/compiler/htmlBundler.test.ts +++ b/packages/core/src/compiler/htmlBundler.test.ts @@ -758,7 +758,8 @@ describe("bundleToSingleHtml", () => { expect(bundled).toContain('[data-composition-id="scene"] .title { color: red; }'); expect(bundled).toContain("new Proxy(window.document"); expect(bundled).toContain("new Proxy(__hfBaseGsap"); - expect(bundled).toContain('tl.to(".title"'); + expect(bundled).toContain('Function("document", "gsap", "window", "__hyperframes",'); + expect(bundled).toContain("tl.to('.title'"); }); it("isolates sibling instances of the same external sub-composition", async () => { diff --git a/packages/core/src/compiler/htmlBundler.ts b/packages/core/src/compiler/htmlBundler.ts index f4442a955..de590c850 100644 --- a/packages/core/src/compiler/htmlBundler.ts +++ b/packages/core/src/compiler/htmlBundler.ts @@ -8,7 +8,11 @@ import { stripEmbeddedRuntimeScripts, } from "./htmlDocument"; // rewriteSubCompPaths functions are used by inlineSubCompositions (shared module) -import { scopeCssToComposition, wrapScopedCompositionScript } from "./compositionScoping"; +import { + scopeCssToComposition, + wrapInlineScriptWithErrorBoundary, + wrapScopedCompositionScript, +} from "./compositionScoping"; import { validateHyperframeHtmlContract } from "./staticGuard"; import { getHyperframeRuntimeScript } from "../generated/runtime-inline"; import { readDeclaredDefaults } from "../runtime/getVariables"; @@ -824,7 +828,10 @@ export async function bundleToSingleHtml( runtimeCompId || compId, authoredRootId, ) - : `(function(){ try { ${scriptEl.textContent || ""} } catch (_err) { console.error('[HyperFrames] composition script error:', _err); } })();`, + : wrapInlineScriptWithErrorBoundary( + scriptEl.textContent || "", + "[HyperFrames] composition script error:", + ), ); } scriptEl.remove(); @@ -875,7 +882,10 @@ export async function bundleToSingleHtml( runtimeScope, runtimeCompId || compId, ) - : `(function(){ try { ${scriptEl.textContent || ""} } catch (_err) { console.error('[HyperFrames] composition script error:', _err); } })();`, + : wrapInlineScriptWithErrorBoundary( + scriptEl.textContent || "", + "[HyperFrames] composition script error:", + ), ); } scriptEl.remove(); diff --git a/packages/core/src/compiler/inlineSubCompositions.ts b/packages/core/src/compiler/inlineSubCompositions.ts index 647e51988..00f09f268 100644 --- a/packages/core/src/compiler/inlineSubCompositions.ts +++ b/packages/core/src/compiler/inlineSubCompositions.ts @@ -13,7 +13,11 @@ import { rewriteCssAssetUrls, rewriteInlineStyleAssetUrls, } from "./rewriteSubCompPaths"; -import { scopeCssToComposition, wrapScopedCompositionScript } from "./compositionScoping"; +import { + scopeCssToComposition, + wrapInlineScriptWithErrorBoundary, + wrapScopedCompositionScript, +} from "./compositionScoping"; // --------------------------------------------------------------------------- // Public interface @@ -287,7 +291,7 @@ export function inlineSubCompositions( runtimeCompId || scopeCompId, authoredRootId, ) - : `(function(){ try { ${s.textContent || ""} } catch (_err) { console.error(${JSON.stringify(scriptErrorLabel)}, _err); } })();`; + : wrapInlineScriptWithErrorBoundary(s.textContent || "", scriptErrorLabel); scripts.push(wrappedScript); scriptItems.push({ kind: "inline", content: wrappedScript }); }