From e9ab9a0b5145bc9208549a73d2bed1b8c0ea483a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 26 Jun 2026 21:45:08 +0000 Subject: [PATCH] fix(lint): allow producer-resolved google fonts --- packages/core/src/lint/rules/fonts.test.ts | 63 ++++++++++++++++-- packages/core/src/lint/rules/fonts.ts | 76 +++++++++++++++++++--- 2 files changed, 127 insertions(+), 12 deletions(-) diff --git a/packages/core/src/lint/rules/fonts.test.ts b/packages/core/src/lint/rules/fonts.test.ts index 6e28c881a0..853781bd74 100644 --- a/packages/core/src/lint/rules/fonts.test.ts +++ b/packages/core/src/lint/rules/fonts.test.ts @@ -8,21 +8,24 @@ async function findByCode(html: string, code: string, isSubComposition = true) { describe("font rules", () => { describe("google_fonts_import", () => { - it("flags @import url with fonts.googleapis.com", async () => { + it("warns on @import url with fonts.googleapis.com without failing lint", async () => { const html = `
`; - const findings = await findByCode(html, "google_fonts_import"); + const result = await lintHyperframeHtml(html, { isSubComposition: true }); + const findings = result.findings.filter((f) => f.code === "google_fonts_import"); expect(findings).toHaveLength(1); - expect(findings[0]!.severity).toBe("error"); + expect(findings[0]!.severity).toBe("warning"); + expect(result.errorCount).toBe(0); }); - it("flags to fonts.googleapis.com", async () => { + it("warns on to fonts.googleapis.com", async () => { const html = `
`; const findings = await findByCode(html, "google_fonts_import"); expect(findings).toHaveLength(1); + expect(findings[0]!.severity).toBe("warning"); }); it("does not flag local @font-face usage", async () => { @@ -185,6 +188,58 @@ describe("font rules", () => { expect(findings[0]!.message).toContain("geist"); }); + it("does not flag a non-bundled family when a Google Fonts link loads it", async () => { + const html = `
+ + +
`; + const result = await lintHyperframeHtml(html, { isSubComposition: true }); + expect(result.findings.filter((f) => f.code === "google_fonts_import")).toHaveLength(1); + expect( + result.findings.filter((f) => f.code === "font_family_without_font_face"), + ).toHaveLength(0); + expect(result.errorCount).toBe(0); + }); + + it("parses unquoted Google Fonts link href values", async () => { + const html = `
+ + +
`; + const result = await lintHyperframeHtml(html, { isSubComposition: true }); + expect(result.findings.filter((f) => f.code === "google_fonts_import")).toHaveLength(1); + expect( + result.findings.filter((f) => f.code === "font_family_without_font_face"), + ).toHaveLength(0); + expect(result.errorCount).toBe(0); + }); + + it("parses multiple Google Fonts family parameters and URL-encoded spaces", async () => { + const html = `
+ +
`; + const result = await lintHyperframeHtml(html, { isSubComposition: true }); + expect(result.findings.filter((f) => f.code === "google_fonts_import")).toHaveLength(1); + expect( + result.findings.filter((f) => f.code === "font_family_without_font_face"), + ).toHaveLength(0); + expect(result.errorCount).toBe(0); + }); + + it("still flags non-bundled families not covered by the Google Fonts URL", async () => { + const html = `
+ + +
`; + const findings = await findByCode(html, "font_family_without_font_face"); + expect(findings).toHaveLength(1); + expect(findings[0]!.message).toContain("geist"); + }); + it("is case-insensitive when matching @font-face to font-family", async () => { const html = `