Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/core/src/lint/rules/composition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,44 @@ describe("composition rules", () => {
});
});

describe("missing_data_no_timeline", () => {
it("warns when root has no timeline registration and no data-no-timeline", async () => {
const html = `<!DOCTYPE html><html><body>
<div data-composition-id="c1" data-width="320" data-height="180" data-duration="5"></div>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "missing_data_no_timeline");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("warning");
});

it("does not warn when data-no-timeline is present (boolean form)", async () => {
const html = `<!DOCTYPE html><html><body>
<div data-composition-id="c1" data-no-timeline data-width="320" data-height="180" data-duration="5"></div>
</body></html>`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toBeUndefined();
});

it("does not warn when a script registers window.__timelines[id]", async () => {
const html = `<!DOCTYPE html><html><body>
<div data-composition-id="c1" data-width="320" data-height="180" data-duration="5"></div>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["c1"] = gsap.timeline({ paused: true });
</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toBeUndefined();
});

it("does not warn when there is no root composition-id", async () => {
const html = `<!DOCTYPE html><html><body><p>hello</p></body></html>`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "missing_data_no_timeline")).toBeUndefined();
});
});

describe("root_composition_missing_data_duration (removed)", () => {
// The rule was a static proxy for the runtime's loop-inflation Infinity
// emission, but lint cannot observe GSAP timeline duration statically and
Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/lint/rules/composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,31 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
return findings;
},

// missing_data_no_timeline
// The producer polls window.__timelines[id] with a 45-second timeout waiting
// for GSAP timeline registration. Compositions that never call
// window.__timelines[id] = tl stall for 45 s every render. Adding
// data-no-timeline to the root element tells the producer to skip the poll.
({ rootTag, rootCompositionId, scripts }) => {
if (!rootCompositionId || !rootTag) return [];
if (/\bdata-no-timeline\b/.test(rootTag.raw)) return [];
const registersTimeline = scripts.some((s) => s.content.includes("window.__timelines["));
if (registersTimeline) return [];
return [
{
code: "missing_data_no_timeline",
severity: "warning",
message:
"This composition has no `window.__timelines` registration but is missing `data-no-timeline`. " +
"The producer polls for timeline registration for up to 45 seconds before timing out, " +
"adding 45 s to every render.",
fixHint:
'Add `data-no-timeline` to the root element to skip the poll: `<div data-composition-id="..." data-no-timeline ...>`.',
snippet: truncateSnippet(rootTag.raw),
},
];
},

// requestanimationframe_in_composition
({ scripts }) => {
const findings: HyperframeLintFinding[] = [];
Expand Down
Loading