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

feat(fonts): as prop (#13366)

* feat(fonts): as prop

* chore: comment
This commit is contained in:
Florian Lefebvre 2025-03-10 14:06:50 +01:00 committed by GitHub
parent 75b1f5b877
commit 0f3419c4c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 58 additions and 42 deletions

View file

@ -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(),
});

View file

@ -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 });
}

View file

@ -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<TName extends string> {
name: TName;
@ -22,13 +24,11 @@ export interface ResolvedFontProvider {
export type ResolveFontOptions = z.output<typeof resolveFontOptionsSchema>;
// TODO: support optional as prop
interface FontFamilyAttributes extends Partial<ResolveFontOptions> {
name: string;
provider: string;
}
export interface FontFamilyAttributes
extends z.infer<typeof fontFamilyAttributesSchema>,
Partial<ResolveFontOptions> {}
export interface LocalFontFamily extends Pick<FontFamilyAttributes, 'name' | 'fallbacks'> {
export interface LocalFontFamily extends Pick<FontFamilyAttributes, 'name' | 'fallbacks' | 'as'> {
provider: LocalProviderName;
src: Array<Partial<Omit<ResolveFontOptions, 'fallbacks'>> & { paths: Array<string> }>;
}

View file

@ -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<string>;
/** 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<FontFamilyAttributes, 'name' | 'as'>): string {
return family.as ?? family.name;
}

View file

@ -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),
});
}

View file

@ -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()

View file

@ -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,
);
});

View file

@ -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');
});
});