From 189d8582f46d74083982a0408fd0f9d973aebf56 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Mon, 29 Jun 2026 11:11:35 -0700 Subject: [PATCH] fix(lint): catch fromTo from-object layout/reflow props in gsap_non_transform_motion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rule sourced a tween's animated props from anim.properties only — the acorn parser's to-vars. A fromTo() exposes its first ("from") vars object separately as anim.fromProperties, so a layout or reflow prop animated only in the from-object (e.g. tl.fromTo("#t", { left: 100, letterSpacing: "0.3em" }, { opacity: 1 })) escaped the rule entirely and shipped stuttering. fromTo is the most common tween form, so this was a real recall hole. Union fromProperties into the checked property set; add the field to the lint parse type. Registry re-scan unchanged (0 comps animate a layout prop only in a from-object today), so no collateral. Known remaining gaps (documented, not fixed): standalone gsap.fromTo only scans its first vars object via regex (and aborts on nested braces); roundProps:true (boolean form) is dropped by the parser. Both are rare and the clean fix is disproportionate to the rule's planned sunset when drawElement render lands. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/lint/src/rules/gsap.test.ts | 16 ++++++++++++++++ packages/lint/src/rules/gsap.ts | 12 +++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) 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)),