fix: issue with fonts

This commit is contained in:
shadcn
2026-02-27 21:52:51 +04:00
parent 7e766f4714
commit fd9c64f416
2 changed files with 83 additions and 19 deletions

View File

@@ -159,7 +159,7 @@ export default function RootLayout({
children: React.ReactNode
}) {
return (
<html lang="en" className={cn("font-sans", "font-mono", inter.variable, jetbrainsMono.variable)}>
<html lang="en" className={cn("font-mono", inter.variable, jetbrainsMono.variable)}>
<body>{children}</body>
</html>
)
@@ -962,7 +962,7 @@ export default function RootLayout({
children: React.ReactNode
}) {
return (
<html lang="en" className={cn("font-sans", "font-serif", inter.variable, lora.variable)}>
<html lang="en" className={cn("font-serif", inter.variable, lora.variable)}>
<body>{children}</body>
</html>
)
@@ -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 (
<html lang="en" className={cn("font-sans", inter.variable)}>
<body>{children}</body>
</html>
)
}
`
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({

View File

@@ -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 <html>.
// 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(")