mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(producer): hoist external CDN scripts from sub-compositions (#164)
## Summary
- **Fixes**: External `<script src="...">` tags in sub-compositions (e.g. GSAP TextPlugin, ScrollTrigger) were silently discarded during `hyperframes render`, causing `ReferenceError` at runtime
- **Root cause**: The producer's `inlineSubCompositions` skipped external scripts with `if (src) continue` but then removed them via the blanket `querySelectorAll("style, script").forEach(remove)` — they were never hoisted to the parent document
- **Fix**: Mirror the core bundler's (`htmlBundler.ts`) approach — collect external script `src` URLs, deduplicate against existing scripts in the parent, and inject as `<script>` tags before inline composition scripts
## How to reproduce
1. Create a sub-composition that uses GSAP TextPlugin:
```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/TextPlugin.min.js"></script>
<script>
gsap.registerPlugin(TextPlugin);
tl.to("#text", { text: { value: "Hello" }, duration: 1 });
</script>
```
1. Run `hyperframes render`
2. Browser console shows: `[Compiler] Composition script failed scene-hook ReferenceError: TextPlugin is not defined`
## Test plan
- [x] Verify `hyperframes render` on a project using TextPlugin in a sub-composition no longer errors
- [x] Verify external scripts are deduped (same CDN URL in 2 sub-compositions → only 1 `<script>` tag)
- [x] Verify external scripts already in `index.html` are not duplicated
- [x] Verify script order: external CDN scripts load before inline composition scripts
This commit is contained in:
@@ -490,6 +490,7 @@ function inlineSubCompositions(
|
||||
|
||||
const collectedStyles: string[] = [];
|
||||
const collectedScripts: string[] = [];
|
||||
const collectedExternalScriptSrcs: string[] = [];
|
||||
|
||||
for (const host of hosts) {
|
||||
const srcPath = host.getAttribute("data-composition-src");
|
||||
@@ -541,7 +542,15 @@ function inlineSubCompositions(
|
||||
|
||||
for (const scriptEl of contentDoc.querySelectorAll("script")) {
|
||||
const src = (scriptEl.getAttribute("src") || "").trim();
|
||||
if (src) continue;
|
||||
if (src) {
|
||||
// External CDN/remote script — collect for deduped injection into the
|
||||
// parent document, mirroring the bundler's hoisting behavior.
|
||||
if (!collectedExternalScriptSrcs.includes(src)) {
|
||||
collectedExternalScriptSrcs.push(src);
|
||||
}
|
||||
scriptEl.remove();
|
||||
continue;
|
||||
}
|
||||
const content = (scriptEl.textContent || "").trim();
|
||||
if (content) {
|
||||
const scriptMountCompId = compId || inferredCompId || "";
|
||||
@@ -624,6 +633,25 @@ function inlineSubCompositions(
|
||||
head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
// Inject external CDN scripts before inline scripts so plugins (e.g.
|
||||
// TextPlugin, ScrollTrigger) are registered before composition code runs.
|
||||
// Deduplicate against scripts already present in the document.
|
||||
if (collectedExternalScriptSrcs.length && body) {
|
||||
const existingScriptSrcs = new Set(
|
||||
Array.from(document.querySelectorAll("script[src]")).map((el) =>
|
||||
(el.getAttribute("src") || "").trim(),
|
||||
),
|
||||
);
|
||||
for (const src of collectedExternalScriptSrcs) {
|
||||
if (!existingScriptSrcs.has(src)) {
|
||||
const scriptEl = document.createElement("script");
|
||||
scriptEl.setAttribute("src", src);
|
||||
body.appendChild(scriptEl);
|
||||
existingScriptSrcs.add(src);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (collectedScripts.length && body) {
|
||||
const scriptEl = document.createElement("script");
|
||||
scriptEl.textContent = collectedScripts.join("\n;\n");
|
||||
|
||||
Reference in New Issue
Block a user