mirror of
https://github.com/withastro/astro.git
synced 2025-03-10 23:01:26 -05:00
parent
75b1f5b877
commit
0f3419c4c1
8 changed files with 58 additions and 42 deletions
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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> }>;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue