Skip to content
Draft
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
29 changes: 29 additions & 0 deletions packages/core/src/runtime/compositionLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,35 @@ describe("loadCompositions external hosts", () => {
).toBeNull();
});

// `assets/theme.css` in a sub-composition loaded from compositions/assets/ in the preview and 404'd, while the render
// resolved it from the project root (rewriteAssetPath: a plain path is a sibling only if one exists).
it.each([
["no sibling file: the project root", false, "assets/theme.css"],
["a sibling file: beside the composition", true, "compositions/assets/theme.css"],
])(
"a plain stylesheet path resolves as the render does, %s",
async (_case, sibling, expected) => {
appendExternalHost("compositions/scene.html", "scene");
const compositionHtml = `<template id="scene-template">
<div data-composition-id="scene"><link rel="stylesheet" href="assets/theme.css"><p>Styled</p></div>
</template>`;
const siblingUrl = new URL("compositions/assets/theme.css", document.baseURI).href;
vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
const url = new URL(String(input instanceof Request ? input.url : input), document.baseURI)
.href;
if (url === siblingUrl) return new Response("", { status: sibling ? 200 : 404 });
return new Response(compositionHtml, { status: 200 });
});

await loadFixture();

const hrefs = [
...document.head.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]'),
].map((link) => link.href);
expect(hrefs).toEqual([new URL(expected, document.baseURI).href]);
},
);

it("does not inject stylesheet href variants that resolve to the composition document", async () => {
appendExternalHost("https://example.com/compositions/scene.html", "scene");

Expand Down
20 changes: 19 additions & 1 deletion packages/core/src/runtime/compositionLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,24 @@ function rewriteSubCompositionAssetPaths(root: ParentNode, compositionUrl: URL |
}
}

/**
* A hoisted `<link>`'s URL, by the compiler's rule (`rewriteAssetPath` with its `assetExists` probe): a plain relative
* href is the file beside the composition when one is there, else the project root's (`assets/theme.css`). `../`
* hrefs are already absolute from `rewriteSubCompositionAssetPaths`.
*/
async function hoistedLinkHref(rawHref: string, compositionUrl: URL | null): Promise<string> {
if (!compositionUrl) return rawHref;
if (isNonRelativeRuntimeUrl(rawHref)) return new URL(rawHref, compositionUrl).href;
const sibling = new URL(rawHref, compositionUrl).href;
const root = new URL(rawHref, document.baseURI).href;
if (sibling === root) return root;
try {
return (await fetch(sibling, { method: "HEAD" })).ok ? sibling : root;
} catch {
return root;
}
}

function uniqueCompositionId(baseId: string, index: number): string {
return `${baseId}__hf${index}`;
}
Expand Down Expand Up @@ -487,7 +505,7 @@ async function mountCompositionContent(params: {
for (const link of plan.linkSources) {
const rawHref = (link.getAttribute("href") || "").trim();
if (!rawHref) continue;
const href = params.compositionUrl ? new URL(rawHref, params.compositionUrl).href : rawHref;
const href = await hoistedLinkHref(rawHref, params.compositionUrl);
if (params.compositionUrl && isSameDocumentUrl(href, params.compositionUrl)) continue;
const clonedLink = link.cloneNode(true);
if (!isLinkElement(clonedLink)) continue;
Expand Down
Loading