diff --git a/packages/lint/src/rules/composition.test.ts b/packages/lint/src/rules/composition.test.ts index 68881f4d0..dbf6eecf7 100644 --- a/packages/lint/src/rules/composition.test.ts +++ b/packages/lint/src/rules/composition.test.ts @@ -315,6 +315,50 @@ describe("composition rules", () => { expect(finding).toBeUndefined(); }); + it("does not flag one sub-composition mounted repeatedly with per-instance values", async () => { + // Regression: sub-compositions.md "Per-Instance Variables" documents + // mounting one source several times with different data-variable-values. + // That necessarily repeats the id, and the runtime rewrites repeated + // mounts to `id__hf1`/`id__hf2` so they coexist. Flagging it made the + // documented pattern an error with no correct way to satisfy it. + const html = ` + + +
+
+
+
+
+ +`; + + const result = await lintHyperframeHtml(html); + expect(result.findings.find((f) => f.code === "duplicate_composition_id")).toBeUndefined(); + }); + + it("still flags a real collision between a root and a non-mount element", async () => { + // The guard that keeps the exemption honest: skipping mounts must not + // blind the rule to the meta-versus-root collision it exists for, even + // when a legitimately repeated mount is present in the same file. + const html = ` + + + + + +
+
+
+
+ +`; + + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "duplicate_composition_id"); + expect(finding).toBeDefined(); + expect(finding?.message).toContain("main"); + }); + it("ignores composition ids inside inert template content", async () => { const html = ` diff --git a/packages/lint/src/rules/composition.ts b/packages/lint/src/rules/composition.ts index 021826864..c2902f4c7 100644 --- a/packages/lint/src/rules/composition.ts +++ b/packages/lint/src/rules/composition.ts @@ -262,6 +262,16 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding const tagsByCompositionId = new Map(); for (const tag of tags) { if (isInsideInertTemplate(tag, tags)) continue; + // A `data-composition-src` element is a MOUNT of a sub-composition, not a + // composition root, and sub-compositions.md documents mounting one source + // repeatedly with different `data-variable-values` to get per-instance + // variations. Those mounts legitimately share an id: the runtime rewrites + // repeated ones to `id__hf1`, `id__hf2` so they coexist. Counting them + // here made the documented pattern an error with no correct way to + // satisfy it. The collision this rule exists for -- a tag carrying + // the root's id, per its own fixHint -- is unaffected, since that tag has + // no `data-composition-src`. + if (readAttr(tag.raw, "data-composition-src")) continue; const compositionId = readDecodedAttr(tag.raw, "data-composition-id"); if (!compositionId || compositionId.trim().length === 0) continue;