Skip to content

Commit 78679aa

Browse files
committed
fix(parsers): keep the hold for label-placed tweens and re-sync one-line scripts stably
The recast parser leaves a label start unresolved; an unknown start no longer counts as earlier, so the hold stays. The acorn insert now ends the hold on its own line when the next statement shares the timeline line, matching what removal takes, so a second sync changes no bytes.
1 parent 0760b04 commit 78679aa

3 files changed

Lines changed: 34 additions & 8 deletions

File tree

‎packages/parsers/src/gsapConstants.ts‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ export function classifyTweenPropertyGroup(
109109
return undefined;
110110
}
111111

112-
function animationStart(animation: GsapAnimation): number {
112+
function knownStart(animation: GsapAnimation): number | undefined {
113113
if (animation.resolvedStart !== undefined) return animation.resolvedStart;
114-
return typeof animation.position === "number" ? animation.position : 0;
114+
return typeof animation.position === "number" ? animation.position : undefined;
115115
}
116116

117117
/**
@@ -124,19 +124,23 @@ export function positionHoldForAnimation(
124124
animations: readonly GsapAnimation[],
125125
): Record<string, number> | null {
126126
if (!animation.keyframes) return null;
127-
const start = animationStart(animation);
127+
const start = knownStart(animation) ?? 0;
128128
if (!(start > 0.001)) return null;
129129
const first = [...animation.keyframes.keyframes].sort(
130130
(left, right) => left.percentage - right.percentage,
131131
)[0];
132132
if (!first) return null;
133-
const earlier = animations.filter(
134-
(other) =>
133+
// A tween whose start the parser could not resolve (a label, say) is not known to come first.
134+
const earlier = animations.filter((other) => {
135+
const otherStart = knownStart(other);
136+
return (
135137
other !== animation &&
136138
!other.global &&
137139
other.targetSelector === animation.targetSelector &&
138-
animationStart(other) < start - 0.001,
139-
);
140+
otherStart !== undefined &&
141+
otherStart < start - 0.001
142+
);
143+
});
140144
const position: Record<string, number> = {};
141145
for (const [property, value] of Object.entries(first.properties)) {
142146
if (classifyPropertyGroup(property) !== "position" || typeof value !== "number") continue;

‎packages/parsers/src/gsapParser.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,6 +1827,26 @@ describe("keyframe mutations", () => {
18271827
expect(hold!.properties).toEqual({ x: -50, data: "hf-hold" });
18281828
});
18291829

1830+
it.each([
1831+
["recast", syncPositionHoldsBeforeKeyframes],
1832+
["acorn", syncPositionHoldsBeforeKeyframesAcorn],
1833+
])("%s: still holds when the other tween sits at a label after the keyframes", (_, sync) => {
1834+
const script =
1835+
`const tl = gsap.timeline({ paused: true });\n` +
1836+
`tl.addLabel("later", 5);\n` +
1837+
`tl.to("#t", { x: 20, duration: 1 }, "later");\n` +
1838+
`tl.to("#t", { keyframes: { "0%": { x: -50 }, "100%": { x: 60 } }, duration: 1 }, 2);`;
1839+
const hold = parseGsapScript(sync(script)).animations.find((a) => a.method === "set");
1840+
expect(hold!.properties).toEqual({ x: -50, data: "hf-hold" });
1841+
});
1842+
1843+
it("acorn: a re-sync of a one-line script is byte-stable", () => {
1844+
const once = syncPositionHoldsBeforeKeyframesAcorn(
1845+
`const tl = gsap.timeline({ paused: true });tl.to("#t", { keyframes: { "0%": { x: -50 }, "100%": { x: 60 } }, duration: 1 }, 2);`,
1846+
);
1847+
expect(syncPositionHoldsBeforeKeyframesAcorn(once)).toBe(once);
1848+
});
1849+
18301850
it("removes an orphaned hold when its tween is gone", () => {
18311851
const withHold = syncPositionHoldsBeforeKeyframes(posTweenAt(1.2));
18321852
const tweenId = parseGsapScript(withHold).animations.find((a) => a.keyframes)!.id;

‎packages/parsers/src/gsapWriterAcorn.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2440,7 +2440,9 @@ function insertInheritedStateSetInScript(
24402440
const tlDecl = findTimelineDeclarationStatement(parsed.ast, parsed.timelineVar);
24412441
const firstLocated = parsed.located[0];
24422442
if (tlDecl) {
2443-
ms.appendLeft(tlDecl.end, "\n" + code);
2443+
// Own line both sides: a removal takes the trailing newline, so a re-sync is byte-stable.
2444+
const lineEnd = script[tlDecl.end] === "\n" ? "" : "\n";
2445+
ms.appendLeft(tlDecl.end, "\n" + code + lineEnd);
24442446
} else if (firstLocated) {
24452447
const firstCall = firstLocated.call;
24462448
const exprStmt = findEnclosingExpressionStatement(firstCall.ancestors);

0 commit comments

Comments
 (0)