fix(core): skip empty sub-composition files instead of aborting render (#1678)

This commit is contained in:
Miguel Ángel
2026-06-23 20:05:56 -04:00
committed by GitHub
parent d4aa5f93e1
commit 656c1200ee
4 changed files with 38 additions and 41 deletions
@@ -38,18 +38,27 @@ function makeHostDocument(compId: string) {
} }
describe("inlineSubCompositions #ID selector scoping divergence", () => { describe("inlineSubCompositions #ID selector scoping divergence", () => {
it("throws an actionable error when a resolved sub-composition file is empty", () => { it.each([
{ label: "empty", html: "" },
{ label: "whitespace-only", html: " \n \t " },
{
label: "valid-parse-empty-body",
html: "<!doctype html><html><head></head><body></body></html>",
},
])("skips $label sub-composition files gracefully", ({ html }) => {
const document = makeHostDocument("intro"); const document = makeHostDocument("intro");
const host = document.querySelector('[data-composition-src="intro.html"]')!; const host = document.querySelector('[data-composition-src="intro.html"]')!;
const missing: string[] = [];
expect(() => const result = inlineSubCompositions(document, [host], {
inlineSubCompositions(document, [host], { resolveHtml: () => html,
resolveHtml: () => "", parseHtml: (h) => parseHTML(h).document,
parseHtml: (html) => parseHTML(html).document, onMissingComposition: (src) => missing.push(src),
}), });
).toThrow(
"Composition HTML is empty or could not be parsed: intro.html. Check that the file referenced by data-composition-src contains valid HTML.", expect(missing).toEqual(["intro.html"]);
); expect(result.styles).toHaveLength(0);
expect(result.scripts).toHaveLength(0);
}); });
it("producer path (no flattenInnerRoot): strips inner root, losing #id attribute", () => { it("producer path (no flattenInnerRoot): strips inner root, losing #id attribute", () => {
@@ -125,24 +125,6 @@ function defaultBuildScopeSelector(compId: string): string {
return `[data-composition-id="${escaped}"]`; return `[data-composition-id="${escaped}"]`;
} }
function emptyCompositionHtmlError(src: string): Error {
return new Error(
`Composition HTML is empty or could not be parsed: ${src}. Check that the file referenced by data-composition-src contains valid HTML.`,
);
}
function assertNonEmptyCompositionHtml(html: string, src: string): void {
if (!html.trim()) {
throw emptyCompositionHtmlError(src);
}
}
function assertParsedCompositionDocument(doc: Document, src: string): void {
if (!doc.documentElement) {
throw emptyCompositionHtmlError(src);
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Core implementation // Core implementation
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -194,16 +176,16 @@ export function inlineSubCompositions(
if (!src) continue; if (!src) continue;
const compHtml = resolveHtml(src); const compHtml = resolveHtml(src);
if (compHtml == null) { if (compHtml == null || !compHtml.trim()) {
if (onMissingComposition) { onMissingComposition?.(src);
onMissingComposition(src);
}
continue; continue;
} }
assertNonEmptyCompositionHtml(compHtml, src);
const compDoc = parseHtml(compHtml); const compDoc = parseHtml(compHtml);
assertParsedCompositionDocument(compDoc, src); if (!compDoc.documentElement) {
onMissingComposition?.(src);
continue;
}
// Determine composition IDs // Determine composition IDs
let compId: string | null; let compId: string | null;
@@ -220,9 +202,15 @@ export function inlineSubCompositions(
// Find content: prefer <template>, fall back to <body> // Find content: prefer <template>, fall back to <body>
const contentRoot = compDoc.querySelector("template"); const contentRoot = compDoc.querySelector("template");
const contentHtml = contentRoot ? contentRoot.innerHTML || "" : compDoc.body?.innerHTML || ""; const contentHtml = contentRoot ? contentRoot.innerHTML || "" : compDoc.body?.innerHTML || "";
assertNonEmptyCompositionHtml(contentHtml, src); if (!contentHtml.trim()) {
onMissingComposition?.(src);
continue;
}
const contentDoc = parseHtml(contentHtml); const contentDoc = parseHtml(contentHtml);
assertParsedCompositionDocument(contentDoc, src); if (!contentDoc.documentElement) {
onMissingComposition?.(src);
continue;
}
// Find the inner composition root // Find the inner composition root
const innerRoot = compId const innerRoot = compId
@@ -492,7 +492,7 @@ describe("detectRenderModeHints", () => {
} }
}); });
it("compileForRender reports empty sub-composition HTML with an actionable error", async () => { it("compileForRender skips empty sub-composition files instead of aborting", async () => {
const projectDir = mkdtempSync(join(tmpdir(), "hf-empty-subcomp-")); const projectDir = mkdtempSync(join(tmpdir(), "hf-empty-subcomp-"));
const compositionsDir = join(projectDir, "compositions"); const compositionsDir = join(projectDir, "compositions");
mkdirSync(compositionsDir, { recursive: true }); mkdirSync(compositionsDir, { recursive: true });
@@ -514,11 +514,8 @@ describe("detectRenderModeHints", () => {
); );
writeFileSync(join(compositionsDir, "intro.html"), ""); writeFileSync(join(compositionsDir, "intro.html"), "");
await expect( const result = await compileForRender(projectDir, join(projectDir, "index.html"), projectDir);
compileForRender(projectDir, join(projectDir, "index.html"), projectDir), expect(result.html).toContain("data-composition-id");
).rejects.toThrow(
"Composition HTML is empty or could not be parsed: compositions/intro.html. Check that the file referenced by data-composition-src contains valid HTML.",
);
}); });
}); });
@@ -604,6 +604,9 @@ function inlineSubCompositions(
parseHtml: (htmlStr: string) => parseHTML(htmlStr).document as unknown as Document, parseHtml: (htmlStr: string) => parseHTML(htmlStr).document as unknown as Document,
scriptErrorLabel: "[Compiler] Composition script failed", scriptErrorLabel: "[Compiler] Composition script failed",
compoundAuthoredRoot: true, compoundAuthoredRoot: true,
onMissingComposition: (srcPath: string) => {
console.warn(`[Compiler] Composition file missing or empty: ${srcPath}`);
},
}, },
); );