From 11432ea8ce82571c8473244f56edc06a8decf531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 30 Jun 2026 11:31:36 -0700 Subject: [PATCH] fix(lint): stop font_family_without_font_face flagging system-ui stacks and var() (#1796) The font_family_without_font_face rule raised a blocking error on two legitimate, very common cases: 1. The `-apple-system, BlinkMacSystemFont` system-ui stack. These two tokens are the cross-browser incantation for the platform UI font (synonyms of `system-ui`); they name no font file, so demanding an @font-face for them is wrong. They appear in almost every CSS reset. 2. `font-family: var(--x)` indirection. The shared family extractor took the literal `var(--heading)` as a font name. The linter cannot statically resolve a custom property, so it must not flag it. Fix at the root: add the two system-ui synonyms to GENERIC_FAMILIES, and skip any parenthesised (function) token in the shared family extractor so both this rule and system_font_will_alias stop misreading var(). A real undeclared font sitting in the same stack is still flagged. --- packages/lint/src/rules/fonts.test.ts | 34 +++++++++++++++++++++++++++ packages/lint/src/rules/fonts.ts | 25 ++++++++++++++------ 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/lint/src/rules/fonts.test.ts b/packages/lint/src/rules/fonts.test.ts index 964cecbf5..8a22f53f3 100644 --- a/packages/lint/src/rules/fonts.test.ts +++ b/packages/lint/src/rules/fonts.test.ts @@ -291,5 +291,39 @@ describe("font rules", () => { const findings = await findByCode(html, "font_family_without_font_face"); expect(findings).toHaveLength(0); }); + + it("does not flag the -apple-system / BlinkMacSystemFont system-ui stack", async () => { + const html = `
+ +
`; + const findings = await findByCode(html, "font_family_without_font_face"); + expect(findings).toHaveLength(0); + }); + + it("does not flag a var() font-family indirection it cannot resolve", async () => { + const html = `
+ +
`; + const findings = await findByCode(html, "font_family_without_font_face"); + expect(findings).toHaveLength(0); + }); + + it("does not flag a var() with a quoted fallback font", async () => { + const html = `
+ +
`; + const findings = await findByCode(html, "font_family_without_font_face"); + expect(findings).toHaveLength(0); + }); + + it("still flags a real undeclared font sitting next to a system stack", async () => { + const html = `
+ +
`; + const findings = await findByCode(html, "font_family_without_font_face"); + expect(findings).toHaveLength(1); + expect(findings[0]!.message).toContain("aeonik"); + expect(findings[0]!.message).not.toContain("apple-system"); + }); }); }); diff --git a/packages/lint/src/rules/fonts.ts b/packages/lint/src/rules/fonts.ts index 5ffdd22d7..611fb4a18 100644 --- a/packages/lint/src/rules/fonts.ts +++ b/packages/lint/src/rules/fonts.ts @@ -56,6 +56,22 @@ function extractFontFaceFamilies(styles: Array<{ content: string }>): Set): string[] { const used: string[] = []; const seen = new Set(); @@ -64,13 +80,8 @@ function extractUsedFontFamilies(styles: Array<{ content: string }>): string[] { const withoutFontFace = stripCssComments(style.content).replace(/@font-face\s*\{[^}]*\}/gi, ""); let match: RegExpExecArray | null; while ((match = propRe.exec(withoutFontFace)) !== null) { - const stack = match[1]!; - for (const part of stack.split(",")) { - const name = part - .trim() - .replace(/^['"]|['"]$/g, "") - .trim() - .toLowerCase(); + for (const part of match[1]!.split(",")) { + const name = normalizeUsedFontName(part); if (name && !GENERIC_FAMILIES.has(name) && !seen.has(name)) { seen.add(name); used.push(name);