Skip to content
Merged
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
30 changes: 30 additions & 0 deletions packages/polycss/src/snapshot/exportPolySceneSnapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,36 @@ describe("exportPolySceneSnapshot", () => {
expect(fetchMock).toHaveBeenCalledTimes(1);
});

it("falls back to XHR when fetch() fails for a blob URL (WebKit)", async () => {
const fetchMock = vi.fn(async () => {
throw new TypeError("Load failed");
});
vi.stubGlobal("fetch", fetchMock);
class FakeXHR {
status = 200;
response: Blob | null = null;
responseType = "";
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
open(): void {}
send(): void {
this.response = new Blob(["atlas"], { type: "image/png" });
this.onload?.();
}
}
vi.stubGlobal("XMLHttpRequest", FakeXHR);

const { leaf } = makeRenderedScene(
'background: url("blob:atlas") 0 0 / 64px 64px no-repeat; --polycss-atlas-size: 64px;',
);

const html = await exportPolySceneSnapshot(leaf);

expect(html).toContain("data:image/png;base64,YXRsYXM=");
expect(html).not.toContain("blob:atlas");
expect(fetchMock).toHaveBeenCalledTimes(1);
});

it("does not fetch already-inline data URLs", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
Expand Down
57 changes: 47 additions & 10 deletions packages/polycss/src/snapshot/exportPolySceneSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,52 @@ function arrayBufferToBase64(buffer: ArrayBuffer): string {
return btoa(binary);
}

function isObjectUrl(url: string): boolean {
return /^(blob|filesystem):/i.test(url.trim());
}

function loadBlobViaXhr(url: string): Promise<Blob> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "blob";
xhr.onload = () => {
// Object URLs report status 0 on success; treat that as OK.
if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) {
resolve(xhr.response as Blob);
} else {
reject(new Error(`XHR ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error("XHR request failed"));
xhr.send();
});
}

async function loadAssetBlob(url: string): Promise<Blob> {
// Object URLs (blob:/filesystem:) are same-document; a credentials mode is
// meaningless for them and trips some WebKit builds, so omit it there.
const objectUrl = isObjectUrl(url);
try {
if (typeof fetch !== "function") {
throw new Error("fetch is not available");
}
const response = await fetch(url, objectUrl ? undefined : { credentials: "same-origin" });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.blob();
} catch (error) {
// WebKit/Safari intermittently fails to fetch() object URLs ("Load
// failed"); XMLHttpRequest is the reliable fallback for same-document
// object URLs and is why a textured/atlas export breaks only on Safari.
if (objectUrl && typeof XMLHttpRequest === "function") {
return loadBlobViaXhr(url);
}
throw error;
}
}

async function inlineAssetUrl(rawUrl: string, ctx: InlineContext): Promise<string> {
if (isInlineOrLocalReference(rawUrl)) return rawUrl;

Expand All @@ -664,16 +710,7 @@ async function inlineAssetUrl(rawUrl: string, ctx: InlineContext): Promise<strin

const next = (async () => {
try {
if (typeof fetch !== "function") {
throw new Error("fetch is not available");
}

const response = await fetch(resolvedUrl, { credentials: "same-origin" });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

const blob = await response.blob();
const blob = await loadAssetBlob(resolvedUrl);
const mime = blob.type || inferMimeType(resolvedUrl);
const base64 = arrayBufferToBase64(await blob.arrayBuffer());
return `data:${mime};base64,${base64}`;
Expand Down
Loading