mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(parsers): never emit duplicate vars keys in either writer
A parsed timeline set carries immediateRender in extras; the recast statement builder ALSO pushed the flag unconditionally, so every split/re-add of a set wrote 'immediateRender: true, immediateRender: true' into the file, doubling on each pass. Both writer twins (recast + acorn) now emit each vars key exactly once, properties winning over extras, and reconcileEditableProps skips newProps keys it already preserved.
This commit is contained in:
@@ -3026,3 +3026,24 @@ describe("single position write per element (consolidation)", () => {
|
||||
expect(parseGsapScript(out).animations.some((a) => "opacity" in a.properties)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("recast writer never doubles vars keys", () => {
|
||||
// Regression: buildTweenStatementCode pushed `immediateRender: true` for
|
||||
// every timeline set AND appended extras — a parsed set carries the flag in
|
||||
// extras, so splitting a mixed set emitted
|
||||
// `immediateRender: true, immediateRender: true` into the file.
|
||||
const src = `window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.set("#a", { z: 0, rotationX: 5, immediateRender: true, scale: 1 }, 0);
|
||||
window.__timelines["main"] = tl;`;
|
||||
|
||||
it("split of a mixed set emits the flag once per group", () => {
|
||||
const id = parseGsapScript(src).animations[0]!.id;
|
||||
const { script: out } = splitIntoPropertyGroups(src, id);
|
||||
const setLines = out.split("\n").filter((l) => l.includes("tl.set("));
|
||||
expect(setLines.length).toBeGreaterThan(1);
|
||||
for (const line of setLines) {
|
||||
expect(line.match(/immediateRender/g) ?? []).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1387,13 +1387,25 @@ function buildTweenStatementCode(timelineVar: string, anim: Omit<GsapAnimation,
|
||||
if (anim.method !== "set" && anim.duration !== undefined) props.duration = anim.duration;
|
||||
if (anim.ease) props.ease = anim.ease;
|
||||
const entries = Object.entries(props).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
||||
const emitted = new Set(Object.keys(props));
|
||||
// immediateRender forces GSAP to apply the set when added to the timeline,
|
||||
// not on the first seek — without it, tl.set at position 0 on a paused
|
||||
// timeline is invisible until the playhead moves past 0. A base `gsap.set`
|
||||
// already runs immediately, so it doesn't need (or get) the flag.
|
||||
if (anim.method === "set" && !anim.global) entries.push("immediateRender: true");
|
||||
// already runs immediately, so it doesn't need (or get) the flag. A parsed
|
||||
// set carries the flag in extras — never emit the same key twice.
|
||||
if (
|
||||
anim.method === "set" &&
|
||||
!anim.global &&
|
||||
!emitted.has("immediateRender") &&
|
||||
!(anim.extras && "immediateRender" in anim.extras)
|
||||
) {
|
||||
entries.push("immediateRender: true");
|
||||
emitted.add("immediateRender");
|
||||
}
|
||||
if (anim.extras) {
|
||||
for (const [k, v] of Object.entries(anim.extras)) {
|
||||
if (emitted.has(k)) continue;
|
||||
emitted.add(k);
|
||||
entries.push(`${safeKey(k)}: ${valueToCode(v as number | string)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,3 +101,32 @@ window.__timelines["scene"] = gsap.timeline({ paused: true });`;
|
||||
expect(parseGsapScriptAcorn(out).animations).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no duplicate vars keys on rewrite", () => {
|
||||
const setSrc = `window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.set("#a", { z: 0, rotationX: 5, immediateRender: true }, 0);
|
||||
window.__timelines["main"] = tl;`;
|
||||
|
||||
it("update with immediateRender riding in newProps emits the key once", () => {
|
||||
const id = parseGsapScriptAcorn(setSrc).animations[0]!.id;
|
||||
// Studio paths that build props off live tween vars carry the flag along.
|
||||
const out = updateAnimationInScript(setSrc, id, {
|
||||
properties: { z: 0, rotationX: 9, immediateRender: "__raw:true" },
|
||||
});
|
||||
expect(out).toContain("rotationX: 9");
|
||||
expect(out.match(/immediateRender/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("addAnimationToScript dedupes a key present in both properties and extras", () => {
|
||||
const { script: out } = addAnimationToScript(setSrc, {
|
||||
targetSelector: "#b",
|
||||
method: "set",
|
||||
position: 0,
|
||||
properties: { scale: 1, immediateRender: "__raw:true" },
|
||||
extras: { immediateRender: "__raw:true" },
|
||||
});
|
||||
const setB = out.split("\n").find((l) => l.includes('"#b"'));
|
||||
expect(setB?.match(/immediateRender/g)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,7 +59,9 @@ function buildTweenStatementCode(timelineVar: string, anim: Omit<GsapAnimation,
|
||||
const entries = Object.entries(props).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`);
|
||||
if (anim.extras) {
|
||||
for (const [k, v] of Object.entries(anim.extras)) {
|
||||
entries.push(`${safeKey(k)}: ${valueToCode(v)}`);
|
||||
// A key carried by both properties and extras (a set's parsed
|
||||
// `immediateRender: true`) must emit once — properties win.
|
||||
if (!(k in props)) entries.push(`${safeKey(k)}: ${valueToCode(v)}`);
|
||||
}
|
||||
}
|
||||
const objCode = `{ ${entries.join(", ")} }`;
|
||||
@@ -263,10 +265,16 @@ function reconcileEditableProps(
|
||||
const overrides = nonEditableOverrides ?? {};
|
||||
const { entries, keys } = preservedEntries(objNode, source, isEditableVarKey, overrides);
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
if (!keys.has(key)) entries.push(`${safeKey(key)}: ${valueToCode(value)}`);
|
||||
if (!keys.has(key)) {
|
||||
keys.add(key);
|
||||
entries.push(`${safeKey(key)}: ${valueToCode(value)}`);
|
||||
}
|
||||
}
|
||||
for (const [key, value] of Object.entries(newProps)) {
|
||||
entries.push(`${safeKey(key)}: ${valueToCode(value)}`);
|
||||
// A non-editable key riding along in newProps (immediateRender read off
|
||||
// live tween vars) is already preserved above — emitting it again writes
|
||||
// `immediateRender: true, immediateRender: true` into the file.
|
||||
if (!keys.has(key)) entries.push(`${safeKey(key)}: ${valueToCode(value)}`);
|
||||
}
|
||||
ms.overwrite(objNode.start, objNode.end, `{ ${entries.join(", ")} }`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user