fix(studio): load GSAP core when only a GSAP plugin is on the page (#6260)

* fix(studio): load GSAP core when only a GSAP plugin is on the page

The Studio preview checked for GSAP core by matching "gsap" anywhere in a
script src, so the MotionPathPlugin it had just injected (served from
gsap@x/dist/) counted as core. A composition with a motion path and
Studio-edited motion but no GSAP script of its own then got the plugin and
no core, and its animations did not run.

Detect core by file name (gsap.js, gsap.min.js, or a vendored
gsap-<version>.min.js) on any host or path, and inject Studio motion's core
before the MotionPathPlugin so the plugin can register onto it. Apply the
same core-only check to the sub-composition preview fallback and the
missing_gsap_script lint rule.

Refs #6205

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(studio): keep GSAP core detection within the comment budget

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(studio): load GSAP core ahead of head plugins and only on a real core script

Studio motion and sub-composition previews now insert GSAP core before the
first GSAP plugin script in the head, so an authored MotionPathPlugin or
CustomEase registers on core instead of running first. Core detection keys on
a core script (src, inlined marker, license banner or library body), not on
usage like gsap.registerPlugin() or gsap.defaults(). The missing_gsap_script
lint rule drops the same usage-based and plugin-banner shortcuts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
James Russo
2026-10-25 02:37:08 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e2129c3915
commit 685cd75a92
8 changed files with 340 additions and 54 deletions
+63 -2
View File
@@ -1008,9 +1008,42 @@ describe("GSAP rules", () => {
expect(finding).toBeUndefined();
});
it("reports missing_gsap_script when only a GSAP plugin script is loaded", async () => {
const html = `
<html><body>
<div data-composition-id="main" data-width="1920" data-height="1080"></div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/MotionPathPlugin.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.to("#box", { x: 100, duration: 1 }, 0);
window.__timelines["main"] = tl;
</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "missing_gsap_script");
expect(finding?.severity).toBe("error");
});
it.each(["/api/vendor/npm/gsap@3.15.0/dist/gsap.min.js", "assets/gsap-3.14.2.min.js"])(
"does not report missing_gsap_script for the local GSAP core %s",
async (src) => {
const html = `
<html><body>
<div data-composition-id="main" data-width="1920" data-height="1080"></div>
<script src="${src}"></script>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["main"] = gsap.timeline({ paused: true });
</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "missing_gsap_script")).toBeUndefined();
},
);
it("does not report missing_gsap_script when GSAP is bundled inline", async () => {
// Simulate a large inline GSAP bundle (>5KB) with GreenSock marker
const fakeGsapLib = "/* GreenSock GSAP */" + " ".repeat(6000);
const fakeGsapLib = "/*!\n * GSAP 3.15.0\n * https://gsap.com\n */\n" + " ".repeat(6000);
const html = `
<html><body>
<div data-composition-id="main" data-width="1920" data-height="1080"></div>
@@ -1027,6 +1060,34 @@ describe("GSAP rules", () => {
expect(finding).toBeUndefined();
});
it.each([
[
"registerPlugin",
"gsap.registerPlugin(MotionPathPlugin);",
'<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/MotionPathPlugin.min.js"></script>',
],
["defaults", 'gsap.defaults({ ease: "none" });', ""],
["a large GreenSock comment", "/* GreenSock */" + " ".repeat(6000), ""],
[
"an inlined plugin",
"/* inlined: https://cdn.jsdelivr.net/npm/gsap@3/dist/MotionPathPlugin.min.js */",
"",
],
])("reports missing_gsap_script when the only inline GSAP is %s", async (_name, inline, tag) => {
const html = `
<html><body>
<div data-composition-id="main" data-width="1920" data-height="1080"></div>
${tag}
<script>${inline}</script>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["main"] = gsap.timeline({ paused: true });
</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "missing_gsap_script")?.severity).toBe("error");
});
it("does not report missing_gsap_script when producer inlined CDN script", async () => {
const html = `
<html><body>
+10 -10
View File
@@ -1401,18 +1401,18 @@ export const gsapRules: LintRule<LintContext>[] = [
const usesGsap = allScriptTexts.some((t) =>
/gsap\.(to|from|fromTo|timeline|set|registerPlugin)\b/.test(t),
);
const hasGsapScript = allScriptSrcs.some((src) => /gsap/i.test(src));
// Detect GSAP bundled inline (no src attribute). Match:
// - Producer's CDN-inlining comment: /* inlined: ...gsap... */
// - GSAP library internals: _gsScope, GreenSock, gsap.config
// - Large inline scripts (>5KB) that reference gsap (likely bundled library)
const isGsapCoreSrc = (src: string) =>
/(^|\/)gsap(-\d+(\.\d+)*)?(\.min)?\.js$/i.test(src.split(/[?#]/)[0] ?? "");
const hasGsapScript = allScriptSrcs.some(isGsapCoreSrc);
// Inline core is its inlining marker, license banner or library body, never usage like gsap.defaults().
const hasInlineGsap = allScriptTexts.some(
(t) =>
/\/\*\s*inlined:.*gsap/i.test(t) ||
/\b_gsScope\b/.test(t) ||
/\bGreenSock\b/.test(t) ||
/\bgsap\.(config|defaults|version)\b/.test(t) ||
(t.length > 5000 && /\bgsap\b/i.test(t)),
[...t.matchAll(/\/\*\s*inlined:\s*(\S+)\s*\*\//gi)].some((m) =>
isGsapCoreSrc(m[1] ?? ""),
) ||
/\/\*!\s*\*\s*GSAP\s+\d/.test(t) ||
/Missing plugin\? gsap\.registerPlugin\(\)/.test(t) ||
/\b_gsScope\b/.test(t),
);
if (!usesGsap || hasGsapScript || hasInlineGsap || canInheritGsapFromHost) return [];
@@ -0,0 +1,97 @@
// @vitest-environment node
import { describe, expect, it } from "vitest";
import { htmlHasGsapCore, insertGsapCoreBeforeHeadPlugin } from "./gsapCoreScript";
const script = (src: string) => `<html><head><script src="${src}"></script></head></html>`;
const PLUGIN = "https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js";
const CORE = `<script src="core/gsap.min.js"></script>`;
describe("htmlHasGsapCore", () => {
it.each([
"https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.js",
"/api/vendor/npm/gsap@3.15.0/dist/gsap.min.js",
"./vendor/gsap.min.js",
"assets/gsap-3.14.2.min.js",
"gsap.min.js?v=2",
])("finds GSAP core at %s", (src) => {
expect(htmlHasGsapCore(script(src))).toBe(true);
});
it.each([
"https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js",
"https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/CustomEase.min.js",
"/api/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js",
"./vendor/gsap/ScrollTrigger.js",
])("does not treat the plugin %s as GSAP core", (src) => {
expect(htmlHasGsapCore(script(src))).toBe(false);
});
it("reads core from a producer inlining marker, but not a plugin's", () => {
const inlined = (src: string) => `<script>/* inlined: ${src} */\nvoid 0;</script>`;
expect(htmlHasGsapCore(inlined("https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"))).toBe(
true,
);
expect(
htmlHasGsapCore(inlined("https://cdn.jsdelivr.net/npm/gsap@3/dist/MotionPathPlugin.min.js")),
).toBe(false);
});
it("reads core from its license banner, but not a plugin's", () => {
const banner = (name: string) =>
`<script>/*!\n * ${name} 3.15.0\n * https://gsap.com\n * @license Copyright 2026, GreenSock.\n */\n</script>`;
expect(htmlHasGsapCore(banner("GSAP"))).toBe(true);
expect(htmlHasGsapCore(banner("MotionPathPlugin"))).toBe(false);
});
it.each([
"gsap.registerPlugin(MotionPathPlugin);",
'gsap.defaults({ ease: "none" }); gsap.config({ nullTargetWarn: false });',
'gsap.to("#a", { x: 1 }); /* GreenSock */',
])("does not treat the authored usage %s as GSAP core", (usage) => {
const html = `<head><script src="${PLUGIN}"></script><script>${usage}</script></head>`;
expect(htmlHasGsapCore(html)).toBe(false);
});
it("reads core from its library body", () => {
const body = `function S(t,e){console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}`;
expect(htmlHasGsapCore(`<script>${body}</script>`)).toBe(true);
});
it("ignores GSAP core inside a <template>", () => {
const src = "https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js";
expect(htmlHasGsapCore(`<template><script src="${src}"></script></template>`)).toBe(false);
});
});
describe("insertGsapCoreBeforeHeadPlugin", () => {
it.each([
["a plugin src", `<script src="${PLUGIN}"></script>`],
["a local plugin", `<script src="./vendor/CustomEase.min.js"></script>`],
["an inlined plugin", `<script>/* inlined: ${PLUGIN} */</script>`],
[
"a plugin license banner",
`<script>/*!\n * MotionPathPlugin 3.15.0\n * https://gsap.com\n */</script>`,
],
])("puts core before %s in the head", (_name, plugin) => {
const html = `<html><head><script>var a;</script>${plugin}</head><body></body></html>`;
const out = insertGsapCoreBeforeHeadPlugin(html, CORE) ?? "";
expect(out.indexOf(CORE)).toBeGreaterThan(out.indexOf("var a;"));
expect(out.indexOf(CORE)).toBeLessThan(out.indexOf(plugin));
});
it("works on a bare head fragment", () => {
const out = insertGsapCoreBeforeHeadPlugin(`<script src="${PLUGIN}"></script>`, CORE) ?? "";
expect(out.startsWith(CORE)).toBe(true);
});
it("leaves the head alone when the only plugin is in the body or a template", () => {
const tag = `<script src="${PLUGIN}"></script>`;
expect(
insertGsapCoreBeforeHeadPlugin(`<html><head></head><body>${tag}</body></html>`, CORE),
).toBeNull();
expect(
insertGsapCoreBeforeHeadPlugin(`<head><template>${tag}</template></head>`, CORE),
).toBeNull();
});
});
@@ -0,0 +1,57 @@
import { findStartTags, insertBeforeCloseTag } from "@hyperframes/core/compiler/html-document";
const URL_BASE = "http://localhost/";
function urlPath(src: string): string | null {
return URL.canParse(src, URL_BASE) ? new URL(src, URL_BASE).pathname : null;
}
export function isGsapCoreUrl(src: string): boolean {
return /\/gsap(-\d+(\.\d+)*)?(\.min)?\.js$/i.test(urlPath(src) ?? "");
}
const GSAP_PLUGIN_FILE =
/\/(\w+Plugin|CustomBounce|CustomEase|CustomWiggle|Draggable|EasePack|Flip|GSDevTools|MotionPathHelper|Observer|ScrollSmoother|ScrollTrigger|SplitText)(\.min)?\.js$/i;
function isGsapPluginUrl(src: string): boolean {
const path = urlPath(src);
if (!path || isGsapCoreUrl(src)) return false;
return GSAP_PLUGIN_FILE.test(path) || /\/gsap(@[^/]*)?\/(.+\/)?[^/]+\.js$/i.test(path);
}
const TEMPLATE_ELEMENT = /<template\b[^>]*>[\s\S]*?<\/template>/gi;
const SCRIPT_SRC = /<script\b[^>]*?\ssrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
const INLINED_MARKER = /\/\*\s*inlined:\s*(\S+)\s*\*\//gi;
const CORE_LICENSE_BANNER = /\/\*!\s*\*\s*GSAP\s+\d/;
const PLUGIN_LICENSE_BANNER = /\/\*!\s*\*\s*(?!GSAP\s)\w+\s+\d[^*]*\*\s*https:\/\/gsap\.com/;
const CORE_BODY = /Missing plugin\? gsap\.registerPlugin\(\)/;
function scriptUrls(html: string): string[] {
const srcs = [...html.matchAll(SCRIPT_SRC)].map((m) =>
(m[1] ?? m[2] ?? m[3] ?? "").replace(/&amp;/gi, "&"),
);
return [...srcs, ...[...html.matchAll(INLINED_MARKER)].map((m) => m[1] ?? "")];
}
// Only a loaded core script counts: authored usage such as gsap.registerPlugin() proves nothing.
export function htmlHasGsapCore(html: string): boolean {
const live = html.replace(TEMPLATE_ELEMENT, "");
return (
scriptUrls(live).some(isGsapCoreUrl) || CORE_LICENSE_BANNER.test(live) || CORE_BODY.test(live)
);
}
function isGsapPluginScript(script: string): boolean {
return scriptUrls(script).some(isGsapPluginUrl) || PLUGIN_LICENSE_BANNER.test(script);
}
/** Puts `coreTag` ahead of the first GSAP plugin script in the head; null when the head has none. */
export function insertGsapCoreBeforeHeadPlugin(html: string, coreTag: string): string | null {
const lowered = html.toLowerCase();
const at = findStartTags(html, "script").find((start) => {
const close = lowered.indexOf("</script", start);
return isGsapPluginScript(html.slice(start, close === -1 ? undefined : close));
});
if (at === undefined || insertBeforeCloseTag(html.slice(0, at), "head", "") !== null) return null;
return `${html.slice(0, at)}${coreTag}\n${html.slice(at)}`;
}
@@ -29,6 +29,21 @@ describe("buildSubCompositionHtml", () => {
expect(html).toContain(`<script src="${gsapCdnDist()}gsap.min.js"></script>`);
});
it.each(["", "<script>gsap.registerPlugin(MotionPathPlugin);</script>"])(
"loads GSAP core before a project head that has only a GSAP plugin (%#)",
(usage) => {
const plugin = `<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js"></script>`;
const dir = makeTempProject({
"index.html": `<!doctype html><html><head>${plugin}${usage}</head><body></body></html>`,
"compositions/scene.html": `<div data-composition-id="scene" data-width="320" data-height="180"></div>`,
});
const html = buildSubCompositionHtml(dir, "compositions/scene.html", "/api/runtime.js") ?? "";
const core = html.indexOf(`<script src="${gsapCdnDist()}gsap.min.js"></script>`);
expect(core).toBeGreaterThanOrEqual(0);
expect(core).toBeLessThan(html.indexOf(plugin));
},
);
it("adds the preview runtime even when the project head's script mentions a runtime file", () => {
const dir = makeTempProject({
"index.html": `<!doctype html><html><head><script>console.log("hyperframe.runtime.iife.js");</script></head><body></body></html>`,
@@ -13,6 +13,7 @@ import {
} from "@hyperframes/core/compiler";
import { isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
import { htmlHasGsapCore, insertGsapCoreBeforeHeadPlugin } from "./gsapCoreScript.js";
/**
* Rewrite relative asset paths in a parsed DOM tree. Shared across all
@@ -353,8 +354,9 @@ export function buildSubCompositionHtml(
headContent += `\n<script ${RUNTIME_BOOTSTRAP_ATTR}="1" src="${runtimeUrl}"></script>`;
// Fallback: if no index.html head was found, add minimal deps
if (!headContent.includes("gsap")) {
headContent += `\n<script src="${gsapCdnDist(undefined, gsapBaseUrl).replace(/&/g, "&amp;")}gsap.min.js"></script>`;
if (!htmlHasGsapCore(headContent)) {
const core = `<script src="${gsapCdnDist(undefined, gsapBaseUrl).replace(/&/g, "&amp;")}gsap.min.js"></script>`;
headContent = insertGsapCoreBeforeHeadPlugin(headContent, core) ?? `${headContent}\n${core}`;
}
const htmlOpen = htmlAttrs ? `<html ${htmlAttrs}>` : "<html>";
@@ -359,6 +359,67 @@ describe("registerPreviewRoutes", () => {
expect(html.indexOf("CustomEase.min.js")).toBeLessThan(html.indexOf("__hfStudioMotionApply"));
});
it("injects GSAP core before the MotionPathPlugin when Studio motion and a motionPath share a page with no GSAP", async () => {
const projectDir = createProjectDir();
writeFileSync(
join(projectDir, "index.html"),
`<!doctype html><html><head></head><body><div id="card"></div>
<script>
window.__hfPath = { motionPath: { path: [{ x: 0, y: 0 }, { x: 100, y: 50 }] } };
</script>
</body></html>`,
);
const manifestDir = join(projectDir, ".hyperframes");
mkdirSync(manifestDir, { recursive: true });
writeFileSync(
join(manifestDir, "studio-motion.json"),
`{"version":1,"motions":[{"kind":"gsap-motion","target":{"sourceFile":"index.html","id":"card"},"start":0,"duration":1,"ease":"power2.out","from":{"y":32},"to":{"y":0}}]}`,
);
const app = new Hono();
registerPreviewRoutes(app, createAdapter(projectDir));
const response = await app.request("http://localhost/projects/demo/preview");
const srcs = [...parseHTML(await response.text()).document.querySelectorAll("script[src]")].map(
(el) => el.getAttribute("src") ?? "",
);
expect(response.status).toBe(200);
const core = srcs.findIndex((src) => src.endsWith("/gsap.min.js"));
const plugin = srcs.findIndex((src) => src.endsWith("/MotionPathPlugin.min.js"));
expect(core).toBeGreaterThanOrEqual(0);
expect(plugin).toBeGreaterThan(core);
});
it.each([
["", ""],
["", "<script>gsap.registerPlugin(MotionPathPlugin);</script>"],
["<script>var early = 1;</script>", "<script>gsap.registerPlugin(MotionPathPlugin);</script>"],
])("injects GSAP core before an authored head MotionPathPlugin (%#)", async (before, after) => {
const projectDir = createProjectDir();
writeFileSync(
join(projectDir, "index.html"),
`<!doctype html><html><head>${before}<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js"></script>${after}</head><body><div id="card"></div></body></html>`,
);
const manifestDir = join(projectDir, ".hyperframes");
mkdirSync(manifestDir, { recursive: true });
writeFileSync(
join(manifestDir, "studio-motion.json"),
`{"version":1,"motions":[{"kind":"gsap-motion","target":{"sourceFile":"index.html","id":"card"},"start":0,"duration":1,"ease":"power2.out","from":{"y":32},"to":{"y":0}}]}`,
);
const app = new Hono();
registerPreviewRoutes(app, createAdapter(projectDir));
const response = await app.request("http://localhost/projects/demo/preview");
const srcs = [...parseHTML(await response.text()).document.querySelectorAll("script[src]")].map(
(el) => el.getAttribute("src") ?? "",
);
expect(response.status).toBe(200);
const core = srcs.findIndex((src) => src.endsWith("/gsap.min.js"));
expect(core).toBeGreaterThanOrEqual(0);
expect(srcs.findIndex((src) => src.endsWith("/MotionPathPlugin.min.js"))).toBeGreaterThan(core);
});
it("injects the GSAP MotionPathPlugin when the composition uses a motionPath", async () => {
const projectDir = createProjectDir();
writeFileSync(
+33 -40
View File
@@ -35,6 +35,11 @@ import {
} from "../helpers/safePath.js";
import { getMimeType } from "../helpers/mime.js";
import { buildSubCompositionHtml, hasBaseElement } from "../helpers/subComposition.js";
import {
htmlHasGsapCore,
insertGsapCoreBeforeHeadPlugin,
isGsapCoreUrl,
} from "../helpers/gsapCoreScript.js";
import {
createProjectSignature,
resolveProjectAndSignature,
@@ -138,18 +143,6 @@ function injectScriptTagIntoHead(html: string, scriptTag: string): string {
return insertBeforeCloseTag(html, "head", `${scriptTag}\n`) ?? `${scriptTag}\n${html}`;
}
function htmlHasGsap(html: string): boolean {
// Only match GSAP references outside <template> elements — scripts inside
// templates are inert when cloned and don't make GSAP globally available.
const outsideTemplates = html.replace(/<template\b[^>]*>[\s\S]*?<\/template>/gi, "");
return (
/<script\b[^>]*src=["'][^"']*gsap/i.test(outsideTemplates) ||
/\/\*\s*inlined:.*gsap/i.test(outsideTemplates) ||
/\b(GreenSock|_gsScope)\b/.test(outsideTemplates) ||
/\bgsap\.(config|defaults|registerPlugin|version)\b/.test(outsideTemplates)
);
}
function htmlHasCustomEase(html: string): boolean {
return (
/<script\b[^>]*src=["'][^"']*CustomEase/i.test(html) ||
@@ -191,11 +184,6 @@ function injectMotionPathPluginIfNeeded(html: string, baseUrl: string): string {
return injectScriptTagIntoHead(html, gsapScriptTag("MotionPathPlugin.min.js", baseUrl));
}
function isGsapCoreUrl(src: string): boolean {
const base = "http://localhost/";
return URL.canParse(src, base) && /\/gsap(\.min)?\.js$/i.test(new URL(src, base).pathname);
}
type GsapCoreScript = { src: string; type: string | null; defer: boolean; end: number };
function findGsapCoreScript(html: string): GsapCoreScript | null {
@@ -238,28 +226,30 @@ function injectStudioMotionDependencies(
const manifest = parseStudioMotionManifestContent(manifestContent);
if (!manifest.hasMotion) return html;
let next = html;
if (!htmlHasGsap(next))
next = injectScriptTagIntoHead(next, gsapScriptTag("gsap.min.js", baseUrl));
if (!htmlHasGsapCore(next)) {
const core = gsapScriptTag("gsap.min.js", baseUrl);
next = insertGsapCoreBeforeHeadPlugin(next, core) ?? injectScriptTagIntoHead(next, core);
}
if (manifest.hasCustomEase && !htmlHasCustomEase(next)) {
next = injectScriptTagIntoHead(next, gsapScriptTag("CustomEase.min.js", baseUrl));
}
return next;
}
function injectStudioMotionScript(
html: string,
function readStudioMotion(
projectDir: string,
activeCompositionPath: string,
baseUrl: string,
): string {
): { manifestContent: string; script: string } | null {
const manifestContent = readStudioMotionManifestContent(projectDir);
const script = createStudioMotionRenderBodyScript(manifestContent, {
activeCompositionPath,
});
if (!script) return html;
const withDependencies = injectStudioMotionDependencies(html, manifestContent, baseUrl);
return script ? { manifestContent, script } : null;
}
function injectStudioMotionScript(html: string, script: string): string {
const tag = `<script type="${AFTER_FONTS_SCRIPT_TYPE}">${script.replace(/<\/script/gi, "<\\/script")}</script>`;
return insertBeforeCloseTag(withDependencies, "body", tag) ?? `${withDependencies}${tag}`;
return insertBeforeCloseTag(html, "body", tag) ?? `${html}${tag}`;
}
const GSAP_FALLBACK_ATTR = "data-hf-gsap-fallback";
@@ -362,24 +352,27 @@ function injectStudioPreviewAugmentations(
lazyPreviewImages(html),
`<meta name="${STUDIO_PREVIEW_MARK_META}">${EARLY_ERRORS_SCRIPT}`,
);
return injectStudioMotionScript(
injectMotionPathPluginIfNeeded(
injectGsapCdnFallback(
injectProjectSignature(
injectTagsAtHeadStart(
marked,
`<meta name="${GSAP_BASE_URL_META}" content="${escapeGsapUrl(options.gsapBaseUrl)}">`,
),
resolveProjectSignature(adapter, projectDir),
),
options.gsapBaseUrl,
const withFallback = injectGsapCdnFallback(
injectProjectSignature(
injectTagsAtHeadStart(
marked,
`<meta name="${GSAP_BASE_URL_META}" content="${escapeGsapUrl(options.gsapBaseUrl)}">`,
),
options.gsapBaseUrl,
resolveProjectSignature(adapter, projectDir),
),
projectDir,
activeCompositionPath,
options.gsapBaseUrl,
);
const studioMotion = readStudioMotion(projectDir, activeCompositionPath);
// Studio motion's GSAP core goes in first, so a MotionPathPlugin injected next lands after it.
const withMotionDependencies = studioMotion
? injectStudioMotionDependencies(
withFallback,
studioMotion.manifestContent,
options.gsapBaseUrl,
)
: withFallback;
const withPlugin = injectMotionPathPluginIfNeeded(withMotionDependencies, options.gsapBaseUrl);
return studioMotion ? injectStudioMotionScript(withPlugin, studioMotion.script) : withPlugin;
}
async function transformPreviewHtml(