mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(lint): check timeline registration before explicit font loads (#5363)
This commit is contained in:
@@ -138,6 +138,79 @@ describe("GSAP rules", () => {
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["bracket / load", 'window.__timelines["c1"]', 'document.fonts.load("16px Inter")'],
|
||||
["dot / ready", "window.__timelines.c1", "document.fonts.ready"],
|
||||
["dot / load", "window.__timelines.c1", 'document.fonts.load("16px Inter")'],
|
||||
["spaced dot / load", "window . __timelines . c1", 'document . fonts . load("16px Inter")'],
|
||||
])("flags early registration with %s", async (_label, registry, fontWait) => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080"><h1 id="title">Text</h1></div>
|
||||
<script>
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
${registry} = tl;
|
||||
${fontWait}.then(() => {
|
||||
tl.to("#title", { x: 20, duration: 1 });
|
||||
});
|
||||
</script>
|
||||
</body></html>`;
|
||||
|
||||
const result = await lintHyperframeHtml(html);
|
||||
expect(
|
||||
result.findings.filter(
|
||||
(finding) => finding.code === "gsap_timeline_registered_before_async_build",
|
||||
),
|
||||
).toEqual([expect.objectContaining({ severity: "error" })]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['window.__timelines["c1"]', "document.fonts.ready"],
|
||||
['window.__timelines["c1"]', 'document.fonts.load("16px Inter")'],
|
||||
["window.__timelines.c1", "document.fonts.ready"],
|
||||
["window.__timelines.c1", 'document.fonts.load("16px Inter")'],
|
||||
])("accepts %s registered after %s builds the timeline", async (registry, fontWait) => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080"><h1 id="title">Text</h1></div>
|
||||
<script>
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const existing = ${registry};
|
||||
${fontWait}.then(() => {
|
||||
tl.to("#title", { x: 20, duration: 1 });
|
||||
${registry} = tl;
|
||||
});
|
||||
</script>
|
||||
</body></html>`;
|
||||
|
||||
const result = await lintHyperframeHtml(html);
|
||||
expect(
|
||||
result.findings.find(
|
||||
(finding) => finding.code === "gsap_timeline_registered_before_async_build",
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("accepts a fonts.load callback that adds no deferred animation", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080"><h1 id="title">Text</h1></div>
|
||||
<script>
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.to("#title", { x: 20, duration: 1 });
|
||||
window.__timelines.c1 = tl;
|
||||
document.fonts.load("16px Inter").then(() => console.log("fonts loaded"));
|
||||
</script>
|
||||
</body></html>`;
|
||||
|
||||
const result = await lintHyperframeHtml(html);
|
||||
expect(
|
||||
result.findings.find(
|
||||
(finding) => finding.code === "gsap_timeline_registered_before_async_build",
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does NOT error when GSAP animates opacity on a clip element (by id)", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
|
||||
@@ -1671,7 +1671,7 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
},
|
||||
|
||||
// gsap_timeline_registered_before_async_build — registering window.__timelines[id]
|
||||
// BEFORE the timeline is built inside document.fonts.ready (or any async callback)
|
||||
// BEFORE the timeline is built inside a font-ready callback
|
||||
// leaves an EMPTY timeline registered. The runtime's sub-composition readiness gate
|
||||
// treats "key present" as "ready" and nests the child ONCE, while still empty — so the
|
||||
// animation never renders when this composition is mounted as a sub-composition.
|
||||
@@ -1680,14 +1680,16 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
for (const script of scripts) {
|
||||
const content = stripJsComments(script.content);
|
||||
const regIdx = content.search(/window\s*\.\s*__timelines\s*\[/);
|
||||
const regIdx = content.search(
|
||||
/window\s*\.\s*__timelines\s*(?:\[[^\]]+\]|\.\s*[A-Za-z_$][\w$]*)\s*=\s*(?!=)/,
|
||||
);
|
||||
if (regIdx < 0) continue;
|
||||
const fontsReadyIdx = content.search(/document\s*\.\s*fonts\s*\.\s*ready/);
|
||||
const fontsReadyIdx = content.search(/document\s*\.\s*fonts\s*\.\s*(?:ready\b|load\s*\()/);
|
||||
if (fontsReadyIdx < 0) continue;
|
||||
// Registering after the async boundary is the correct pattern — skip it.
|
||||
if (regIdx >= fontsReadyIdx) continue;
|
||||
// Confirm the build is actually deferred past the boundary (a tween/build call
|
||||
// appears after document.fonts.ready), i.e. the registered timeline starts empty.
|
||||
// appears after the font wait), i.e. the registered timeline starts empty.
|
||||
const tail = content.slice(fontsReadyIdx);
|
||||
if (!/\.(?:to|from|fromTo)\s*\(|buildEffect\s*\(/.test(tail)) continue;
|
||||
findings.push({
|
||||
@@ -1695,12 +1697,12 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
severity: "error",
|
||||
message:
|
||||
"window.__timelines is assigned BEFORE the timeline is built inside " +
|
||||
"document.fonts.ready. An empty timeline registered early gets nested empty " +
|
||||
"document.fonts.ready or document.fonts.load(). An empty timeline registered early gets nested empty " +
|
||||
"when this composition is used as a sub-composition (the readiness gate treats " +
|
||||
'"key present" as "ready" and never re-nests), so the animation renders blank.',
|
||||
fixHint:
|
||||
"Move the `window.__timelines[id] = tl;` assignment to the END of the " +
|
||||
"document.fonts.ready callback, after the tweens are added. Optionally call " +
|
||||
"font-ready callback, after the tweens are added. Optionally call " +
|
||||
"window.__hfForceTimelineRebind() right after, to re-nest the populated timeline.",
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user