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
47 changes: 47 additions & 0 deletions packages/lint/src/rules/composition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,53 @@ describe("composition rules", () => {
});
});

describe("duplicate_composition_id", () => {
it("flags a meta tag and root div sharing the same data-composition-id", async () => {
const html = `<!DOCTYPE html>
<html>
<head>
<meta name="composition-id" data-composition-id="x">
</head>
<body>
<div data-composition-id="x" data-width="1920" data-height="1080" data-start="0" data-duration="1" data-no-timeline></div>
</body>
</html>`;

const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "duplicate_composition_id");
expect(finding).toBeDefined();
expect(finding?.severity).toBe("error");
});

it("does not flag a single valid composition id", async () => {
const html = `<!DOCTYPE html>
<html>
<body>
<div data-composition-id="main" data-width="1920" data-height="1080" data-start="0" data-duration="1" data-no-timeline></div>
</body>
</html>`;

const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "duplicate_composition_id");
expect(finding).toBeUndefined();
});

it("does not flag distinct composition ids in one file", async () => {
const html = `<!DOCTYPE html>
<html>
<body>
<div data-composition-id="main" data-width="1920" data-height="1080" data-start="0" data-duration="5" data-no-timeline>
<div data-composition-id="scene" data-composition-src="compositions/scene.html" data-start="0" data-duration="5"></div>
</div>
</body>
</html>`;

const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "duplicate_composition_id");
expect(finding).toBeUndefined();
});
});

it("reports error when querySelector uses template literal variable", async () => {
const html = `
<html><body>
Expand Down
29 changes: 29 additions & 0 deletions packages/lint/src/rules/composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,35 @@ function rootClassStyledSelectors(styles: ExtractedBlock[], rootClasses: string[
}

export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
// duplicate_composition_id catches meta-tag/root collisions that create duplicate composition entries.
({ tags }) => {
const tagsByCompositionId = new Map<string, string[]>();
for (const tag of tags) {
const compositionId = readAttr(tag.raw, "data-composition-id");
if (!compositionId || compositionId.trim().length === 0) continue;

const matchingTags = tagsByCompositionId.get(compositionId) ?? [];
matchingTags.push(tag.raw);
tagsByCompositionId.set(compositionId, matchingTags);
}

const findings: HyperframeLintFinding[] = [];
for (const [compositionId, matchingTags] of tagsByCompositionId) {
if (matchingTags.length < 2) continue;

findings.push({
code: "duplicate_composition_id",
severity: "error",
message: `Composition id "${compositionId}" is used by ${matchingTags.length} elements. Each data-composition-id value must be unique within a composition file.`,
fixHint:
"Keep data-composition-id on exactly one element, the composition root. Remove it from metadata or duplicate hosts, especially a <meta> tag carrying the same data-composition-id as the root <div>, which causes a silent duplicate-id collision.",
snippet: truncateSnippet(matchingTags[0] ?? ""),
});
}

return findings;
},

// invalid_parent_traversal_in_asset_path — catches `../` traversal in src,
// href, inline-style url(), and <style> url() asset references on
// compositions. Sub-compositions live under compositions/ but are served
Expand Down
Loading