fix(core): wrap bundled composition scripts as string literals

This commit is contained in:
func25
2026-05-25 15:41:13 +07:00
parent e101b47be4
commit 3bb0d1efd9
5 changed files with 53 additions and 10 deletions
@@ -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 = "</script><script>window.pwned = true;</script>";',
"scene",
);
expect(wrapped).toContain('Function("document", "gsap", "window", "__hyperframes", ');
expect(wrapped).toContain('\\"</script><script>window.pwned = true;</script>\\"');
});
it("wraps unscoped composition script source as a string literal", () => {
const wrapped = wrapInlineScriptWithErrorBoundary(
'window.payload = "</script><script>window.pwned = true;</script>";',
"[HyperFrames] composition script error:",
);
expect(wrapped).toContain("Function(");
expect(wrapped).toContain('\\"</script><script>window.pwned = true;</script>\\"');
});
it("rewrites #id CSS selectors to [data-hf-authored-id] when authoredRootId is provided", () => {
const scoped = scopeCssToComposition(
`#intro { background: #111; }
@@ -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); } })();`;
}
@@ -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 () => {
+13 -3
View File
@@ -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();
@@ -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 });
}