Skip to content

Commit e915957

Browse files
committed
feat(core): add gsap_timeline_not_registered lint rule
Warns when a composition creates gsap.timeline() but never registers it in window.__timelines. Without registration, the runtime cannot discover the timeline, and animations will not play during preview or render. Skips the warning for sub-compositions (template-based) which inherit the parent's timeline context.
1 parent dcdee10 commit e915957

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

‎packages/core/src/lint/rules/gsap.test.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,4 +865,57 @@ describe("GSAP rules", () => {
865865
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
866866
expect(finding).toBeUndefined();
867867
});
868+
869+
it("warns when gsap.timeline is created but not registered in __timelines", async () => {
870+
const html = `
871+
<html><body>
872+
<div data-composition-id="root" data-width="1920" data-height="1080">
873+
<div id="box">Hello</div>
874+
</div>
875+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
876+
<script>
877+
const tl = gsap.timeline({ paused: true });
878+
tl.to("#box", { opacity: 0.5, duration: 2 });
879+
</script>
880+
</body></html>`;
881+
const result = await lintHyperframeHtml(html);
882+
const finding = result.findings.find((f) => f.code === "gsap_timeline_not_registered");
883+
expect(finding).toBeDefined();
884+
expect(finding?.severity).toBe("warning");
885+
});
886+
887+
it("does NOT warn when timeline is registered in __timelines", async () => {
888+
const html = `
889+
<html><body>
890+
<div data-composition-id="root" data-width="1920" data-height="1080">
891+
<div id="box">Hello</div>
892+
</div>
893+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
894+
<script>
895+
window.__timelines = window.__timelines || {};
896+
const tl = gsap.timeline({ paused: true });
897+
tl.to("#box", { opacity: 0.5, duration: 2 });
898+
window.__timelines["root"] = tl;
899+
</script>
900+
</body></html>`;
901+
const result = await lintHyperframeHtml(html);
902+
const finding = result.findings.find((f) => f.code === "gsap_timeline_not_registered");
903+
expect(finding).toBeUndefined();
904+
});
905+
906+
it("does NOT warn for sub-compositions (template-based)", async () => {
907+
const html = `
908+
<template>
909+
<div data-composition-id="sub" data-width="1920" data-height="1080">
910+
<div id="box">Hello</div>
911+
</div>
912+
<script>
913+
const tl = gsap.timeline({ paused: true });
914+
tl.to("#box", { opacity: 0.5, duration: 2 });
915+
</script>
916+
</template>`;
917+
const result = await lintHyperframeHtml(html);
918+
const finding = result.findings.find((f) => f.code === "gsap_timeline_not_registered");
919+
expect(finding).toBeUndefined();
920+
});
868921
});

‎packages/core/src/lint/rules/gsap.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,33 @@ export const gsapRules: LintRule<LintContext>[] = [
785785
return findings;
786786
},
787787

788+
// gsap_timeline_not_registered
789+
({ scripts, rawSource, options }) => {
790+
const findings: HyperframeLintFinding[] = [];
791+
const canInheritFromHost =
792+
options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template");
793+
794+
for (const script of scripts) {
795+
const content = script.content;
796+
if (!/gsap\.timeline/.test(content)) continue;
797+
const hasRegistration = WINDOW_TIMELINE_ASSIGN_PATTERN.test(content);
798+
if (hasRegistration || canInheritFromHost) continue;
799+
findings.push({
800+
code: "gsap_timeline_not_registered",
801+
severity: "warning",
802+
message:
803+
"GSAP timeline is created but never registered in window.__timelines. " +
804+
"The runtime discovers timelines from this registry — without registration, " +
805+
"animations will not play during preview or render.",
806+
fixHint:
807+
"Add `window.__timelines = window.__timelines || {};` and " +
808+
'`window.__timelines["root"] = tl;` after creating the timeline (use the ' +
809+
"composition's data-composition-id as the key).",
810+
});
811+
}
812+
return findings;
813+
},
814+
788815
// gsap_from_opacity_noop — CSS opacity:0 + gsap.from({opacity:0}) = invisible forever
789816
async ({ styles, scripts, tags }) => {
790817
const findings: HyperframeLintFinding[] = [];

0 commit comments

Comments
 (0)