From 0f3419c4c127b2b8dd286f6747d8242ef044bdfe Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Mon, 10 Mar 2025 14:06:50 +0100 Subject: [PATCH] feat(fonts): as prop (#13366) * feat(fonts): as prop * chore: comment --- packages/astro/src/assets/fonts/config.ts | 6 ++++ packages/astro/src/assets/fonts/load.ts | 16 ++++++---- packages/astro/src/assets/fonts/types.ts | 14 ++++---- packages/astro/src/assets/fonts/utils.ts | 17 ++++++---- .../src/assets/fonts/vite-plugin-fonts.ts | 1 + packages/astro/src/core/config/schema.ts | 11 +++---- .../test/units/assets/fonts/load.test.js | 3 +- .../test/units/assets/fonts/utils.test.js | 32 +++++++++---------- 8 files changed, 58 insertions(+), 42 deletions(-) diff --git a/packages/astro/src/assets/fonts/config.ts b/packages/astro/src/assets/fonts/config.ts index fd11955eaf..46a7b18fd9 100644 --- a/packages/astro/src/assets/fonts/config.ts +++ b/packages/astro/src/assets/fonts/config.ts @@ -24,3 +24,9 @@ export const resolveFontOptionsSchema = z.object({ .transform((arr) => dedupe(arr)) .optional(), }); + +export const fontFamilyAttributesSchema = z.object({ + name: z.string(), + provider: z.string(), + as: z.string().optional(), +}); diff --git a/packages/astro/src/assets/fonts/load.ts b/packages/astro/src/assets/fonts/load.ts index d4ec51a0b0..51ad732e9d 100644 --- a/packages/astro/src/assets/fonts/load.ts +++ b/packages/astro/src/assets/fonts/load.ts @@ -1,7 +1,13 @@ import { readFileSync } from 'node:fs'; import { resolveLocalFont } from './providers/local.js'; import { resolveProviders, type ResolveMod } from './providers/utils.js'; -import { generateFallbacksCSS, generateFontFace, proxyURL, type ProxyURLOptions } from './utils.js'; +import { + generateFallbacksCSS, + generateFontFace, + getFamilyName, + proxyURL, + type ProxyURLOptions, +} from './utils.js'; import * as unifont from 'unifont'; import { AstroError, AstroErrorData } from '../../core/errors/index.js'; import { DEFAULTS, LOCAL_PROVIDER_NAME } from './constants.js'; @@ -132,23 +138,21 @@ export async function loadFonts({ .filter(Boolean); const fallbackData = await generateFallbacksCSS({ - family: family.name, + family, fallbacks: family.fallbacks ?? [], fontURL: urls.at(0) ?? null, getMetricsForFamily, generateFontFace: generateFallbackFontFace, }); - // TODO: support family.as - const cssVarValues = [family.name]; + const cssVarValues = [getFamilyName(family)]; if (fallbackData) { css += fallbackData.css; cssVarValues.push(...fallbackData.fallbacks); } - // TODO: support family.as - css += `:root { --astro-font-${generateCSSVariableName(family.name)}: ${cssVarValues.join(', ')}; }`; + css += `:root { --astro-font-${generateCSSVariableName(getFamilyName(family))}: ${cssVarValues.join(', ')}; }`; resolvedMap.set(family.name, { preloadData, css }); } diff --git a/packages/astro/src/assets/fonts/types.ts b/packages/astro/src/assets/fonts/types.ts index 631cd51a1b..ebcdc59248 100644 --- a/packages/astro/src/assets/fonts/types.ts +++ b/packages/astro/src/assets/fonts/types.ts @@ -6,7 +6,9 @@ import type { FONT_TYPES, } from './constants.js'; import type * as unifont from 'unifont'; -import type { resolveFontOptionsSchema } from './config.js'; +import type { fontFamilyAttributesSchema, resolveFontOptionsSchema } from './config.js'; + +// TODO: jsdoc for everything, most of those end up in the public AstroConfig type export interface FontProvider { name: TName; @@ -22,13 +24,11 @@ export interface ResolvedFontProvider { export type ResolveFontOptions = z.output; -// TODO: support optional as prop -interface FontFamilyAttributes extends Partial { - name: string; - provider: string; -} +export interface FontFamilyAttributes + extends z.infer, + Partial {} -export interface LocalFontFamily extends Pick { +export interface LocalFontFamily extends Pick { provider: LocalProviderName; src: Array> & { paths: Array }>; } diff --git a/packages/astro/src/assets/fonts/utils.ts b/packages/astro/src/assets/fonts/utils.ts index 8b86134eba..c1034ab6b8 100644 --- a/packages/astro/src/assets/fonts/utils.ts +++ b/packages/astro/src/assets/fonts/utils.ts @@ -1,5 +1,5 @@ import type * as unifont from 'unifont'; -import type { FontType } from './types.js'; +import type { FontFamilyAttributes, FontType } from './types.js'; import { extname } from 'node:path'; import { DEFAULT_FALLBACKS, FONT_TYPES } from './constants.js'; import type { Storage } from 'unstorage'; @@ -132,8 +132,10 @@ export async function generateFallbacksCSS({ // eslint-disable-next-line @typescript-eslint/no-shadow generateFontFace, }: { - /** The family name */ - family: string; + family: { + name: string; + as?: string; + }; /** The family fallbacks */ fallbacks: Array; /** A remote url or local filepath to a font file. Used if metrics can't be resolved purely from the family name */ @@ -163,7 +165,7 @@ export async function generateFallbacksCSS({ return { css, fallbacks }; } - const metrics = await getMetricsForFamily(family, fontURL); + const metrics = await getMetricsForFamily(family.name, fontURL); if (!metrics) { // If there are no metrics, we can't generate useful fallbacks return { css, fallbacks }; @@ -171,8 +173,7 @@ export async function generateFallbacksCSS({ const localFontsMappings = localFonts.map((font) => ({ font, - // TODO: support family.as - name: `"${family} fallback: ${font}"`, + name: `"${getFamilyName(family)} fallback: ${font}"`, })); // We prepend the fallbacks with the local fonts and we dedupe in case a local font is already provided @@ -247,3 +248,7 @@ export function kebab(value: string) { .replace(TRIM_DASHES_REGEX, '') // Trim leading/trailing dashes .toLowerCase(); } + +export function getFamilyName(family: Pick): string { + return family.as ?? family.name; +} diff --git a/packages/astro/src/assets/fonts/vite-plugin-fonts.ts b/packages/astro/src/assets/fonts/vite-plugin-fonts.ts index a88a8442bd..95749d74b7 100644 --- a/packages/astro/src/assets/fonts/vite-plugin-fonts.ts +++ b/packages/astro/src/assets/fonts/vite-plugin-fonts.ts @@ -113,6 +113,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { }, generateFontFace: fontaine.generateFontFace, log: (message) => logger.info('assets', message), + // TODO: warn if characters are stripped out OR show the list of all generated css variables generateCSSVariableName: (name) => kebab(name), }); } diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 4ce3af6708..0306cd342c 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -19,7 +19,8 @@ import { GOOGLE_PROVIDER_NAME, LOCAL_PROVIDER_NAME, } from '../../assets/fonts/constants.js'; -import { resolveFontOptionsSchema } from '../../assets/fonts/config.js'; +import { fontFamilyAttributesSchema, resolveFontOptionsSchema } from '../../assets/fonts/config.js'; +import { getFamilyName } from '../../assets/fonts/utils.js'; // The below types are required boilerplate to workaround a Zod issue since v3.21.2. Since that version, // Zod's compiled TypeScript would "simplify" certain values to their base representation, causing references @@ -624,7 +625,6 @@ export const AstroConfigSchema = z.object({ .strict(), ) .optional(), - // TODO: support family.as families: z .array( z @@ -633,7 +633,6 @@ export const AstroConfigSchema = z.object({ z .object({ provider: z.literal(LOCAL_PROVIDER_NAME), - name: z.string(), src: z.array( z .object({ @@ -643,13 +642,14 @@ export const AstroConfigSchema = z.object({ .strict(), ), }) + .merge(fontFamilyAttributesSchema.omit({ provider: true })) .merge(resolveFontOptionsSchema.pick({ fallbacks: true }).partial()) .strict(), z .object({ provider: z.string().optional().default(GOOGLE_PROVIDER_NAME), - name: z.string(), }) + .merge(fontFamilyAttributesSchema.omit({ provider: true })) .merge(resolveFontOptionsSchema.partial()) .strict(), ]) @@ -661,9 +661,8 @@ export const AstroConfigSchema = z.object({ ) // We dedupe families .transform((families) => [ - // TODO: support family.as // TODO: warn if some families are being overriden and how to resolve the issue - ...new Map(families.map((family) => [family.name, family])).values(), + ...new Map(families.map((family) => [getFamilyName(family), family])).values(), ]), }) .strict() diff --git a/packages/astro/test/units/assets/fonts/load.test.js b/packages/astro/test/units/assets/fonts/load.test.js index 673709cce5..efb5577469 100644 --- a/packages/astro/test/units/assets/fonts/load.test.js +++ b/packages/astro/test/units/assets/fonts/load.test.js @@ -57,6 +57,7 @@ it('loadFonts()', async () => { // we do weird typings internally for "reasons" (provider is typed as "local" | "custom") but this is valid provider: /** @type {any} */ ('google'), fallbacks: ['sans-serif'], + as: 'Custom' }, ], storage, @@ -93,7 +94,7 @@ it('loadFonts()', async () => { assert.equal( resolvedMap .get('Roboto') - .css.includes(':root { --astro-font-Roboto: Roboto, "Roboto fallback: Arial", sans-serif; }'), + .css.includes(':root { --astro-font-Custom: Custom, "Custom fallback: Arial", sans-serif; }'), true, ); }); diff --git a/packages/astro/test/units/assets/fonts/utils.test.js b/packages/astro/test/units/assets/fonts/utils.test.js index bddeac266e..f31f7a4ff7 100644 --- a/packages/astro/test/units/assets/fonts/utils.test.js +++ b/packages/astro/test/units/assets/fonts/utils.test.js @@ -179,7 +179,7 @@ describe('fonts utils', () => { it('should return null if there are no fallbacks', async () => { assert.equal( await generateFallbacksCSS({ - family: 'Roboto', + family: { name: 'Roboto' }, fallbacks: [], fontURL: null, getMetricsForFamily: async () => null, @@ -192,7 +192,7 @@ describe('fonts utils', () => { it('should return fallbacks if there are no metrics', async () => { assert.deepStrictEqual( await generateFallbacksCSS({ - family: 'Roboto', + family: { name: 'Roboto' }, fallbacks: ['foo'], fontURL: null, getMetricsForFamily: async () => null, @@ -208,7 +208,7 @@ describe('fonts utils', () => { it('should return fallbacks if there are metrics but no generic font family', async () => { assert.deepStrictEqual( await generateFallbacksCSS({ - family: 'Roboto', + family: { name: 'Roboto' }, fallbacks: ['foo'], fontURL: null, getMetricsForFamily: async () => ({ @@ -230,7 +230,7 @@ describe('fonts utils', () => { it('shold return fallbacks if the generic font family does not have fonts associated', async () => { assert.deepStrictEqual( await generateFallbacksCSS({ - family: 'Roboto', + family: { name: 'Roboto' }, fallbacks: ['emoji'], fontURL: null, getMetricsForFamily: async () => ({ @@ -252,7 +252,7 @@ describe('fonts utils', () => { it('resolves fallbacks correctly', async () => { assert.deepStrictEqual( await generateFallbacksCSS({ - family: 'Roboto', + family: { name: 'Roboto' }, fallbacks: ['foo', 'bar'], fontURL: null, getMetricsForFamily: async () => ({ @@ -271,7 +271,7 @@ describe('fonts utils', () => { ); assert.deepStrictEqual( await generateFallbacksCSS({ - family: 'Roboto', + family: { name: 'Roboto' }, fallbacks: ['sans-serif', 'foo'], fontURL: null, getMetricsForFamily: async () => ({ @@ -290,7 +290,7 @@ describe('fonts utils', () => { ); assert.deepStrictEqual( await generateFallbacksCSS({ - family: 'Roboto', + family: { name: 'Roboto', as: 'Custom' }, fallbacks: ['foo', 'sans-serif'], fontURL: null, getMetricsForFamily: async () => ({ @@ -303,19 +303,19 @@ describe('fonts utils', () => { generateFontFace: (_metrics, fallback) => `[${fallback.font},${fallback.name}]`, }), { - css: `[Arial,"Roboto fallback: Arial"]`, - fallbacks: ['"Roboto fallback: Arial"', 'foo', 'sans-serif'], + css: `[Arial,"Custom fallback: Arial"]`, + fallbacks: ['"Custom fallback: Arial"', 'foo', 'sans-serif'], }, ); }); }); it('kebab()', () => { - assert.equal(kebab('valid'), 'valid') - assert.equal(kebab('camelCase'), 'camel-case') - assert.equal(kebab('PascalCase'), 'pascal-case') - assert.equal(kebab('snake_case'), 'snake-case') - assert.equal(kebab(' trim- '), 'trim') - assert.equal(kebab('de--dupe'), 'de-dupe') - }) + assert.equal(kebab('valid'), 'valid'); + assert.equal(kebab('camelCase'), 'camel-case'); + assert.equal(kebab('PascalCase'), 'pascal-case'); + assert.equal(kebab('snake_case'), 'snake-case'); + assert.equal(kebab(' trim- '), 'trim'); + assert.equal(kebab('de--dupe'), 'de-dupe'); + }); });