mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
Merge pull request #2099 from heygen-com/07-09-fix_parsers_emit_global_gsap.set_position_holds_before_the_timeline_declaration
fix(parsers): emit global gsap.set position holds before the timeline declaration
This commit is contained in:
@@ -224,6 +224,63 @@ describe("T6c — addAnimationToScript", () => {
|
||||
// Inserted after timeline declaration
|
||||
expect(result.indexOf('tl.to("#hero"')).toBeGreaterThan(result.indexOf("gsap.timeline"));
|
||||
});
|
||||
|
||||
it("inserts a global gsap.set BEFORE the timeline declaration", () => {
|
||||
// A base set emitted after the tween calls is wiped by GSAP's from()-init
|
||||
// revert on the first backwards render (studio soft-reload rebind) — it
|
||||
// must precede the timeline so the from() records it as pre-tween state.
|
||||
const script = `\
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
tl.from("#hero", { scale: 0.9, duration: 0.5 }, 0.2);
|
||||
window.__timelines["t"] = tl;`;
|
||||
const { script: result, id } = addAnimationToScript(script, {
|
||||
targetSelector: "#hero",
|
||||
method: "set",
|
||||
position: 0,
|
||||
properties: { x: 267, y: 20 },
|
||||
global: true,
|
||||
});
|
||||
expect(result).toContain('gsap.set("#hero", { x: 267, y: 20 });');
|
||||
expect(result.indexOf('gsap.set("#hero"')).toBeLessThan(result.indexOf("gsap.timeline"));
|
||||
expect(id).toBeTruthy();
|
||||
// Round-trip: the id resolves back to the inserted set
|
||||
const updated = updateAnimationInScript(result, id, { properties: { x: 300, y: 20 } });
|
||||
expect(updated).toContain("x: 300");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Legacy global-set relocation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("T6c — updateAnimationInScript relocates legacy trailing global set", () => {
|
||||
const LEGACY = `\
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
tl.from("#hero", { scale: 0.9, duration: 0.5 }, 0.2);
|
||||
gsap.set("#hero", { x: 267, y: 20 });
|
||||
window.__timelines["t"] = tl;`;
|
||||
|
||||
it("moves a post-timeline global set above the declaration when updated", () => {
|
||||
const result = updateAnimationInScript(LEGACY, "#hero-set-0-position", {
|
||||
properties: { x: 300, y: 20 },
|
||||
});
|
||||
expect(result).toContain("x: 300");
|
||||
expect(result.indexOf("gsap.set(")).toBeLessThan(result.indexOf("gsap.timeline"));
|
||||
expect(result).toContain('window.__timelines["t"] = tl;');
|
||||
});
|
||||
|
||||
it("leaves an already-hoisted global set in place", () => {
|
||||
const hoisted = `\
|
||||
gsap.set("#hero", { x: 267, y: 20 });
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
tl.from("#hero", { scale: 0.9, duration: 0.5 }, 0.2);
|
||||
window.__timelines["t"] = tl;`;
|
||||
const result = updateAnimationInScript(hoisted, "#hero-set-0-position", {
|
||||
properties: { x: 300, y: 20 },
|
||||
});
|
||||
expect(result).toContain("x: 300");
|
||||
expect(result.indexOf("gsap.set(")).toBeLessThan(result.indexOf("gsap.timeline"));
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -301,6 +301,25 @@ function findInsertionPoint(parsed: ParsedGsapAcornForWrite): number | null {
|
||||
return tlDecl?.end ?? (parsed.ast.end as number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Line-start offset of the timeline declaration — where a global `gsap.set`
|
||||
* must be inserted. A base set emitted AFTER the tween calls gets wiped on the
|
||||
* next seek-driven rebuild: when a `from()` tween on the same target lazily
|
||||
* initializes during a backwards render (the studio rebind's `progress(0.0001)`
|
||||
* after a soft reload), GSAP reverts its internal isFromStart set, removing the
|
||||
* whole inline `transform` — and the base set's x/y with it. Emitted BEFORE the
|
||||
* timeline, the set is part of the pre-tween state the from() records, so every
|
||||
* revert restores it instead. Returns null when no declaration exists.
|
||||
*/
|
||||
function findGlobalSetInsertionPoint(
|
||||
parsed: ParsedGsapAcornForWrite,
|
||||
script: string,
|
||||
): number | null {
|
||||
const tlDecl = findTimelineDeclarationStatement(parsed.ast, parsed.timelineVar);
|
||||
if (!tlDecl) return null;
|
||||
return script.lastIndexOf("\n", tlDecl.start) + 1;
|
||||
}
|
||||
|
||||
// ── Public write API ─────────────────────────────────────────────────────────
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
@@ -370,6 +389,26 @@ export function updateAnimationInScript(
|
||||
overwritePosition(ms, call, updates.position);
|
||||
}
|
||||
|
||||
// Heal legacy scripts: a global `gsap.set` sitting after the timeline
|
||||
// declaration is subject to the from()-init revert wipe (see
|
||||
// findGlobalSetInsertionPoint) — relocate it above the declaration whenever
|
||||
// it's touched, so the next nudge fixes files written before the reorder.
|
||||
if (target.animation.method === "set" && target.animation.global) {
|
||||
const globalSetPoint = findGlobalSetInsertionPoint(parsed, script);
|
||||
const exprStmt = findEnclosingExpressionStatement(call.ancestors);
|
||||
if (globalSetPoint !== null && exprStmt && exprStmt.start > globalSetPoint) {
|
||||
const lineStart = script.lastIndexOf("\n", exprStmt.start) + 1;
|
||||
const moveStart = /^\s*$/.test(script.slice(lineStart, exprStmt.start))
|
||||
? lineStart
|
||||
: exprStmt.start;
|
||||
const moveEnd =
|
||||
exprStmt.end < script.length && script[exprStmt.end] === "\n"
|
||||
? exprStmt.end + 1
|
||||
: exprStmt.end;
|
||||
ms.move(moveStart, moveEnd, globalSetPoint);
|
||||
}
|
||||
}
|
||||
|
||||
return ms.toString();
|
||||
}
|
||||
|
||||
@@ -455,16 +494,38 @@ export function addAnimationToScript(
|
||||
const parsed = parseGsapScriptAcornForWrite(script);
|
||||
if (!parsed) return { script, id: "" };
|
||||
|
||||
const insertionPoint = findInsertionPoint(parsed);
|
||||
if (insertionPoint === null) return { script, id: "" };
|
||||
|
||||
const ms = new MagicString(script);
|
||||
const statementCode = buildTweenStatementCode(parsed.timelineVar, animation);
|
||||
ms.appendLeft(insertionPoint, "\n" + statementCode);
|
||||
const globalSetPoint =
|
||||
animation.method === "set" && animation.global
|
||||
? findGlobalSetInsertionPoint(parsed, script)
|
||||
: null;
|
||||
if (globalSetPoint !== null) {
|
||||
ms.appendLeft(globalSetPoint, statementCode + "\n");
|
||||
} else {
|
||||
const insertionPoint = findInsertionPoint(parsed);
|
||||
if (insertionPoint === null) return { script, id: "" };
|
||||
ms.appendLeft(insertionPoint, "\n" + statementCode);
|
||||
}
|
||||
|
||||
const result = ms.toString();
|
||||
const reParsed = parseGsapScriptAcornForWrite(result);
|
||||
const newId = reParsed?.located[reParsed.located.length - 1]?.id ?? "";
|
||||
// IDs are content-based, not positional — the new statement isn't necessarily
|
||||
// the last located entry (a global set is inserted before the timeline).
|
||||
// Diff against the pre-insert ids, counting duplicates.
|
||||
const oldIdCounts = new Map<string, number>();
|
||||
for (const entry of parsed.located) {
|
||||
oldIdCounts.set(entry.id, (oldIdCounts.get(entry.id) ?? 0) + 1);
|
||||
}
|
||||
let newId = "";
|
||||
for (const entry of reParsed?.located ?? []) {
|
||||
const remaining = oldIdCounts.get(entry.id) ?? 0;
|
||||
if (remaining === 0) {
|
||||
newId = entry.id;
|
||||
break;
|
||||
}
|
||||
oldIdCounts.set(entry.id, remaining - 1);
|
||||
}
|
||||
return { script: result, id: newId };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user