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 = /