diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts index 85089e8eb..e452a20e3 100644 --- a/packages/lint/src/rules/gsap.test.ts +++ b/packages/lint/src/rules/gsap.test.ts @@ -1499,6 +1499,22 @@ describe("GSAP rules", () => { expect(finding).toBeDefined(); }); + it("gsap_non_transform_motion: fires on a layout/reflow prop that appears only in a fromTo's from-object", async () => { + const html = ` + +
+ +`; + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "gsap_non_transform_motion"); + expect(finding).toBeDefined(); + }); + it("gsap_non_transform_motion: fires on text-reflow props (letterSpacing / fontSize)", async () => { const html = ` diff --git a/packages/lint/src/rules/gsap.ts b/packages/lint/src/rules/gsap.ts index baf00a15f..0fa1e8261 100644 --- a/packages/lint/src/rules/gsap.ts +++ b/packages/lint/src/rules/gsap.ts @@ -4,6 +4,9 @@ interface LintParsedGsap { method: string; position: number | string; properties: Record; + // fromTo() exposes its first ("from") vars object separately; a layout/reflow prop + // that appears only here still animates and must be checked. + fromProperties?: Record; duration?: number; ease?: string; extras?: Record; @@ -1198,7 +1201,14 @@ export const gsapRules: LintRule[] = [ ...parsed.animations.map((anim) => ({ method: anim.method, selector: anim.targetSelector, - properties: Object.keys(anim.properties), + // Union the from-vars: a fromTo() can animate a layout/reflow prop that appears + // only in its first ("from") object, which is just as stutter-prone as the to-vars. + properties: [ + ...new Set([ + ...Object.keys(anim.properties), + ...Object.keys(anim.fromProperties ?? {}), + ]), + ], raw: synthesizeWindowRaw(parsed.timelineVar, anim), })), ...extractStandaloneGsapTransformCalls(stripJsComments(script.content)),