From 141d23df7a85cf736a9b9e8c36a745ff9cee2345 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 16 Apr 2025 11:04:36 +0200 Subject: [PATCH 1/5] feat(fonts): fallback improvements --- .changeset/cold-carrots-stare.md | 5 ++ packages/astro/src/assets/fonts/metrics.ts | 17 ++----- packages/astro/src/assets/fonts/utils.ts | 48 ++++++++++++------- .../src/assets/fonts/vite-plugin-fonts.ts | 10 +++- .../test/units/assets/fonts/utils.test.js | 7 +++ 5 files changed, 55 insertions(+), 32 deletions(-) create mode 100644 .changeset/cold-carrots-stare.md diff --git a/.changeset/cold-carrots-stare.md b/.changeset/cold-carrots-stare.md new file mode 100644 index 000000000000..7066aa2131ef --- /dev/null +++ b/.changeset/cold-carrots-stare.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Improves the optimized fallback name diff --git a/packages/astro/src/assets/fonts/metrics.ts b/packages/astro/src/assets/fonts/metrics.ts index f0d9b45dffb9..2ebda5bbfec5 100644 --- a/packages/astro/src/assets/fonts/metrics.ts +++ b/packages/astro/src/assets/fonts/metrics.ts @@ -1,4 +1,5 @@ import { type Font, fromBuffer } from '@capsizecss/unpack'; +import { renderFontFace, renderFontSrc } from './utils.js'; export type FontFaceMetrics = Pick< Font, @@ -37,12 +38,6 @@ function toPercentage(value: number, fractionDigits = 4) { return `${+percentage.toFixed(fractionDigits)}%`; } -function toCSS(properties: Record, indent = 2) { - return Object.entries(properties) - .map(([key, value]) => `${' '.repeat(indent)}${key}: ${value};`) - .join('\n'); -} - export function generateFallbackFontFace( metrics: FontFaceMetrics, fallback: { @@ -79,15 +74,13 @@ export function generateFallbackFontFace( const descentOverride = Math.abs(metrics.descent) / adjustedEmSquare; const lineGapOverride = metrics.lineGap / adjustedEmSquare; - const declaration = { - 'font-family': JSON.stringify(fallbackName), - src: `local(${JSON.stringify(fallbackFontName)})`, + return renderFontFace({ + 'font-family': fallbackName, + src: renderFontSrc([{ name: fallbackFontName }]), 'size-adjust': toPercentage(sizeAdjust), 'ascent-override': toPercentage(ascentOverride), 'descent-override': toPercentage(descentOverride), 'line-gap-override': toPercentage(lineGapOverride), ...properties, - }; - - return `@font-face {\n${toCSS(declaration)}\n}\n`; + }); } diff --git a/packages/astro/src/assets/fonts/utils.ts b/packages/astro/src/assets/fonts/utils.ts index 2d8c8ef2225c..caed99925255 100644 --- a/packages/astro/src/assets/fonts/utils.ts +++ b/packages/astro/src/assets/fonts/utils.ts @@ -15,28 +15,33 @@ import type { ResolvedLocalFontFamily, } from './types.js'; -// Source: https://github.com/nuxt/fonts/blob/main/src/css/render.ts#L7-L21 -export function generateFontFace(family: string, font: unifont.FontFaceData) { - return [ - '@font-face {', - ` font-family: ${family};`, - ` src: ${renderFontSrc(font.src)};`, - ` font-display: ${font.display ?? 'swap'};`, - font.unicodeRange && ` unicode-range: ${font.unicodeRange};`, - font.weight && - ` font-weight: ${Array.isArray(font.weight) ? font.weight.join(' ') : font.weight};`, - font.style && ` font-style: ${font.style};`, - font.stretch && ` font-stretch: ${font.stretch};`, - font.featureSettings && ` font-feature-settings: ${font.featureSettings};`, - font.variationSettings && ` font-variation-settings: ${font.variationSettings};`, - `}`, - ] - .filter(Boolean) +export function toCSS(properties: Record, indent = 2) { + return Object.entries(properties) + .filter(([, value]) => Boolean(value)) + .map(([key, value]) => `${' '.repeat(indent)}${key}: ${value};`) .join('\n'); } +export function renderFontFace(properties: Record) { + return `@font-face {\n${toCSS(properties)}\n}\n`; +} + +export function generateFontFace(family: string, font: unifont.FontFaceData) { + return renderFontFace({ + 'font-family': family, + src: renderFontSrc(font.src), + 'font-display': font.display ?? 'swap', + 'unicode-range': font.unicodeRange?.join(','), + 'font-weight': Array.isArray(font.weight) ? font.weight.join(' ') : font.weight?.toString(), + 'font-style': font.style, + 'font-stretch': font.stretch, + 'font-feature-settings': font.featureSettings, + 'font-variation-settings': font.variationSettings, + }); +} + // Source: https://github.com/nuxt/fonts/blob/main/src/css/render.ts#L68-L81 -function renderFontSrc(sources: Exclude[]) { +export function renderFontSrc(sources: Exclude[]) { return sources .map((src) => { if ('url' in src) { @@ -53,6 +58,12 @@ function renderFontSrc(sources: Exclude m.name), ...fallbacks])]; for (const { font, name } of localFontsMappings) { + // TODO: forward some properties? css += metrics.generateFontFace(foundMetrics, { font, name }); } diff --git a/packages/astro/src/assets/fonts/vite-plugin-fonts.ts b/packages/astro/src/assets/fonts/vite-plugin-fonts.ts index 57a7d1e0a420..e6ea231e67cd 100644 --- a/packages/astro/src/assets/fonts/vite-plugin-fonts.ts +++ b/packages/astro/src/assets/fonts/vite-plugin-fonts.ts @@ -23,7 +23,13 @@ import { loadFonts } from './load.js'; import { generateFallbackFontFace, readMetrics } from './metrics.js'; import type { ResolveMod } from './providers/utils.js'; import type { PreloadData, ResolvedFontFamily } from './types.js'; -import { cache, extractFontType, resolveFontFamily, sortObjectByKey } from './utils.js'; +import { + cache, + extractFontType, + resolveFontFamily, + sortObjectByKey, + withoutQuotes, +} from './utils.js'; interface Options { settings: AstroSettings; @@ -116,7 +122,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { root: settings.config.root, resolveMod, generateNameWithHash: (_family) => - `${_family.name}-${h64ToString(JSON.stringify(sortObjectByKey(_family)))}`, + `${withoutQuotes(_family.name)}-${h64ToString(JSON.stringify(sortObjectByKey(_family)))}`, }), ); } diff --git a/packages/astro/test/units/assets/fonts/utils.test.js b/packages/astro/test/units/assets/fonts/utils.test.js index 68dc95e2954f..4be47d096130 100644 --- a/packages/astro/test/units/assets/fonts/utils.test.js +++ b/packages/astro/test/units/assets/fonts/utils.test.js @@ -11,6 +11,7 @@ import { isGenericFontFamily, proxyURL, resolveFontFamily, + toCSS, } from '../../../../dist/assets/fonts/utils.js'; /** @@ -537,4 +538,10 @@ describe('fonts utils', () => { ]); }); }); + + it('toCSS', () => { + assert.deepStrictEqual(toCSS({}, 0), ''); + assert.deepStrictEqual(toCSS({ foo: 'bar' }, 0), 'foo: bar;'); + assert.deepStrictEqual(toCSS({ foo: 'bar', bar: undefined }, 0), 'foo: bar;'); + }); }); From 676be699ccfc6bcf419857693438205ab36883e8 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 16 Apr 2025 12:16:51 +0200 Subject: [PATCH 2/5] feat: improve fallbacks generation --- .changeset/stupid-wasps-walk.md | 5 ++ packages/astro/src/assets/fonts/constants.ts | 59 +++++++++++++++++++- packages/astro/src/assets/fonts/metrics.ts | 39 +++++-------- packages/astro/src/assets/fonts/utils.ts | 11 +++- 4 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 .changeset/stupid-wasps-walk.md diff --git a/.changeset/stupid-wasps-walk.md b/.changeset/stupid-wasps-walk.md new file mode 100644 index 000000000000..dd2f322554c1 --- /dev/null +++ b/.changeset/stupid-wasps-walk.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Improves the quality of optimized fallbacks diff --git a/packages/astro/src/assets/fonts/constants.ts b/packages/astro/src/assets/fonts/constants.ts index c12d31fbd9b6..2f9d6669948b 100644 --- a/packages/astro/src/assets/fonts/constants.ts +++ b/packages/astro/src/assets/fonts/constants.ts @@ -1,3 +1,4 @@ +import type { FontFaceMetrics } from './metrics.js'; import type { ResolvedRemoteFontFamily } from './types.js'; export const LOCAL_PROVIDER_NAME = 'local'; @@ -20,8 +21,62 @@ export const CACHE_DIR = './fonts/'; export const FONT_TYPES = ['woff2', 'woff', 'otf', 'ttf', 'eot'] as const; +// Extracted from https://raw.githubusercontent.com/seek-oss/capsize/refs/heads/master/packages/metrics/src/entireMetricsCollection.json +export const SYSTEM_METRICS = { + 'Times New Roman': { + ascent: 1825, + descent: -443, + lineGap: 87, + unitsPerEm: 2048, + xWidthAvg: 832, + }, + Arial: { + ascent: 1854, + descent: -434, + lineGap: 67, + unitsPerEm: 2048, + xWidthAvg: 913, + }, + 'Courier New': { + ascent: 1705, + descent: -615, + lineGap: 0, + unitsPerEm: 2048, + xWidthAvg: 1229, + }, + BlinkMacSystemFont: { + ascent: 1980, + descent: -432, + lineGap: 0, + unitsPerEm: 2048, + xWidthAvg: 853, + }, + 'Segoe UI': { + ascent: 2210, + descent: -514, + lineGap: 0, + unitsPerEm: 2048, + xWidthAvg: 908, + }, + Roboto: { + ascent: 1900, + descent: -500, + lineGap: 0, + unitsPerEm: 2048, + xWidthAvg: 911, + }, + 'Helvetica Neue': { + ascent: 952, + descent: -213, + lineGap: 28, + unitsPerEm: 1000, + xWidthAvg: 450, + }, +} satisfies Record; + +// Keep up to date with system metrics data // Source: https://github.com/nuxt/fonts/blob/3a3eb6dfecc472242b3011b25f3fcbae237d0acc/src/module.ts#L55-L75 -export const DEFAULT_FALLBACKS: Record> = { +export const DEFAULT_FALLBACKS = { serif: ['Times New Roman'], 'sans-serif': ['Arial'], monospace: ['Courier New'], @@ -35,6 +90,6 @@ export const DEFAULT_FALLBACKS: Record> = { emoji: [], math: [], fangsong: [], -}; +} as const satisfies Record>; export const FONTS_TYPES_FILE = 'fonts.d.ts'; diff --git a/packages/astro/src/assets/fonts/metrics.ts b/packages/astro/src/assets/fonts/metrics.ts index 2ebda5bbfec5..0ab6f3fa424b 100644 --- a/packages/astro/src/assets/fonts/metrics.ts +++ b/packages/astro/src/assets/fonts/metrics.ts @@ -38,34 +38,25 @@ function toPercentage(value: number, fractionDigits = 4) { return `${+percentage.toFixed(fractionDigits)}%`; } -export function generateFallbackFontFace( - metrics: FontFaceMetrics, - fallback: { - name: string; - font: string; - metrics?: FontFaceMetrics; - [key: string]: any; - }, -) { - const { - name: fallbackName, - font: fallbackFontName, - metrics: fallbackMetrics, - ...properties - } = fallback; - +export function generateFallbackFontFace({ + metrics, + fallbackMetrics, + name: fallbackName, + font: fallbackFontName, + properties = {}, +}: { + metrics: FontFaceMetrics; + fallbackMetrics: FontFaceMetrics; + name: string; + font: string; + properties?: Record; +}) { // Credits to: https://github.com/seek-oss/capsize/blob/master/packages/core/src/createFontStack.ts // Calculate size adjust const preferredFontXAvgRatio = metrics.xWidthAvg / metrics.unitsPerEm; - const fallbackFontXAvgRatio = fallbackMetrics - ? fallbackMetrics.xWidthAvg / fallbackMetrics.unitsPerEm - : 1; - - const sizeAdjust = - fallbackMetrics && preferredFontXAvgRatio && fallbackFontXAvgRatio - ? preferredFontXAvgRatio / fallbackFontXAvgRatio - : 1; + const fallbackFontXAvgRatio = fallbackMetrics.xWidthAvg / fallbackMetrics.unitsPerEm; + const sizeAdjust = preferredFontXAvgRatio / fallbackFontXAvgRatio; const adjustedEmSquare = metrics.unitsPerEm * sizeAdjust; diff --git a/packages/astro/src/assets/fonts/utils.ts b/packages/astro/src/assets/fonts/utils.ts index a43b09b16d9a..e3ab2c53d750 100644 --- a/packages/astro/src/assets/fonts/utils.ts +++ b/packages/astro/src/assets/fonts/utils.ts @@ -4,7 +4,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; import type * as unifont from 'unifont'; import type { Storage } from 'unstorage'; import { AstroError, AstroErrorData } from '../../core/errors/index.js'; -import { DEFAULT_FALLBACKS, FONT_TYPES, LOCAL_PROVIDER_NAME } from './constants.js'; +import { DEFAULT_FALLBACKS, FONT_TYPES, LOCAL_PROVIDER_NAME, SYSTEM_METRICS } from './constants.js'; import type { FontFaceMetrics, generateFallbackFontFace } from './metrics.js'; import { type ResolveProviderOptions, resolveProvider } from './providers/utils.js'; import type { @@ -216,8 +216,13 @@ export async function generateFallbacksCSS({ fallbacks = [...new Set([...localFontsMappings.map((m) => m.name), ...fallbacks])]; for (const { font, name } of localFontsMappings) { - // TODO: forward some properties? - css += metrics.generateFontFace(foundMetrics, { font, name }); + css += metrics.generateFontFace({ + metrics: foundMetrics, + fallbackMetrics: SYSTEM_METRICS[font], + font, + name, + // TODO: forward some properties? + }); } return { css, fallbacks }; From 70a0172165a8ded86b63d0eb89bd9925887dc962 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 16 Apr 2025 12:19:26 +0200 Subject: [PATCH 3/5] chore: comments --- packages/astro/src/assets/fonts/constants.ts | 1 - packages/astro/src/assets/fonts/utils.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/astro/src/assets/fonts/constants.ts b/packages/astro/src/assets/fonts/constants.ts index 2f9d6669948b..9e22d1da43a6 100644 --- a/packages/astro/src/assets/fonts/constants.ts +++ b/packages/astro/src/assets/fonts/constants.ts @@ -74,7 +74,6 @@ export const SYSTEM_METRICS = { }, } satisfies Record; -// Keep up to date with system metrics data // Source: https://github.com/nuxt/fonts/blob/3a3eb6dfecc472242b3011b25f3fcbae237d0acc/src/module.ts#L55-L75 export const DEFAULT_FALLBACKS = { serif: ['Times New Roman'], diff --git a/packages/astro/src/assets/fonts/utils.ts b/packages/astro/src/assets/fonts/utils.ts index e3ab2c53d750..8d5fd4ee4179 100644 --- a/packages/astro/src/assets/fonts/utils.ts +++ b/packages/astro/src/assets/fonts/utils.ts @@ -221,7 +221,7 @@ export async function generateFallbacksCSS({ fallbackMetrics: SYSTEM_METRICS[font], font, name, - // TODO: forward some properties? + // TODO: forward some properties once we generate one fallback per font face data }); } From d27d26f09fe5e29ef2c8f8a5c97477431f8c5464 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 16 Apr 2025 12:51:31 +0200 Subject: [PATCH 4/5] Update packages/astro/src/assets/fonts/utils.ts Co-authored-by: Emanuele Stoppa --- packages/astro/src/assets/fonts/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/assets/fonts/utils.ts b/packages/astro/src/assets/fonts/utils.ts index 8d5fd4ee4179..6c76406c7096 100644 --- a/packages/astro/src/assets/fonts/utils.ts +++ b/packages/astro/src/assets/fonts/utils.ts @@ -23,7 +23,7 @@ export function toCSS(properties: Record, indent = 2 } export function renderFontFace(properties: Record) { - return `@font-face {\n${toCSS(properties)}\n}\n`; + return `@font-face {\n\t${toCSS(properties)}\n}\n`; } export function generateFontFace(family: string, font: unifont.FontFaceData) { From 16b1478d6af5feb8aa31a48f0df242ce94c2b498 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 16 Apr 2025 12:59:05 +0200 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- .changeset/cold-carrots-stare.md | 2 +- .changeset/stupid-wasps-walk.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/cold-carrots-stare.md b/.changeset/cold-carrots-stare.md index 7066aa2131ef..2920f25124ec 100644 --- a/.changeset/cold-carrots-stare.md +++ b/.changeset/cold-carrots-stare.md @@ -2,4 +2,4 @@ 'astro': patch --- -Improves the optimized fallback name +Improves the optimized fallback name generated by the experimental Fonts API diff --git a/.changeset/stupid-wasps-walk.md b/.changeset/stupid-wasps-walk.md index dd2f322554c1..971a9dc39277 100644 --- a/.changeset/stupid-wasps-walk.md +++ b/.changeset/stupid-wasps-walk.md @@ -2,4 +2,4 @@ 'astro': patch --- -Improves the quality of optimized fallbacks +Improves the quality of optimized fallbacks generated by the experimental Fonts API