fix(lint): catch fromTo from-object layout/reflow props in gsap_non_transform_motion

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) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-06-29 11:11:35 -07:00
co-authored by Claude Opus 4.8
parent 6e21072f44
commit 189d8582f4
2 changed files with 27 additions and 1 deletions
+16
View File
@@ -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 = `
<html><body>
<div data-composition-id="c1" data-width="1920" data-height="1080"><div id="t"></div></div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.fromTo("#t", { left: 100, letterSpacing: "0.3em" }, { opacity: 1, duration: 1 }, 0);
window.__timelines["c1"] = tl;
</script>
</body></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 = `
<html><body>
+11 -1
View File
@@ -4,6 +4,9 @@ interface LintParsedGsap {
method: string;
position: number | string;
properties: Record<string, number | string>;
// fromTo() exposes its first ("from") vars object separately; a layout/reflow prop
// that appears only here still animates and must be checked.
fromProperties?: Record<string, number | string>;
duration?: number;
ease?: string;
extras?: Record<string, unknown>;
@@ -1198,7 +1201,14 @@ export const gsapRules: LintRule<LintContext>[] = [
...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)),