fix(lint): allow bounded GSAP infinite repeats (#2763)

This commit is contained in:
James Russo
2026-07-24 14:56:03 -04:00
committed by GitHub
parent 7778c093b6
commit adb149b869
2 changed files with 35 additions and 9 deletions
+20
View File
@@ -865,6 +865,26 @@ describe("GSAP rules", () => {
expect(finding?.fixHint).toContain("Math.max(0, Math.floor");
});
it("warns on repeat: -1 when an explicit finite composition duration bounds export", async () => {
const html = `
<html><body>
<div data-composition-id="main" data-width="1920" data-height="1080" data-duration="8"></div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.to("#spinner", { rotation: 360, duration: 0.8, repeat: -1, ease: "none" }, 0);
window.__timelines["main"] = tl;
</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "gsap_infinite_repeat");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("warning");
expect(finding?.message).toContain("finite 8s window");
expect(finding?.fixHint).toContain("explicit finite composition");
});
it("does not error on finite repeat values", async () => {
const html = `
<html><body>
+15 -9
View File
@@ -1550,8 +1550,12 @@ export const gsapRules: LintRule<LintContext>[] = [
},
// gsap_infinite_repeat
({ scripts }) => {
({ scripts, rootTag }) => {
const findings: HyperframeLintFinding[] = [];
const declaredDuration = Number.parseFloat(
rootTag ? (readAttr(rootTag.raw, "data-duration") ?? "") : "",
);
const hasFiniteCompositionWindow = Number.isFinite(declaredDuration) && declaredDuration > 0;
// Match repeat: -1 in GSAP tweens or timeline configs
const pattern = /repeat\s*:\s*-1(?!\d)/g;
for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
@@ -1561,14 +1565,16 @@ export const gsapRules: LintRule<LintContext>[] = [
})) {
findings.push({
code: "gsap_infinite_repeat",
severity: "error",
message:
"GSAP tween uses `repeat: -1` (infinite). Infinite repeats break the deterministic " +
"capture engine which seeks to exact frame times. Use a finite repeat count calculated " +
"from the composition duration: `repeat: Math.max(0, Math.floor(duration / cycleDuration) - 1)`.",
fixHint:
"Replace `repeat: -1` with a finite count, e.g. `repeat: Math.max(0, Math.floor(totalDuration / singleCycleDuration) - 1)`. " +
"Use Math.floor (not Math.ceil) so the animation fits, and clamp at zero so a short composition cannot evaluate to -1.",
severity: hasFiniteCompositionWindow ? "warning" : "error",
message: hasFiniteCompositionWindow
? `GSAP tween uses \`repeat: -1\` (infinite), but the composition declares a finite ${declaredDuration}s window. ` +
"HyperFrames clips deterministic seeking and export to that explicit duration."
: "GSAP tween uses `repeat: -1` (infinite) without a finite composition `data-duration`. " +
"The timeline can report an unbounded duration and make render planning fail.",
fixHint: hasFiniteCompositionWindow
? "Keep the explicit finite composition `data-duration`. Use a finite repeat count only when the loop itself must end before the composition does."
: "Add a finite composition `data-duration`, or replace `repeat: -1` with " +
"`repeat: Math.max(0, Math.floor(totalDuration / singleCycleDuration) - 1)`.",
snippet: truncateSnippet(snippet),
});
}