Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { describe, expect, it } from "bun:test";
import { defaultLogger } from "../logger.js";
import {
FONT_FETCH_FAILED,
FontFetchError,
Expand Down Expand Up @@ -56,6 +57,43 @@
}

describe("injectDeterministicFontFaces — failClosedFontFetch: false (default)", () => {
it("warns before downloading a very large Google Fonts subset matrix", async () => {
const faces = Array.from(
{ length: 101 },
(_, index) => `
@font-face {
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.test/noto-${index}.woff2) format('woff2');
unicode-range: U+${index.toString(16).padStart(4, "0")};
}`,
).join("\n");
const fetchImpl = (async (input: string | URL | Request) => {
const url = String(input);
return url.includes("fonts.googleapis.com")

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
fonts.googleapis.com
' can be anywhere in the URL, and arbitrary hosts may come before or after it.
? new Response(faces, { status: 200 })
: new Response(new Uint8Array([1, 2, 3]), { status: 200 });
}) as unknown as typeof fetch;
const html = `<!doctype html><html><head><style>
body { font-family: "Noto Sans JP", sans-serif; }
</style></head><body>日本語</body></html>`;
const warnings: string[] = [];
const originalWarn = defaultLogger.warn;
defaultLogger.warn = (message) => warnings.push(message);

try {
await injectDeterministicFontFaces(html, {
allowSystemFontCapture: false,
fetchImpl,
});
} finally {
defaultLogger.warn = originalWarn;
}

expect(warnings).toContainEqual(expect.stringContaining("101 subset faces"));
expect(warnings).toContainEqual(expect.stringContaining("self-hosting or subsetting"));
});

it("swallows a network failure and returns the original HTML (no throw)", async () => {
const result = await injectDeterministicFontFaces(HTML_REQUESTING_UNRESOLVED_FONT, {
failClosedFontFetch: false,
Expand Down
20 changes: 19 additions & 1 deletion packages/producer/src/services/deterministicFonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,12 @@ async function fetchGoogleFont(
const faceRegex =
/@font-face\s*\{[^}]*font-style:\s*(normal|italic)[^}]*font-weight:\s*(\d+)[^}]*src:\s*url\(([^)]+)\)\s*format\(['"]woff2['"]\)(?:[^}]*?unicode-range:\s*([^;}]+))?[^}]*\}/gi;

const faces: GoogleFontFace[] = [];
const parsedFaces: Array<{
style: string;
weight: string;
woff2Url: string;
unicodeRange?: string;
}> = [];

for (const match of cssText.matchAll(faceRegex)) {
const style = match[1] || "normal";
Expand All @@ -788,6 +793,19 @@ async function fetchGoogleFont(

if (!woff2Url) continue;

parsedFaces.push({ style, weight, woff2Url, unicodeRange });
}

if (parsedFaces.length > 100) {
defaultLogger.warn(
`[Compiler] Google Fonts "${familyName}" expands to ${parsedFaces.length} subset faces. ` +
`Embedding them can make compile iterations several minutes slower. Consider self-hosting or subsetting ` +
`the font to the characters used by this composition: https://hyperframes.heygen.com/docs/fonts`,
);
}

const faces: GoogleFontFace[] = [];
for (const { style, weight, woff2Url, unicodeRange } of parsedFaces) {
const cachePath = cachedWoff2Path(slug, weight, style, subsetToken(woff2Url));
const dataUri = await ensureWoff2DataUri(
cachePath,
Expand Down
Loading