From fd9c64f41629efc6856206839b049937502e1366 Mon Sep 17 00:00:00 2001 From: shadcn Date: Fri, 27 Feb 2026 21:52:51 +0400 Subject: [PATCH] fix: issue with fonts --- .../src/utils/updaters/update-fonts.test.ts | 47 +++++++++++++++- .../shadcn/src/utils/updaters/update-fonts.ts | 55 +++++++++++++------ 2 files changed, 83 insertions(+), 19 deletions(-) diff --git a/packages/shadcn/src/utils/updaters/update-fonts.test.ts b/packages/shadcn/src/utils/updaters/update-fonts.test.ts index 2a27db89be..35d8684348 100644 --- a/packages/shadcn/src/utils/updaters/update-fonts.test.ts +++ b/packages/shadcn/src/utils/updaters/update-fonts.test.ts @@ -159,7 +159,7 @@ export default function RootLayout({ children: React.ReactNode }) { return ( - + {children} ) @@ -962,7 +962,7 @@ export default function RootLayout({ children: React.ReactNode }) { return ( - + {children} ) @@ -971,6 +971,49 @@ export default function RootLayout({ `) }) + it("should replace existing font-sans with font-serif on html", async () => { + const input = ` +import { Inter } from "next/font/google"; +import { cn } from "@/lib/utils"; + +const inter = Inter({subsets:['latin'],variable:'--font-sans'}); + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} +` + const fonts = [ + { + name: "font-playfair-display", + type: "registry:font" as const, + font: { + family: "'Playfair Display Variable', serif", + provider: "google" as const, + import: "Playfair_Display", + variable: "--font-serif", + subsets: ["latin"], + }, + }, + ] + + const result = await transformLayoutFonts(input, fonts, mockConfig) + + // font-sans should be replaced with font-serif. + expect(result).toContain('"font-serif"') + expect(result).not.toContain('"font-sans"') + expect(result).toContain("playfairDisplay.variable") + // Inter's variable should remain since we only added Playfair. + expect(result).toContain("inter.variable") + }) + it("should be idempotent with multiple fonts", async () => { const input = ` export default function RootLayout({ diff --git a/packages/shadcn/src/utils/updaters/update-fonts.ts b/packages/shadcn/src/utils/updaters/update-fonts.ts index 40ed07e257..b6871006ea 100644 --- a/packages/shadcn/src/utils/updaters/update-fonts.ts +++ b/packages/shadcn/src/utils/updaters/update-fonts.ts @@ -250,12 +250,25 @@ export async function transformLayoutFonts( fontUtilityClasses.push(font.font.variable.replace("--", "")) } + // Only keep one font-family class (font-sans, font-serif, font-mono) on . + // The last one in the array takes priority as it's the one being added/changed. + const fontFamilyClasses = new Set(["font-sans", "font-serif", "font-mono"]) + const lastFontFamilyClass = [...fontUtilityClasses] + .reverse() + .find((cls) => fontFamilyClasses.has(cls)) + const filteredUtilityClasses = fontUtilityClasses.filter( + (cls) => !fontFamilyClasses.has(cls) + ) + if (lastFontFamilyClass) { + filteredUtilityClasses.unshift(lastFontFamilyClass) + } + // Update html className to include font variables and utility classes. if (fontVariableNames.length > 0) { updateHtmlClassName( sourceFile, fontVariableNames, - fontUtilityClasses, + filteredUtilityClasses, config ) } @@ -403,17 +416,18 @@ function updateHtmlClassName( const hasAllUtilityClasses = fontUtilityClasses.every((cls) => exprText.includes(`"${cls}"`) ) - if (hasAllFontVars && hasAllUtilityClasses) { + // Check there are no stale font-family classes (e.g., "font-sans" when we want "font-serif"). + const staleFontFamilyClasses = ["font-sans", "font-serif", "font-mono"] + .filter((cls) => !fontUtilityClasses.includes(cls)) + .some((cls) => exprText.includes(`"${cls}"`)) + if (hasAllFontVars && hasAllUtilityClasses && !staleFontFamilyClasses) { // Already has everything, skip. continue } - // Remove existing font variables and utility classes, then add new ones. - let cleanedExpr = removeFontVariablesFromCn(exprText) - cleanedExpr = removeFontUtilityClassesFromCn( - cleanedExpr, - fontUtilityClasses - ) + // Remove existing font variables and font-family classes, then add new ones. + let cleanedExpr = removeFontVariablesFromCn(exprText, newVarExpressions) + cleanedExpr = removeFontFamilyClassesFromCn(cleanedExpr) const newExpr = insertFontVariablesIntoCn(cleanedExpr, allNewArgs) jsxExpr.replaceWithText(`{${newExpr}}`) } else if (/^\w+\.variable$/.test(exprText)) { @@ -510,18 +524,25 @@ function parseTemplateLiteralToCnArgs(templateLiteral: string) { return [...staticArgs, ...variableArgs] } -function removeFontVariablesFromCn(cnExpr: string) { - // Remove patterns like "fontName.variable" from cn() call. - return cnExpr.replace(/,?\s*\w+\.variable/g, "").replace(/cn\(\s*,/, "cn(") +function removeFontVariablesFromCn( + cnExpr: string, + variablesToRemove: string[] +) { + // Remove specific font variable expressions from cn() call. + let result = cnExpr + for (const varExpr of variablesToRemove) { + result = result + .replace(new RegExp(`,?\\s*${varExpr.replace(".", "\\.")}`, "g"), "") + .replace(/cn\(\s*,/, "cn(") + } + return result } -function removeFontUtilityClassesFromCn( - cnExpr: string, - utilityClasses: string[] -) { - // Remove font utility class strings like "font-sans", "font-serif" from cn() call. +function removeFontFamilyClassesFromCn(cnExpr: string) { + // Remove font-family class strings (font-sans, font-serif, font-mono) from cn() call. + // Does not remove other font classes like font-bold, font-semibold, etc. let result = cnExpr - for (const cls of utilityClasses) { + for (const cls of ["font-sans", "font-serif", "font-mono"]) { result = result .replace(new RegExp(`,?\\s*"${cls}"`, "g"), "") .replace(/cn\(\s*,/, "cn(")