+
Scoped Text Should Stay Styled
@@ -1243,17 +1240,76 @@ describe("loadExternalCompositions", () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(compositionHtml, { status: 200 }));
- await loadExternalCompositions({ ...defaultParams });
+ const injectedStyles: HTMLStyleElement[] = [];
+ await loadExternalCompositions({ ...defaultParams, injectedStyles });
- // Not flattened: no data-hf-inner-root wrapper was created.
- expect(host.querySelector("[data-hf-inner-root]")).toBeNull();
- // The composition's own root element, with its own id intact, is a
- // direct descendant of the (still anonymous) host.
+ // Flattened like every other mount, with the declared id restored so the
+ // composition's scoped CSS and self-referencing queries still resolve.
const mountedRoot = host.querySelector('[data-composition-id="scoped-text"]');
- expect(mountedRoot).not.toBeNull();
+ expect(mountedRoot?.getAttribute("data-hf-inner-root")).toBe("true");
expect(mountedRoot?.querySelector(".label")?.textContent).toBe(
"Scoped Text Should Stay Styled",
);
+ // The stylesheet is scoped to the composition rather than leaking whole.
+ const injectedCss = injectedStyles.map((style) => style.textContent).join("\n");
+ expect(injectedCss).toContain('[data-composition-id="scoped-text"]');
+ expect(injectedCss).not.toMatch(/^\s*\.label\s*\{/);
+ });
+
+ // --- D1/D4 regressions: what the compiler and the mount path now agree on ---
+
+ it("executes an inline script of a non-templated composition", async () => {
+ // The compiler used to drop this one on the floor (its loop had a
+ // `src` branch and no else); the mount path always executed it.
+ const host = document.createElement("div");
+ host.setAttribute("data-composition-src", "https://example.com/head-script.html");
+ host.setAttribute("data-composition-id", "head-script");
+ document.body.appendChild(host);
+
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(
+ new Response(
+ `
+
+ `,
+ { status: 200 },
+ ),
+ );
+
+ const injectedScripts: HTMLScriptElement[] = [];
+ await loadExternalCompositions({ ...defaultParams, injectedScripts });
+
+ expect(injectedScripts.map((script) => script.textContent).join("\n")).toContain(
+ "window.__headScriptRan = true;",
+ );
+ });
+
+ it("scopes scripts to the id the content declares when the host names another", async () => {
+ // Two scope ids, not one: CSS stays on the id the host asked for, scripts
+ // follow the id the content actually declares, so a script's own
+ // querySelector('[data-composition-id="..."]') resolves. Collapsing them
+ // pointed every self-query at an id that is nowhere in the content.
+ const host = document.createElement("div");
+ host.setAttribute("data-composition-src", "https://example.com/captions.html");
+ host.setAttribute("data-composition-id", "captions-comp");
+ document.body.appendChild(host);
+
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(
+ new Response(
+ `
+
+
+ `,
+ { status: 200 },
+ ),
+ );
+
+ const injectedScripts: HTMLScriptElement[] = [];
+ const injectedStyles: HTMLStyleElement[] = [];
+ await loadExternalCompositions({ ...defaultParams, injectedScripts, injectedStyles });
+
+ const scriptSource = injectedScripts.map((script) => script.textContent).join("\n");
+ expect(scriptSource).toContain('var __hfCompId = "captions";');
+ expect(scriptSource).not.toContain('var __hfCompId = "captions-comp";');
});
});
diff --git a/packages/core/src/runtime/compositionLoader.ts b/packages/core/src/runtime/compositionLoader.ts
index 67d0b9cefb..55dc5c95fc 100644
--- a/packages/core/src/runtime/compositionLoader.ts
+++ b/packages/core/src/runtime/compositionLoader.ts
@@ -1,3 +1,4 @@
+import { planCompositionAssembly } from "../compiler/compositionAssembly";
import { scopeCssToComposition, wrapScopedCompositionScript } from "../compiler/compositionScoping";
import { markFlattenedInnerRoot } from "./flattenedRoot";
import {
@@ -383,12 +384,12 @@ async function mountCompositionContent(params: {
injectedScripts: HTMLScriptElement[];
injectedLinks: HTMLLinkElement[];
parseDimensionPx: (value: string | null) => string | null;
- /** Extra
+
+ Card
+
+
+ `),
+ },
+ {
+ name: "an anonymous host scoping to the id its content declares",
+ // A host naming no composition id. The mount path used to drop the
+ // content in whole and unscoped, so this composition's CSS landed on the
+ // host document at large; the compiler has always fallen back to the
+ // first declared root and scoped to it.
+ files: anonymousCardHost(`