diff --git a/packages/core/src/lint/rules/core.test.ts b/packages/core/src/lint/rules/core.test.ts
index cd5525648..1a686271b 100644
--- a/packages/core/src/lint/rules/core.test.ts
+++ b/packages/core/src/lint/rules/core.test.ts
@@ -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 = `
+
+
+
+`;
+ 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 = `
@@ -92,6 +110,54 @@ describe("core rules", () => {
expect(finding).toBeUndefined();
});
+ describe("timeline_id_mismatch", () => {
+ it("accepts dot timeline registration", async () => {
+ const 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 = `
+
+
+
+`;
+ 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 = `
+
+
+
+`;
+ 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 = `
diff --git a/packages/core/src/lint/rules/core.ts b/packages/core/src/lint/rules/core.ts
index 273eb340b..f244bfc5e 100644
--- a/packages/core/src/lint/rules/core.ts
+++ b/packages/core/src/lint/rules/core.ts
@@ -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();
const timelineRegKeys = new Set();
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)) {
diff --git a/packages/core/src/lint/rules/gsap.test.ts b/packages/core/src/lint/rules/gsap.test.ts
index cae448ead..6ef3c57ea 100644
--- a/packages/core/src/lint/rules/gsap.test.ts
+++ b/packages/core/src/lint/rules/gsap.test.ts
@@ -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 = `
+
+
+
+
+`;
+ 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 = `
diff --git a/packages/core/src/lint/rules/gsap.ts b/packages/core/src/lint/rules/gsap.ts
index 43745f619..c1f8dc053 100644
--- a/packages/core/src/lint/rules/gsap.ts
+++ b/packages/core/src/lint/rules/gsap.ts
@@ -125,7 +125,7 @@ function countClassUsage(tags: OpenTag[]): Map {
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. */
diff --git a/packages/core/src/lint/utils.ts b/packages/core/src/lint/utils.ts
index b2ee9cdde..fbb2c7452 100644
--- a/packages/core/src/lint/utils.ts
+++ b/packages/core/src/lint/utils.ts
@@ -21,11 +21,15 @@ export const SCRIPT_BLOCK_PATTERN = /