From c52ac2344523a66c9d95b90d62e2c7bf2f52b17a Mon Sep 17 00:00:00 2001 From: Bortlesboat <169967362+Bortlesboat@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:57:32 -0400 Subject: [PATCH] fix(lint): recognize supported timeline registration forms --- packages/lint/src/rules/composition.test.ts | 27 +++++++++++++++++++-- packages/lint/src/rules/composition.ts | 12 ++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/lint/src/rules/composition.test.ts b/packages/lint/src/rules/composition.test.ts index 91452e58ac..62777ab4d5 100644 --- a/packages/lint/src/rules/composition.test.ts +++ b/packages/lint/src/rules/composition.test.ts @@ -912,18 +912,41 @@ describe("composition rules", () => { expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toBeUndefined(); }); - it("does not warn when a script registers window.__timelines[id]", async () => { + it.each([ + 'window.__timelines["c1"] = gsap.timeline({ paused: true });', + "window.__timelines.c1 = gsap.timeline({ paused: true });", + "window.__timelines = { c1: gsap.timeline({ paused: true }) };", + 'window.__timelines = { "c1": gsap.timeline({ paused: true }) };', + 'const spec = { id: "c1" }; window.__timelines[spec.id] = gsap.timeline({ paused: true });', + 'window.__timelines["c1"] ??= gsap.timeline({ paused: true });', + 'window.__timelines["c1"] ||= gsap.timeline({ paused: true });', + 'const ids = ["c1"]; window.__timelines[ids[0]] = gsap.timeline({ paused: true });', + ])("does not warn when a script registers a timeline: %s", async (registration) => { const html = `
`; const result = await lintHyperframeHtml(html); expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toBeUndefined(); }); + it.each([ + "window.__timelines = {};", + '// window.__timelines["c1"] = gsap.timeline({ paused: true });', + ])("still warns when a script does not register a timeline: %s", async (script) => { + const html = ` + + +`; + const result = await lintHyperframeHtml(html); + expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toMatchObject({ + severity: "warning", + }); + }); + it("does not warn when there is no root composition-id", async () => { const html = `hello
`; const result = await lintHyperframeHtml(html); diff --git a/packages/lint/src/rules/composition.ts b/packages/lint/src/rules/composition.ts index 46b3cc084a..c48e1904c5 100644 --- a/packages/lint/src/rules/composition.ts +++ b/packages/lint/src/rules/composition.ts @@ -8,6 +8,8 @@ import { stripJsComments, stripJsCode, truncateSnippet, + TIMELINE_REGISTRY_ASSIGN_PATTERN, + TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN, WINDOW_TIMELINE_ASSIGN_PATTERN, } from "../utils"; import { COMPOSITION_VARIABLE_TYPES, isSafeMediaUrl } from "@hyperframes/parsers/composition"; @@ -631,7 +633,15 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding // Can't scan external script files for timeline registration; skip to avoid // false positives on compositions that register via a bundled JS file. if (/