0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-10 23:01:26 -05:00

feat(fonts): css vars (#13362)

* feat(fonts): css vars

* feat: tests and fix

* chore: add todos
This commit is contained in:
Florian Lefebvre 2025-03-05 15:30:55 +01:00 committed by GitHub
parent f62a7b73f6
commit 75b1f5b877
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 64 additions and 15 deletions

View file

@ -6,6 +6,7 @@ function dedupe<T>(arr: Array<T>): Array<T> {
export const resolveFontOptionsSchema = z.object({
weights: z
// TODO: support numbers
.array(z.string())
.nonempty()
.transform((arr) => dedupe(arr)),

View file

@ -20,6 +20,7 @@ interface Options
resolvedMap: Map<string, { preloadData: PreloadData; css: string }>;
resolveMod: ResolveMod;
log: (message: string) => void;
generateCSSVariableName: (name: string) => string;
}
export async function loadFonts({
@ -35,6 +36,7 @@ export async function loadFonts({
generateFontFace: generateFallbackFontFace,
getMetricsForFamily,
log,
generateCSSVariableName,
}: Options): Promise<void> {
const resolved = await resolveProviders({
root,
@ -137,11 +139,17 @@ export async function loadFonts({
generateFontFace: generateFallbackFontFace,
});
// TODO: support family.as
const cssVarValues = [family.name];
if (fallbackData) {
css += fallbackData.css;
// TODO: generate css var
cssVarValues.push(...fallbackData.fallbacks);
}
// TODO: support family.as
css += `:root { --astro-font-${generateCSSVariableName(family.name)}: ${cssVarValues.join(', ')}; }`;
resolvedMap.set(family.name, { preloadData, css });
}
log('Fonts initialized');

View file

@ -169,15 +169,17 @@ export async function generateFallbacksCSS({
return { css, fallbacks };
}
// We prepend the fallbacks with the local fonts and we dedupe in case a local font is already provided
fallbacks = [...new Set([...localFonts, ...fallbacks])];
const localFontsMappings = localFonts.map((font) => ({
font,
// TODO: support family.as
name: `"${family} fallback: ${font}"`,
}));
for (const fallback of localFonts) {
css += generateFontFace(metrics, {
font: fallback,
// TODO: support family.as
name: `${family} fallback: ${fallback}`,
});
// We prepend the fallbacks with the local fonts and we dedupe in case a local font is already provided
fallbacks = [...new Set([...localFontsMappings.map((m) => m.name), ...fallbacks])];
for (const { font, name } of localFontsMappings) {
css += generateFontFace(metrics, { font, name });
}
return { css, fallbacks };
@ -233,3 +235,15 @@ export function createLogManager(logger: Logger) {
},
};
}
const CAMEL_CASE_REGEX = /([a-z])([A-Z])/g;
const NON_ALPHANUMERIC_REGEX = /[^a-zA-Z0-9]+/g;
const TRIM_DASHES_REGEX = /^-+|-+$/g;
export function kebab(value: string) {
return value
.replace(CAMEL_CASE_REGEX, '$1-$2') // Handle camelCase
.replace(NON_ALPHANUMERIC_REGEX, '-') // Replace non-alphanumeric characters with dashes
.replace(TRIM_DASHES_REGEX, '') // Trim leading/trailing dashes
.toLowerCase();
}

View file

@ -6,7 +6,7 @@ import xxhash from 'xxhash-wasm';
import { isAbsolute } from 'node:path';
import { getClientOutputDirectory } from '../../prerender/utils.js';
import { mkdirSync, writeFileSync } from 'node:fs';
import { cache, createLogManager, extractFontType } from './utils.js';
import { cache, createLogManager, extractFontType, kebab } from './utils.js';
import {
VIRTUAL_MODULE_ID,
RESOLVED_VIRTUAL_MODULE_ID,
@ -113,6 +113,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
},
generateFontFace: fontaine.generateFontFace,
log: (message) => logger.info('assets', message),
generateCSSVariableName: (name) => kebab(name),
});
}

View file

@ -662,6 +662,7 @@ 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(),
]),
})

View file

@ -54,8 +54,9 @@ it('loadFonts()', async () => {
families: [
{
name: 'Roboto',
// @ts-expect-error we do weird typings internally for "reasons" (provider is typed as "local" | "custom") but this is valid
provider: 'google',
// we do weird typings internally for "reasons" (provider is typed as "local" | "custom") but this is valid
provider: /** @type {any} */ ('google'),
fallbacks: ['sans-serif'],
},
],
storage,
@ -68,11 +69,18 @@ it('loadFonts()', async () => {
return await import(id);
},
hashString: (v) => Buffer.from(v).toString('base64'),
getMetricsForFamily: async () => null,
getMetricsForFamily: async () => ({
ascent: 0,
descent: 0,
lineGap: 0,
unitsPerEm: 0,
xWidthAvg: 0,
}),
generateFontFace: () => '',
log: (message) => {
logs.push(message);
},
generateCSSVariableName: (name) => name,
});
assert.equal(
@ -82,4 +90,10 @@ it('loadFonts()', async () => {
assert.equal(Array.from(hashToUrlMap.keys()).length > 0, true);
assert.deepStrictEqual(Array.from(resolvedMap.keys()), ['Roboto']);
assert.deepStrictEqual(logs, ['Fonts initialized']);
assert.equal(
resolvedMap
.get('Roboto')
.css.includes(':root { --astro-font-Roboto: Roboto, "Roboto fallback: Arial", sans-serif; }'),
true,
);
});

View file

@ -8,6 +8,7 @@ import {
proxyURL,
isGenericFontFamily,
generateFallbacksCSS,
kebab,
} from '../../../../dist/assets/fonts/utils.js';
function createSpyCache() {
@ -302,10 +303,19 @@ describe('fonts utils', () => {
generateFontFace: (_metrics, fallback) => `[${fallback.font},${fallback.name}]`,
}),
{
css: `[Arial,Roboto fallback: Arial]`,
fallbacks: ['Arial', 'foo', 'sans-serif'],
css: `[Arial,"Roboto fallback: Arial"]`,
fallbacks: ['"Roboto 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')
})
});