diff --git a/packages/core/src/lint/rules/composition.test.ts b/packages/core/src/lint/rules/composition.test.ts
index 34c0fffcd..6d50c42e0 100644
--- a/packages/core/src/lint/rules/composition.test.ts
+++ b/packages/core/src/lint/rules/composition.test.ts
@@ -201,6 +201,149 @@ describe("composition rules", () => {
});
});
+ describe("root_composition_missing_html_wrapper", () => {
+ it("flags bare composition div as error", () => {
+ // Exact scenario from the screenshot — bare div with composition attributes, no HTML wrapper
+ const html = `
`;
+ const result = lintHyperframeHtml(html, { filePath: "index.html" });
+ const finding = result.findings.find(
+ (f) => f.code === "root_composition_missing_html_wrapper",
+ );
+ expect(finding).toBeDefined();
+ expect(finding?.severity).toBe("error");
+ expect(result.ok).toBe(false);
+ });
+
+ it("does not flag properly wrapped HTML composition", () => {
+ const html = `
+
+
+
+`;
+ const result = lintHyperframeHtml(html);
+ const finding = result.findings.find(
+ (f) => f.code === "root_composition_missing_html_wrapper",
+ );
+ expect(finding).toBeUndefined();
+ });
+
+ it("does not flag composition starting with (no doctype)", () => {
+ const html = `
+
+
+`;
+ const result = lintHyperframeHtml(html);
+ const finding = result.findings.find(
+ (f) => f.code === "root_composition_missing_html_wrapper",
+ );
+ expect(finding).toBeUndefined();
+ });
+
+ it("does not flag sub-compositions", () => {
+ const html = `
+
+
`;
+ const result = lintHyperframeHtml(html, { isSubComposition: true });
+ const finding = result.findings.find(
+ (f) => f.code === "root_composition_missing_html_wrapper",
+ );
+ expect(finding).toBeUndefined();
+ });
+
+ it("does not flag HTML without composition attributes", () => {
+ const html = ``;
+ const result = lintHyperframeHtml(html);
+ const finding = result.findings.find(
+ (f) => f.code === "root_composition_missing_html_wrapper",
+ );
+ expect(finding).toBeUndefined();
+ });
+
+ it("includes root tag snippet in finding", () => {
+ const html = `
+
+
`;
+ const result = lintHyperframeHtml(html);
+ const finding = result.findings.find(
+ (f) => f.code === "root_composition_missing_html_wrapper",
+ );
+ expect(finding).toBeDefined();
+ expect(finding?.snippet).toContain("data-composition-id");
+ });
+ });
+
+ describe("standalone_composition_wrapped_in_template", () => {
+ it("flags root index.html wrapped in template", () => {
+ const html = `
+
+
+
+`;
+ const result = lintHyperframeHtml(html);
+ const finding = result.findings.find(
+ (f) => f.code === "standalone_composition_wrapped_in_template",
+ );
+ expect(finding).toBeDefined();
+ expect(finding?.severity).toBe("warning");
+ });
+
+ it("does not flag sub-compositions in template", () => {
+ const html = `
+
+
+
+`;
+ const result = lintHyperframeHtml(html, { isSubComposition: true });
+ const finding = result.findings.find(
+ (f) => f.code === "standalone_composition_wrapped_in_template",
+ );
+ expect(finding).toBeUndefined();
+ });
+ });
+
describe("requestanimationframe_in_composition", () => {
it("flags requestAnimationFrame usage in script content", () => {
const html = `
diff --git a/packages/core/src/lint/rules/composition.ts b/packages/core/src/lint/rules/composition.ts
index d47e45edf..e7e24e1cd 100644
--- a/packages/core/src/lint/rules/composition.ts
+++ b/packages/core/src/lint/rules/composition.ts
@@ -231,21 +231,26 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
},
// root_composition_missing_html_wrapper
- ({ rawSource, options }) => {
+ ({ rawSource, rootTag, options }) => {
const findings: HyperframeLintFinding[] = [];
if (options.isSubComposition) return findings;
const trimmed = rawSource.trimStart().toLowerCase();
+ // Compositions inside are caught by standalone_composition_wrapped_in_template
+ if (trimmed.startsWith(" and wrapper. " +
- "The bundler and preview expect a complete HTML document for index.html files.",
+ "Composition starts with a bare element instead of a proper HTML document. " +
+ "An index.html that contains data-composition-id but no , , or " +
+ "is a fragment — browsers quirks-mode it, the preview server cannot load it, and " +
+ "the bundler will fail to inject runtime scripts.",
fixHint:
'Wrap the composition in ....',
+ snippet: rootTag ? truncateSnippet(rootTag.raw) : undefined,
});
}
return findings;