diff --git a/packages/core/src/lint/rules/gsap.test.ts b/packages/core/src/lint/rules/gsap.test.ts index 5995ac450..af1547d0b 100644 --- a/packages/core/src/lint/rules/gsap.test.ts +++ b/packages/core/src/lint/rules/gsap.test.ts @@ -142,6 +142,42 @@ describe("GSAP rules", () => { expect(finding).toBeUndefined(); }); + it("does NOT require a local GSAP script for sub-compositions", () => { + const html = ``; + + const result = lintHyperframeHtml(html, { isSubComposition: true }); + const finding = result.findings.find((f) => f.code === "missing_gsap_script"); + expect(finding).toBeUndefined(); + }); + + it("does NOT require a local GSAP script when a template composition is linted in isolation", () => { + const html = ``; + + const result = lintHyperframeHtml(html, { filePath: "compositions/intro.html" }); + const finding = result.findings.find((f) => f.code === "missing_gsap_script"); + expect(finding).toBeUndefined(); + }); + it("ERRORS when GSAP animates visibility on a clip element", () => { const html = ` diff --git a/packages/core/src/lint/rules/gsap.ts b/packages/core/src/lint/rules/gsap.ts index 2e70c27d2..7a6c7b588 100644 --- a/packages/core/src/lint/rules/gsap.ts +++ b/packages/core/src/lint/rules/gsap.ts @@ -432,11 +432,13 @@ export const gsapRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [ }, // missing_gsap_script - ({ scripts }) => { + ({ scripts, rawSource, options }) => { const allScriptTexts = scripts.filter((s) => !/\bsrc\s*=/.test(s.attrs)).map((s) => s.content); const allScriptSrcs = scripts .map((s) => readAttr(` +`; + + const result = detectRenderModeHints(html); + + expect(result.recommendScreenshot).toBe(false); + expect(result.reasons).toEqual([]); + }); + + it("still flags user-authored requestAnimationFrame inside nested composition scripts", () => { + const html = ` + +
+ +`; + + const result = detectRenderModeHints(html); + + expect(result.recommendScreenshot).toBe(true); + expect(result.reasons.map((reason) => reason.code)).toEqual(["requestAnimationFrame"]); + }); + + it("does not recommend screenshot mode for nested compositions that hoist GSAP from a CDN script", async () => { + const projectDir = mkdtempSync(join(tmpdir(), "hf-render-mode-")); + const compositionsDir = join(projectDir, "compositions"); + mkdirSync(compositionsDir, { recursive: true }); + + writeFileSync( + join(projectDir, "index.html"), + ` + +
+
+
+`, + ); + writeFileSync( + join(compositionsDir, "intro.html"), + ``, + ); + + const originalFetch = globalThis.fetch; + globalThis.fetch = mock(async () => { + return new Response( + "window.gsap = { timeline: function() { return { paused: true }; } }; function __ticker(){ requestAnimationFrame(__ticker); }", + { status: 200 }, + ); + }) as any; + + try { + const result = await compileForRender(projectDir, join(projectDir, "index.html"), projectDir); + + expect(result.renderModeHints.recommendScreenshot).toBe(false); + expect(result.renderModeHints.reasons).toEqual([]); + } finally { + globalThis.fetch = originalFetch; + } + }); }); diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts index 0750814d1..892c292de 100644 --- a/packages/producer/src/services/htmlCompiler.ts +++ b/packages/producer/src/services/htmlCompiler.ts @@ -74,11 +74,23 @@ function dedupeElementsById(elements: T[]): T[] { } const INLINE_SCRIPT_PATTERN = /]*)>([\s\S]*?)<\/script>/gi; +const COMPILER_MOUNT_BLOCK_START = "/* __HF_COMPILER_MOUNT_START__ */"; +const COMPILER_MOUNT_BLOCK_END = "/* __HF_COMPILER_MOUNT_END__ */"; function stripJsComments(source: string): string { return source.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); } +function stripCompilerMountBootstrap(source: string): string { + return source.replace( + new RegExp( + `${COMPILER_MOUNT_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${COMPILER_MOUNT_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, + "g", + ), + "", + ); +} + export function detectRenderModeHints(html: string): RenderModeHints { const reasons: RenderModeHint[] = []; const { document } = parseHTML(html); @@ -96,7 +108,7 @@ export function detectRenderModeHints(html: string): RenderModeHints { while ((scriptMatch = scriptPattern.exec(html)) !== null) { const attrs = scriptMatch[1] || ""; if (/\bsrc\s*=/i.test(attrs)) continue; - const content = stripJsComments(scriptMatch[2] || ""); + const content = stripJsComments(stripCompilerMountBootstrap(scriptMatch[2] || "")); if (!/requestAnimationFrame\s*\(/.test(content)) continue; reasons.push({ code: "requestAnimationFrame", @@ -664,6 +676,7 @@ function inlineSubCompositions( } }; if (!__compId) { __run(); return; } + ${COMPILER_MOUNT_BLOCK_START} var __selector = '[data-composition-id="' + (__compId + '').replace(/"/g, '\\\\"') + '"]'; var __attempt = 0; var __tryRun = function() { @@ -672,6 +685,7 @@ function inlineSubCompositions( requestAnimationFrame(__tryRun); }; __tryRun(); + ${COMPILER_MOUNT_BLOCK_END} })()`); } scriptEl.remove();