diff --git a/packages/producer/src/services/deterministicFonts-failClosed.test.ts b/packages/producer/src/services/deterministicFonts-failClosed.test.ts index d3860ed6f1..47463d8c92 100644 --- a/packages/producer/src/services/deterministicFonts-failClosed.test.ts +++ b/packages/producer/src/services/deterministicFonts-failClosed.test.ts @@ -15,6 +15,7 @@ */ import { describe, expect, it } from "bun:test"; +import { defaultLogger } from "../logger.js"; import { FONT_FETCH_FAILED, FontFetchError, @@ -56,6 +57,43 @@ function makeHttp503Fetch(): typeof fetch { } 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") + ? new Response(faces, { status: 200 }) + : new Response(new Uint8Array([1, 2, 3]), { status: 200 }); + }) as unknown as typeof fetch; + const 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, diff --git a/packages/producer/src/services/deterministicFonts.ts b/packages/producer/src/services/deterministicFonts.ts index 2805bae2e9..cc5f518b19 100644 --- a/packages/producer/src/services/deterministicFonts.ts +++ b/packages/producer/src/services/deterministicFonts.ts @@ -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"; @@ -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,