mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(core): recognize dot-property timeline registration (#1138)
This commit is contained in:
@@ -71,6 +71,24 @@ describe("core rules", () => {
|
||||
expect(finding?.message).toContain("without initializing");
|
||||
});
|
||||
|
||||
it("reports error when dot timeline registry is assigned without initializing", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<div id="stage"></div>
|
||||
</div>
|
||||
<script>
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.to("#stage", { opacity: 1, duration: 1 }, 0);
|
||||
window.__timelines.c1 = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "timeline_registry_missing_init");
|
||||
expect(finding).toBeDefined();
|
||||
expect(finding?.severity).toBe("error");
|
||||
});
|
||||
|
||||
it("does not flag timeline assignment when init guard is present", async () => {
|
||||
const validComposition = `
|
||||
<html>
|
||||
@@ -92,6 +110,54 @@ describe("core rules", () => {
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("timeline_id_mismatch", () => {
|
||||
it("accepts dot timeline registration", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="launch" data-width="1920" data-height="1080"></div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines.launch = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "timeline_id_mismatch");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reports mismatched dot timeline registration", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="launch" data-width="1920" data-height="1080"></div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines.intro = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "timeline_id_mismatch");
|
||||
expect(finding).toBeDefined();
|
||||
expect(finding?.message).toContain('Timeline registered as "intro"');
|
||||
});
|
||||
|
||||
it("accepts bracket timeline registration for hyphenated ids", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="product-launch" data-width="1920" data-height="1080"></div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines["product-launch"] = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "timeline_id_mismatch");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("warns when a timeline-visible element has no stable id for Studio editing", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
readAttr,
|
||||
truncateSnippet,
|
||||
extractCompositionIdsFromCss,
|
||||
extractTimelineRegistryKeys,
|
||||
getInlineScriptSyntaxError,
|
||||
TIMELINE_REGISTRY_INIT_PATTERN,
|
||||
TIMELINE_REGISTRY_ASSIGN_PATTERN,
|
||||
@@ -118,13 +119,12 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
const htmlCompIds = new Set<string>();
|
||||
const timelineRegKeys = new Set<string>();
|
||||
const compIdRe = /data-composition-id\s*=\s*["']([^"']+)["']/gi;
|
||||
const tlKeyRe = /window\.__timelines\[\s*["']([^"']+)["']\s*\]/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = compIdRe.exec(source)) !== null) {
|
||||
if (m[1]) htmlCompIds.add(m[1]);
|
||||
}
|
||||
while ((m = tlKeyRe.exec(source)) !== null) {
|
||||
if (m[1]) timelineRegKeys.add(m[1]);
|
||||
for (const key of extractTimelineRegistryKeys(source)) {
|
||||
timelineRegKeys.add(key);
|
||||
}
|
||||
for (const key of timelineRegKeys) {
|
||||
if (!htmlCompIds.has(key)) {
|
||||
|
||||
@@ -903,6 +903,25 @@ describe("GSAP rules", () => {
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does NOT warn when timeline is registered with dot property syntax", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="root" data-width="1920" data-height="1080">
|
||||
<div id="box">Hello</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.to("#box", { opacity: 0.5, duration: 2 });
|
||||
window.__timelines.root = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "gsap_timeline_not_registered");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does NOT warn for sub-compositions (template-based)", async () => {
|
||||
const html = `
|
||||
<template>
|
||||
|
||||
@@ -125,7 +125,7 @@ function countClassUsage(tags: OpenTag[]): Map<string, number> {
|
||||
|
||||
function readRegisteredTimelineCompositionId(script: string): string | null {
|
||||
const match = script.match(WINDOW_TIMELINE_ASSIGN_PATTERN);
|
||||
return match?.[1] || null;
|
||||
return match?.[1] || match?.[2] || null;
|
||||
}
|
||||
|
||||
/** Strip a `__raw:` prefix the parser adds to unresolvable values. */
|
||||
|
||||
@@ -21,11 +21,15 @@ export const SCRIPT_BLOCK_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
||||
const COMPOSITION_ID_IN_CSS_PATTERN = /\[data-composition-id=["']([^"']+)["']\]/g;
|
||||
export const TIMELINE_REGISTRY_INIT_PATTERN =
|
||||
/window\.__timelines\s*=\s*window\.__timelines\s*\|\|\s*\{\}|window\.__timelines\s*=\s*\{\}|window\.__timelines\s*\?\?=\s*\{\}/i;
|
||||
export const TIMELINE_REGISTRY_ASSIGN_PATTERN = /window\.__timelines\[[^\]]+\]\s*=/i;
|
||||
export const TIMELINE_REGISTRY_ASSIGN_PATTERN =
|
||||
/window\.__timelines(?:\[[^\]]+\]|\.[A-Za-z_$][\w$]*)\s*=/i;
|
||||
export const WINDOW_TIMELINE_ASSIGN_PATTERN =
|
||||
/window\.__timelines\[\s*["']([^"']+)["']\s*\]\s*=\s*([A-Za-z_$][\w$]*)/i;
|
||||
/window\.__timelines(?:\[\s*["']([^"']+)["']\s*\]|\.\s*([A-Za-z_$][\w$]*))\s*=\s*([A-Za-z_$][\w$]*)/i;
|
||||
export const INVALID_SCRIPT_CLOSE_PATTERN = /<script[^>]*>[\s\S]*?<\s*\/\s*script(?!>)/i;
|
||||
|
||||
const TIMELINE_REGISTRY_KEY_PATTERN =
|
||||
/window\.__timelines(?:\[\s*["']([^"']+)["']\s*\]|\.\s*([A-Za-z_$][\w$]*))\s*=/g;
|
||||
|
||||
export function extractOpenTags(source: string): OpenTag[] {
|
||||
const tags: OpenTag[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
@@ -141,6 +145,20 @@ export function extractCompositionIdsFromCss(css: string): string[] {
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
export function extractTimelineRegistryKeys(source: string): string[] {
|
||||
const keys = new Set<string>();
|
||||
let match: RegExpExecArray | null;
|
||||
const pattern = new RegExp(
|
||||
TIMELINE_REGISTRY_KEY_PATTERN.source,
|
||||
TIMELINE_REGISTRY_KEY_PATTERN.flags,
|
||||
);
|
||||
while ((match = pattern.exec(source)) !== null) {
|
||||
const key = match[1] ?? match[2];
|
||||
if (key) keys.add(key);
|
||||
}
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
export function getInlineScriptSyntaxError(source: string): string | null {
|
||||
if (!source.trim()) return null;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user