From d004d00e6b518de51324b8fada3649b1a99cf1a7 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 25 Jul 2026 04:37:24 +0000 Subject: [PATCH 1/2] fix(fonts): resolve observed family aliases deterministically --- packages/core/src/fonts/aliases.ts | 2 ++ packages/lint/src/rules/fonts.test.ts | 18 ++++++++++ packages/lint/src/rules/fonts.ts | 7 +++- packages/parsers/src/composition.ts | 2 ++ packages/parsers/src/fontAliases.ts | 22 +++++++++++++ packages/parsers/src/index.ts | 2 ++ .../deterministicFonts-failClosed.test.ts | 33 +++++++++++++++++++ .../src/services/deterministicFonts.test.ts | 2 ++ .../src/services/deterministicFonts.ts | 5 +-- 9 files changed, 90 insertions(+), 3 deletions(-) diff --git a/packages/core/src/fonts/aliases.ts b/packages/core/src/fonts/aliases.ts index 14c7346b83..d88f5d5fc2 100644 --- a/packages/core/src/fonts/aliases.ts +++ b/packages/core/src/fonts/aliases.ts @@ -2,6 +2,8 @@ export { FONT_ALIAS_MAP, FONT_ALIAS_KEYS, + GOOGLE_FONT_FAMILY_ALIASES, + GOOGLE_FONT_FAMILY_ALIAS_KEYS, CANONICAL_FONT_DISPLAY_NAMES, resolveAliasDisplayName, } from "@hyperframes/parsers"; diff --git a/packages/lint/src/rules/fonts.test.ts b/packages/lint/src/rules/fonts.test.ts index 8facee61c9..396f8489f4 100644 --- a/packages/lint/src/rules/fonts.test.ts +++ b/packages/lint/src/rules/fonts.test.ts @@ -211,6 +211,24 @@ describe("font rules", () => { expect(findings).toHaveLength(0); }); + for (const family of [ + "Pretendard", + "Yantra Manav", + "Noto Serif Arabic", + "Noto Serif Urdu", + "Noto Sans Greek", + "Noto Sans Odia", + "Noto Sans Urdu", + ]) { + it(`does not flag the producer-resolved Google family alias ${family}`, async () => { + const html = `
+ +
`; + const findings = await findByCode(html, "font_family_without_font_face"); + expect(findings).toHaveLength(0); + }); + } + it("still flags Google-Fonts-only fonts not pre-bundled", async () => { const html = `
diff --git a/packages/lint/src/rules/fonts.ts b/packages/lint/src/rules/fonts.ts index cba97865f2..d9fdefa548 100644 --- a/packages/lint/src/rules/fonts.ts +++ b/packages/lint/src/rules/fonts.ts @@ -1,4 +1,8 @@ -import { FONT_ALIAS_KEYS, resolveAliasDisplayName } from "@hyperframes/parsers/composition"; +import { + FONT_ALIAS_KEYS, + GOOGLE_FONT_FAMILY_ALIAS_KEYS, + resolveAliasDisplayName, +} from "@hyperframes/parsers/composition"; import type { LintContext, HyperframeLintFinding } from "../context"; import { isRegistrySourceFile, isRegistryInstalledFile } from "./composition"; @@ -223,6 +227,7 @@ export const fontRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [ (name) => !declared.has(name) && !FONT_ALIAS_KEYS.has(name) && + !GOOGLE_FONT_FAMILY_ALIAS_KEYS.has(name) && !googleFonts.has(name.replace(/\+/g, " ")), ); if (undeclared.length === 0) return findings; diff --git a/packages/parsers/src/composition.ts b/packages/parsers/src/composition.ts index ac078646ba..1d9c4c48e9 100644 --- a/packages/parsers/src/composition.ts +++ b/packages/parsers/src/composition.ts @@ -6,6 +6,8 @@ export * from "./types.js"; export { FONT_ALIAS_MAP, FONT_ALIAS_KEYS, + GOOGLE_FONT_FAMILY_ALIASES, + GOOGLE_FONT_FAMILY_ALIAS_KEYS, CANONICAL_FONT_DISPLAY_NAMES, resolveAliasDisplayName, } from "./fontAliases.js"; diff --git a/packages/parsers/src/fontAliases.ts b/packages/parsers/src/fontAliases.ts index 9328f0dc5a..c3f4d2c9d5 100644 --- a/packages/parsers/src/fontAliases.ts +++ b/packages/parsers/src/fontAliases.ts @@ -33,6 +33,7 @@ export const FONT_ALIAS_MAP = { arial: "inter", "helvetica bold": "inter", futura: "montserrat", + "futura std": "montserrat", "din alternate": "montserrat", "arial black": "montserrat", "bebas neue": "league-gothic", @@ -62,6 +63,7 @@ export const FONT_ALIAS_MAP = { corbel: "inter", "lucida sans": "inter", "lucida sans unicode": "inter", + "ms sans serif": "inter", // ── Linux sans-serif system fonts → inter ───────────────────────────── "noto sans": "inter", @@ -93,6 +95,26 @@ export const FONT_ALIAS_MAP = { export const FONT_ALIAS_KEYS: ReadonlySet = new Set(Object.keys(FONT_ALIAS_MAP)); +/** + * Authoring names that are not served by Google Fonts verbatim, but have a + * deterministic, script-compatible Google family. Unlike FONT_ALIAS_MAP, + * these preserve the authored CSS family and only change the upstream fetch + * name used to build its injected @font-face rules. + */ +export const GOOGLE_FONT_FAMILY_ALIASES: Readonly> = { + pretendard: "Noto Sans KR", + "yantra manav": "Yantramanav", + "noto serif arabic": "Noto Naskh Arabic", + "noto serif urdu": "Noto Nastaliq Urdu", + "noto sans greek": "Noto Sans", + "noto sans odia": "Noto Sans Oriya", + "noto sans urdu": "Noto Sans Arabic", +}; + +export const GOOGLE_FONT_FAMILY_ALIAS_KEYS: ReadonlySet = new Set( + Object.keys(GOOGLE_FONT_FAMILY_ALIASES), +); + /** * Human-readable display names for canonical font slugs. Used by the lint * rule to tell authors what their aliased font will render as. diff --git a/packages/parsers/src/index.ts b/packages/parsers/src/index.ts index 2baf100c85..93d919af0d 100644 --- a/packages/parsers/src/index.ts +++ b/packages/parsers/src/index.ts @@ -17,6 +17,8 @@ export { scanVariableUsage, type VariableUsageScan } from "./variableUsage.js"; export { FONT_ALIAS_MAP, FONT_ALIAS_KEYS, + GOOGLE_FONT_FAMILY_ALIASES, + GOOGLE_FONT_FAMILY_ALIAS_KEYS, CANONICAL_FONT_DISPLAY_NAMES, resolveAliasDisplayName, } from "./fontAliases.js"; diff --git a/packages/producer/src/services/deterministicFonts-failClosed.test.ts b/packages/producer/src/services/deterministicFonts-failClosed.test.ts index f2c7cb4c94..4fc6aef8a2 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 { parseHTML } from "linkedom"; import { FONT_FETCH_FAILED, FontFetchError, @@ -141,6 +142,38 @@ describe("injectDeterministicFontFaces — failClosedFontFetch: true", () => { }); } + for (const [authoredFamily, googleFamily] of [ + ["Pretendard", "Noto Sans KR"], + ["Yantra Manav", "Yantramanav"], + ["Noto Serif Arabic", "Noto Naskh Arabic"], + ["Noto Serif Urdu", "Noto Nastaliq Urdu"], + ["Noto Sans Greek", "Noto Sans"], + ["Noto Sans Odia", "Noto Sans Oriya"], + ["Noto Sans Urdu", "Noto Sans Arabic"], + ] as const) { + it(`resolves observed family alias ${authoredFamily} through ${googleFamily}`, async () => { + const cssRequests: string[] = []; + const html = `

hello

`; + + const result = await injectDeterministicFontFaces(html, { + failClosedFontFetch: true, + allowSystemFontCapture: false, + fetchImpl: makeGoogleFontFetch(cssRequests), + }); + + expect(cssRequests).toHaveLength(1); + const familyParam = new URL(cssRequests[0]!).searchParams.get("family"); + expect(familyParam?.startsWith(`${googleFamily}:`)).toBe(true); + const { document } = parseHTML(result); + const injectedCss = document.querySelector( + 'style[data-hyperframes-deterministic-fonts="true"]', + )?.textContent; + expect(injectedCss).toContain(`font-family: "${authoredFamily}"`); + }); + } + it("throws FontFetchError on a network failure", async () => { let caught: unknown; try { diff --git a/packages/producer/src/services/deterministicFonts.test.ts b/packages/producer/src/services/deterministicFonts.test.ts index 3dabac055a..b0bbd7f16b 100644 --- a/packages/producer/src/services/deterministicFonts.test.ts +++ b/packages/producer/src/services/deterministicFonts.test.ts @@ -61,6 +61,8 @@ describe("FONT_ALIASES cross-platform coverage", () => { expect(FONT_ALIASES["courier new"]).toBe("jetbrains-mono"); expect(FONT_ALIASES["segoe ui"]).toBe("roboto"); expect(FONT_ALIASES["futura"]).toBe("montserrat"); + expect(FONT_ALIASES["futura std"]).toBe("montserrat"); + expect(FONT_ALIASES["ms sans serif"]).toBe("inter"); expect(FONT_ALIASES["bebas neue"]).toBe("league-gothic"); }); diff --git a/packages/producer/src/services/deterministicFonts.ts b/packages/producer/src/services/deterministicFonts.ts index df4664d4a1..742844154f 100644 --- a/packages/producer/src/services/deterministicFonts.ts +++ b/packages/producer/src/services/deterministicFonts.ts @@ -4,7 +4,7 @@ import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { defaultLogger } from "../logger.js"; -import { FONT_ALIAS_MAP } from "@hyperframes/core/fonts/aliases"; +import { FONT_ALIAS_MAP, GOOGLE_FONT_FAMILY_ALIASES } from "@hyperframes/core/fonts/aliases"; import { locateSystemFontVariants, SYSTEM_FONT_SIZE_LIMIT, @@ -748,7 +748,8 @@ async function fetchGoogleFont( // space. Resolve that URL-style spelling through the canonical Google family // while preserving `familyName` for the emitted @font-face alias so the // authored CSS still matches it. - const googleFamilyName = familyName.replace(/\+/g, " "); + const googleFamilyName = + GOOGLE_FONT_FAMILY_ALIASES[normalizeFamilyName(familyName)] ?? familyName.replace(/\+/g, " "); const encodedFamily = encodeURIComponent(googleFamilyName); const textParam = fontText ? `&text=${encodeURIComponent(fontText)}` : ""; const url = `https://fonts.googleapis.com/css2?family=${encodedFamily}:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700${textParam}`; From afc0b9d3d8c31834bb38a3fd0c0e052cf378e3b8 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 25 Jul 2026 08:18:24 +0000 Subject: [PATCH 2/2] fix(fonts): cover current unresolved aliases --- packages/lint/src/rules/fonts.test.ts | 3 +++ packages/parsers/src/fontAliases.ts | 3 +++ .../src/services/deterministicFonts-failClosed.test.ts | 3 +++ 3 files changed, 9 insertions(+) diff --git a/packages/lint/src/rules/fonts.test.ts b/packages/lint/src/rules/fonts.test.ts index 396f8489f4..4e8c808c7f 100644 --- a/packages/lint/src/rules/fonts.test.ts +++ b/packages/lint/src/rules/fonts.test.ts @@ -212,10 +212,13 @@ describe("font rules", () => { }); for (const family of [ + "FredericktheGreat", "Pretendard", + "Pyidaungsu", "Yantra Manav", "Noto Serif Arabic", "Noto Serif Urdu", + "Noto Serif VI", "Noto Sans Greek", "Noto Sans Odia", "Noto Sans Urdu", diff --git a/packages/parsers/src/fontAliases.ts b/packages/parsers/src/fontAliases.ts index c3f4d2c9d5..a4fd03190d 100644 --- a/packages/parsers/src/fontAliases.ts +++ b/packages/parsers/src/fontAliases.ts @@ -102,10 +102,13 @@ export const FONT_ALIAS_KEYS: ReadonlySet = new Set(Object.keys(FONT_ALI * name used to build its injected @font-face rules. */ export const GOOGLE_FONT_FAMILY_ALIASES: Readonly> = { + frederickthegreat: "Fredericka the Great", pretendard: "Noto Sans KR", + pyidaungsu: "Noto Sans Myanmar", "yantra manav": "Yantramanav", "noto serif arabic": "Noto Naskh Arabic", "noto serif urdu": "Noto Nastaliq Urdu", + "noto serif vi": "Noto Serif", "noto sans greek": "Noto Sans", "noto sans odia": "Noto Sans Oriya", "noto sans urdu": "Noto Sans Arabic", diff --git a/packages/producer/src/services/deterministicFonts-failClosed.test.ts b/packages/producer/src/services/deterministicFonts-failClosed.test.ts index 4fc6aef8a2..29c5000ec4 100644 --- a/packages/producer/src/services/deterministicFonts-failClosed.test.ts +++ b/packages/producer/src/services/deterministicFonts-failClosed.test.ts @@ -143,10 +143,13 @@ describe("injectDeterministicFontFaces — failClosedFontFetch: true", () => { } for (const [authoredFamily, googleFamily] of [ + ["FredericktheGreat", "Fredericka the Great"], ["Pretendard", "Noto Sans KR"], + ["Pyidaungsu", "Noto Sans Myanmar"], ["Yantra Manav", "Yantramanav"], ["Noto Serif Arabic", "Noto Naskh Arabic"], ["Noto Serif Urdu", "Noto Nastaliq Urdu"], + ["Noto Serif VI", "Noto Serif"], ["Noto Sans Greek", "Noto Sans"], ["Noto Sans Odia", "Noto Sans Oriya"], ["Noto Sans Urdu", "Noto Sans Arabic"],