mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(studio): one owner for gsap CDN URLs, so MotionPathPlugin matches the composition's gsap (#4976)
* fix(studio): load MotionPathPlugin at the composition's own gsap version * fix(core): list the gsap-cdn subpath where core's exports are generated from * fix(core): every default gsap URL comes from the one gsap-cdn owner * fix(core): the gsap a new composition or first tween gets comes from the gsap-cdn owner
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
parseCubeLut,
|
||||
serializeHfColorGrading,
|
||||
} from "@hyperframes/core";
|
||||
import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
|
||||
import { defineCommand } from "citty";
|
||||
import sharp from "sharp";
|
||||
import type { Example } from "./_examples.js";
|
||||
@@ -344,7 +345,7 @@ export function buildGradeCompareHtml(options: GradeCompareHtmlOptions): string
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=${metrics.width}, height=${metrics.height}" />
|
||||
<title>HyperFrames Grade Compare</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="${gsapCdnDist()}gsap.min.js"></script>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
|
||||
@@ -80,6 +80,12 @@
|
||||
"types": "./dist/studioPreviewMark.d.ts",
|
||||
"environments": ["browser", "bun", "node"]
|
||||
},
|
||||
"./gsap-cdn": {
|
||||
"source": "./src/gsapCdn.ts",
|
||||
"runtime": "./dist/gsapCdn.js",
|
||||
"types": "./dist/gsapCdn.d.ts",
|
||||
"environments": ["browser", "bun", "node"]
|
||||
},
|
||||
"./media-timing": {
|
||||
"source": "./src/mediaTiming.ts",
|
||||
"runtime": "./dist/mediaTiming.js",
|
||||
|
||||
@@ -94,6 +94,12 @@
|
||||
"import": "./src/studioPreviewMark.ts",
|
||||
"types": "./src/studioPreviewMark.ts"
|
||||
},
|
||||
"./gsap-cdn": {
|
||||
"bun": "./src/gsapCdn.ts",
|
||||
"node": "./dist/gsapCdn.js",
|
||||
"import": "./src/gsapCdn.ts",
|
||||
"types": "./src/gsapCdn.ts"
|
||||
},
|
||||
"./media-timing": {
|
||||
"bun": "./src/mediaTiming.ts",
|
||||
"node": "./dist/mediaTiming.js",
|
||||
@@ -540,6 +546,10 @@
|
||||
"import": "./dist/studioPreviewMark.js",
|
||||
"types": "./dist/studioPreviewMark.d.ts"
|
||||
},
|
||||
"./gsap-cdn": {
|
||||
"import": "./dist/gsapCdn.js",
|
||||
"types": "./dist/gsapCdn.d.ts"
|
||||
},
|
||||
"./media-timing": {
|
||||
"import": "./dist/mediaTiming.js",
|
||||
"types": "./dist/mediaTiming.d.ts"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { gsapCdnDist, motionPathPluginUrl } from "./gsapCdn";
|
||||
|
||||
describe("gsapCdnDist", () => {
|
||||
it("serves the composition's gsap version", () => {
|
||||
expect(gsapCdnDist("3.14.2")).toBe("https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/");
|
||||
expect(motionPathPluginUrl("3")).toBe(
|
||||
"https://cdn.jsdelivr.net/npm/gsap@3/dist/MotionPathPlugin.min.js",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([undefined, "", "latest", "3.15.0/../x"])("falls back to the default for %j", (v) => {
|
||||
expect(gsapCdnDist(v)).toBe(gsapCdnDist());
|
||||
expect(gsapCdnDist()).toMatch(
|
||||
/^https:\/\/cdn\.jsdelivr\.net\/npm\/gsap@\d+\.\d+\.\d+\/dist\/$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
const GSAP_CDN_VERSION = "3.15.0";
|
||||
|
||||
export function gsapCdnDist(gsapVersion?: string): string {
|
||||
const version = gsapVersion && /^\d+(\.\d+)*$/.test(gsapVersion) ? gsapVersion : GSAP_CDN_VERSION;
|
||||
return `https://cdn.jsdelivr.net/npm/gsap@${version}/dist/`;
|
||||
}
|
||||
|
||||
/** MotionPathPlugin at the composition's own gsap version; a skew registers with a GSAP warning. */
|
||||
export function motionPathPluginUrl(gsapVersion?: string): string {
|
||||
return `${gsapCdnDist(gsapVersion)}MotionPathPlugin.min.js`;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TEXT_STYLES,
|
||||
ZOOM_CONTAINER_STYLES,
|
||||
} from "./constants.js";
|
||||
import { gsapCdnDist } from "../gsapCdn.js";
|
||||
|
||||
describe("generateBaseHtml", () => {
|
||||
it("generates valid HTML structure with DOCTYPE", () => {
|
||||
@@ -76,6 +77,10 @@ describe("getStageStyles", () => {
|
||||
});
|
||||
|
||||
describe("constants", () => {
|
||||
it("GSAP_CDN is the default gsap from the one gsap-cdn owner", () => {
|
||||
expect(GSAP_CDN).toBe(`${gsapCdnDist()}gsap.min.js`);
|
||||
});
|
||||
|
||||
it("GSAP_CDN is a valid URL", () => {
|
||||
expect(GSAP_CDN).toMatch(/^https:\/\//);
|
||||
expect(GSAP_CDN).toContain("gsap");
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const GSAP_CDN = "https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js";
|
||||
import { gsapCdnDist } from "../gsapCdn";
|
||||
|
||||
export const GSAP_CDN = `${gsapCdnDist()}gsap.min.js`;
|
||||
|
||||
export const BASE_STYLES = `* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; display: flex; align-items: center; justify-content: center; }`;
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
type UnresolvedElement,
|
||||
} from "@hyperframes/core";
|
||||
import { MAX_AUDIO_GAIN } from "@hyperframes/core/audio-gain";
|
||||
import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
|
||||
import {
|
||||
assignBundledRuntimeCompositionIds,
|
||||
assignMediaRenderIds,
|
||||
@@ -1884,7 +1885,7 @@ export interface CompileForRenderOptions {
|
||||
variables?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const GSAP_CDN_BASE = "https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/";
|
||||
const GSAP_CDN_BASE = gsapCdnDist();
|
||||
|
||||
function rewriteUnresolvableGsapToCdn(html: string, projectDir: string): string {
|
||||
return html.replace(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it, onTestFinished } from "vitest";
|
||||
import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
|
||||
import { buildSubCompositionHtml, hasBaseElement } from "./subComposition";
|
||||
|
||||
function makeTempProject(files: Record<string, string>): string {
|
||||
@@ -17,6 +18,15 @@ function makeTempProject(files: Record<string, string>): string {
|
||||
}
|
||||
|
||||
describe("buildSubCompositionHtml", () => {
|
||||
it("loads the default gsap version when the project head has no gsap", () => {
|
||||
const dir = makeTempProject({
|
||||
"index.html": `<!doctype html><html><head><title>Host</title></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");
|
||||
expect(html).toContain(`<script src="${gsapCdnDist()}gsap.min.js"></script>`);
|
||||
});
|
||||
|
||||
it("adds the preview base even when the project head's script mentions a <base>", () => {
|
||||
const dir = makeTempProject({
|
||||
"index.html": `<!doctype html><html><head><script>if (0) document.write('<base href="../">');</script></head><body></body></html>`,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@hyperframes/core";
|
||||
import { stripEmbeddedRuntimeScripts } from "@hyperframes/core/compiler";
|
||||
import { isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
|
||||
import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
|
||||
|
||||
/**
|
||||
* Rewrite relative asset paths in a parsed DOM tree. Shared across all
|
||||
@@ -349,7 +350,7 @@ export function buildSubCompositionHtml(
|
||||
|
||||
// Fallback: if no index.html head was found, add minimal deps
|
||||
if (!headContent.includes("gsap")) {
|
||||
headContent += `\n<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>`;
|
||||
headContent += `\n<script src="${gsapCdnDist()}gsap.min.js"></script>`;
|
||||
}
|
||||
|
||||
const htmlOpen = htmlAttrs ? `<html ${htmlAttrs}>` : "<html>";
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type BundleOptions,
|
||||
} from "@hyperframes/core/compiler";
|
||||
import { STUDIO_PREVIEW_MARK_META } from "@hyperframes/core/studio-preview-mark";
|
||||
import { gsapCdnDist, motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
|
||||
import { injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
|
||||
import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
||||
import type { ResolvedProject, StudioApiAdapter } from "../types.js";
|
||||
@@ -63,10 +64,9 @@ import { requestSubPath } from "../helpers/requestSubPath.js";
|
||||
import { lazyPreviewImages } from "../helpers/lazyPreviewImages.js";
|
||||
|
||||
const PROJECT_SIGNATURE_META = "hyperframes-project-signature";
|
||||
const GSAP_CDN_VERSION = "3.15.0";
|
||||
const GSAP_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/gsap.min.js"></script>`;
|
||||
const GSAP_CUSTOM_EASE_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/CustomEase.min.js"></script>`;
|
||||
const GSAP_MOTION_PATH_CDN_SCRIPT = `<script src="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/MotionPathPlugin.min.js"></script>`;
|
||||
const GSAP_CDN_SCRIPT = `<script src="${gsapCdnDist()}gsap.min.js"></script>`;
|
||||
const GSAP_CUSTOM_EASE_CDN_SCRIPT = `<script src="${gsapCdnDist()}CustomEase.min.js"></script>`;
|
||||
const GSAP_MOTION_PATH_CDN_SCRIPT = `<script src="${motionPathPluginUrl()}"></script>`;
|
||||
|
||||
function injectProjectSignature(html: string, signature: string): string {
|
||||
const tag = `<meta name="${PROJECT_SIGNATURE_META}" content="${signature}">`;
|
||||
@@ -157,10 +157,8 @@ function injectMotionPathPluginIfNeeded(html: string): string {
|
||||
const gsapScript = /<script\b[^>]*\bsrc=["'][^"']*\/gsap(\.min)?\.js["'][^>]*>\s*<\/script>/i;
|
||||
const match = html.match(gsapScript);
|
||||
if (match) {
|
||||
// Match the plugin version to the composition's own gsap so the plugin
|
||||
// registers cleanly (a minor-version skew triggers a GSAP compatibility warning).
|
||||
const version = match[0].match(/gsap@([\d.]+)/)?.[1] ?? GSAP_CDN_VERSION;
|
||||
const pluginTag = `<script src="https://cdn.jsdelivr.net/npm/gsap@${version}/dist/MotionPathPlugin.min.js"></script>`;
|
||||
const version = match[0].match(/gsap@([\d.]+)/)?.[1];
|
||||
const pluginTag = `<script src="${motionPathPluginUrl(version)}"></script>`;
|
||||
const end = html.indexOf(match[0]) + match[0].length;
|
||||
return html.slice(0, end) + "\n" + pluginTag + html.slice(end);
|
||||
}
|
||||
@@ -199,7 +197,7 @@ function injectStudioMotionScript(
|
||||
const GSAP_FALLBACK_ATTR = "data-hf-gsap-fallback";
|
||||
const GSAP_CDN_FALLBACK_SCRIPT = `<script ${GSAP_FALLBACK_ATTR}>
|
||||
(function(){
|
||||
var cdnBase="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/";
|
||||
var cdnBase="${gsapCdnDist()}";
|
||||
var loaded={};
|
||||
function loadFallback(file){
|
||||
if(loaded[file])return loaded[file];
|
||||
|
||||
@@ -249,7 +249,7 @@ describe("applySoftReload", () => {
|
||||
// gsap present but MotionPathPlugin unset → async load path.
|
||||
const { iframe, contentWindow } = buildMockIframe({
|
||||
MotionPathPlugin: undefined,
|
||||
gsap: { timeline: vi.fn(), registerPlugin: vi.fn() },
|
||||
gsap: { timeline: vi.fn(), registerPlugin: vi.fn(), version: "3.14.2" },
|
||||
});
|
||||
(iframe.contentDocument as unknown as { head: unknown }).head = head;
|
||||
|
||||
@@ -260,7 +260,7 @@ describe("applySoftReload", () => {
|
||||
// script has NOT executed yet, so the timeline isn't rebound synchronously.
|
||||
expect(result).toBe("applied");
|
||||
expect(appendedScripts).toHaveLength(1);
|
||||
expect(appendedScripts[0]!.src).toContain("MotionPathPlugin");
|
||||
expect(appendedScripts[0]!.src).toContain("gsap@3.14.2/dist/MotionPathPlugin");
|
||||
expect(contentWindow.__hfForceTimelineRebind).not.toHaveBeenCalled();
|
||||
|
||||
// onerror must NOT run the script (that would reference a missing plugin) —
|
||||
@@ -384,6 +384,16 @@ describe("ensureMotionPathPluginLoaded", () => {
|
||||
expect(appendedScripts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("loads the plugin at the composition's own gsap version", () => {
|
||||
const { iframe, appendedScripts } = buildBootstrapIframe({
|
||||
gsap: { version: "3.14.2", registerPlugin: vi.fn() },
|
||||
});
|
||||
ensureMotionPathPluginLoaded(iframe);
|
||||
expect(appendedScripts[0]!.src).toBe(
|
||||
"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/MotionPathPlugin.min.js",
|
||||
);
|
||||
});
|
||||
|
||||
it("appends the plugin script once and registers it on load", () => {
|
||||
const { iframe, contentWindow, appendedScripts, registerPlugin } = buildBootstrapIframe();
|
||||
ensureMotionPathPluginLoaded(iframe);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
|
||||
import { motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
|
||||
import { findAuthoredElement } from "./authoredSource";
|
||||
import { applyAuthoredInlineOpacity, readStampedAuthoredOpacity } from "./authoredOpacity";
|
||||
import { authoringFile, collectResetTargets, compositionFile, fileDocs } from "./softReloadTargets";
|
||||
@@ -14,6 +15,7 @@ type IframeWindow = Window & {
|
||||
// re-flash the iframe. Cleared once the plugin loads or errors.
|
||||
__hfMotionPathPluginLoading?: boolean;
|
||||
gsap?: {
|
||||
version?: string;
|
||||
timeline?: (...args: unknown[]) => unknown;
|
||||
registerPlugin?: (...plugins: unknown[]) => unknown;
|
||||
set?: (targets: Element | Element[], vars: Record<string, unknown>) => void;
|
||||
@@ -22,14 +24,6 @@ type IframeWindow = Window & {
|
||||
MotionPathPlugin?: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* CDN URL for the GSAP MotionPathPlugin. Shared between the one-time preview
|
||||
* bootstrap (ensureMotionPathPluginLoaded) and the soft-reload fallback so the
|
||||
* version is pinned in a single place.
|
||||
*/
|
||||
const MOTION_PATH_PLUGIN_CDN =
|
||||
"https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/MotionPathPlugin.min.js";
|
||||
|
||||
/**
|
||||
* Pre-load + register MotionPathPlugin ONCE in the preview iframe so
|
||||
* `win.MotionPathPlugin` is reliably set before any studio edit. Called from the
|
||||
@@ -66,7 +60,7 @@ export function ensureMotionPathPluginLoaded(iframe: HTMLIFrameElement | null):
|
||||
try {
|
||||
win.__hfMotionPathPluginLoading = true;
|
||||
const pluginScript = doc.createElement("script");
|
||||
pluginScript.src = MOTION_PATH_PLUGIN_CDN;
|
||||
pluginScript.src = motionPathPluginUrl(win.gsap?.version);
|
||||
const finalize = () => {
|
||||
win.__hfMotionPathPluginLoading = false;
|
||||
try {
|
||||
@@ -445,7 +439,7 @@ export function applySoftReload(
|
||||
}
|
||||
win.__hfMotionPathPluginLoading = true;
|
||||
const pluginScript = doc.createElement("script");
|
||||
pluginScript.src = MOTION_PATH_PLUGIN_CDN;
|
||||
pluginScript.src = motionPathPluginUrl(win.gsap?.version);
|
||||
pluginScript.onload = () => {
|
||||
win.__hfMotionPathPluginLoading = false;
|
||||
executeScript();
|
||||
|
||||
Reference in New Issue
Block a user