Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/tricky-papayas-stand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'astro': patch
---

Allows inferring `weight` and `style` when using the local provider of the experimental fonts API

If you want Astro to infer those properties directly from your local font files, leave them undefined:

```js
{
// No weight specified: infer
style: 'normal' // Do not infer
}
```
1 change: 1 addition & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
"esbuild": "^0.25.0",
"estree-walker": "^3.0.3",
"flattie": "^1.1.1",
"fontace": "~0.3.0",
"github-slugger": "^2.0.0",
"html-escaper": "3.0.3",
"http-cache-semantics": "^4.1.1",
Expand Down
19 changes: 12 additions & 7 deletions packages/astro/src/assets/fonts/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { z } from 'zod';
import { LOCAL_PROVIDER_NAME } from './constants.js';

const weightSchema = z.union([z.string(), z.number()]);
const styleSchema = z.enum(['normal', 'italic', 'oblique']);
export const styleSchema = z.enum(['normal', 'italic', 'oblique']);
const unicodeRangeSchema = z.array(z.string()).nonempty();

const familyPropertiesSchema = z.object({
/**
Expand All @@ -12,21 +13,17 @@ const familyPropertiesSchema = z.object({
* weight: "100 900"
* ```
*/
weight: weightSchema,
weight: weightSchema.optional(),
/**
* A [font style](https://developer.mozilla.org/en-US/docs/Web/CSS/font-style).
*/
style: styleSchema,
style: styleSchema.optional(),
/**
* @default `"swap"`
*
* A [font display](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display).
*/
display: z.enum(['auto', 'block', 'swap', 'fallback', 'optional']).optional(),
/**
* A [unicode range](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range).
*/
unicodeRange: z.array(z.string()).nonempty().optional(),
/**
* A [font stretch](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-stretch).
*/
Expand Down Expand Up @@ -122,6 +119,10 @@ export const localFontFamilySchema = requiredFamilyAttributesSchema
]),
)
.nonempty(),
/**
* A [unicode range](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range).
*/
unicodeRange: unicodeRangeSchema.optional(),
// TODO: find a way to support subsets (through fontkit?)
})
.strict(),
Expand Down Expand Up @@ -168,6 +169,10 @@ export const remoteFontFamilySchema = requiredFamilyAttributesSchema
* An array of [font subsets](https://knaap.dev/posts/font-subsetting/):
*/
subsets: z.array(z.string()).nonempty().optional(),
/**
* A [unicode range](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range).
*/
unicodeRange: unicodeRangeSchema.optional(),
}),
)
.strict();
11 changes: 10 additions & 1 deletion packages/astro/src/assets/fonts/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
FontType,
PreloadData,
ResolvedFontProvider,
Style,
} from './types.js';
import type { FontFaceMetrics, GenericFallbackName } from './types.js';

Expand Down Expand Up @@ -42,7 +43,8 @@ export type ErrorHandlerInput =
>
| SingleErrorInput<'unknown-fs-error', {}>
| SingleErrorInput<'cannot-fetch-font-file', { url: string }>
| SingleErrorInput<'cannot-extract-font-type', { url: string }>;
| SingleErrorInput<'cannot-extract-font-type', { url: string }>
| SingleErrorInput<'cannot-extract-data', { family: string; url: string }>;

export interface ErrorHandler {
handle: (input: ErrorHandlerInput) => Error;
Expand Down Expand Up @@ -100,3 +102,10 @@ export interface FontFetcher {
export interface FontTypeExtractor {
extract: (url: string) => FontType;
}

export interface FontFileReader {
extract: (input: { family: string; url: string }) => {
weight: string;
style: Style;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ function getProps(input: ErrorHandlerInput): ConstructorParameters<typeof AstroE
...AstroErrorData.CannotExtractFontType,
message: AstroErrorData.CannotExtractFontType.message(input.data.url),
};
} else if (input.type === 'cannot-extract-data') {
return {
...AstroErrorData.CannotExtractFontData,
message: AstroErrorData.CannotExtractFontData.message(input.data.family, input.data.url),
};
}
input satisfies never;
// Should never happen but TS isn't happy
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { readFileSync } from 'node:fs';
import type { ErrorHandler, FontFileReader } from '../definitions.js';
import { fontace } from 'fontace';
import type { Style } from '../types.js';

export function createFontaceFontFileReader({
errorHandler,
}: { errorHandler: ErrorHandler }): FontFileReader {
return {
extract({ family, url }) {
try {
const data = fontace(readFileSync(url));
return {
weight: data.weight,
style: data.style as Style,
};
} catch (cause) {
throw errorHandler.handle({
type: 'cannot-extract-data',
data: { family, url },
cause,
});
}
},
};
}
2 changes: 1 addition & 1 deletion packages/astro/src/assets/fonts/logic/resolve-families.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function resolveVariants({
}): ResolvedLocalFontFamily['variants'] {
return variants.map((variant) => ({
...variant,
weight: variant.weight.toString(),
weight: variant.weight?.toString(),
src: variant.src.map((value) => {
// A src can be a string or an object, we extract the value accordingly.
const isValue = typeof value === 'string' || value instanceof URL;
Expand Down
4 changes: 4 additions & 0 deletions packages/astro/src/assets/fonts/orchestrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Logger } from '../../core/logger/core.js';
import { LOCAL_PROVIDER_NAME } from './constants.js';
import type {
CssRenderer,
FontFileReader,
FontMetricsResolver,
FontTypeExtractor,
Hasher,
Expand Down Expand Up @@ -56,6 +57,7 @@ export async function orchestrate({
systemFallbacksProvider,
fontMetricsResolver,
fontTypeExtractor,
fontFileReader,
logger,
createUrlProxy,
defaults,
Expand All @@ -69,6 +71,7 @@ export async function orchestrate({
systemFallbacksProvider: SystemFallbacksProvider;
fontMetricsResolver: FontMetricsResolver;
fontTypeExtractor: FontTypeExtractor;
fontFileReader: FontFileReader;
// TODO: follow this implementation: https://github.com/withastro/astro/pull/13756/commits/e30ac2b7082a3eed36225da6e88449890cbcbe6b
logger: Logger;
createUrlProxy: (params: CreateUrlProxyParams) => UrlProxy;
Expand Down Expand Up @@ -150,6 +153,7 @@ export async function orchestrate({
family,
urlProxy,
fontTypeExtractor,
fontFileReader,
});
// URLs are already proxied at this point so no further processing is required
fonts = result.fonts;
Expand Down
3 changes: 0 additions & 3 deletions packages/astro/src/assets/fonts/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ function fontsource() {
});
}

// TODO: https://github.com/unjs/unifont/issues/108. Once resolved, remove the unifont patch
// This provider downloads too many files when there's a variable font
// available. This is bad because it doesn't align with our default font settings
/** [Google](https://fonts.google.com/) */
function google(config?: Parameters<typeof providers.google>[0]) {
return defineAstroFontProvider({
Expand Down
77 changes: 50 additions & 27 deletions packages/astro/src/assets/fonts/providers/local.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,66 @@
import type * as unifont from 'unifont';
import { FONT_FORMAT_MAP } from '../constants.js';
import type { FontTypeExtractor, UrlProxy } from '../definitions.js';
import type { FontFileReader, FontTypeExtractor, UrlProxy } from '../definitions.js';
import type { ResolvedLocalFontFamily } from '../types.js';

interface Options {
family: ResolvedLocalFontFamily;
urlProxy: UrlProxy;
fontTypeExtractor: FontTypeExtractor;
fontFileReader: FontFileReader;
}

export function resolveLocalFont({ family, urlProxy, fontTypeExtractor }: Options): {
export function resolveLocalFont({
family,
urlProxy,
fontTypeExtractor,
fontFileReader,
}: Options): {
fonts: Array<unifont.FontFaceData>;
} {
return {
fonts: family.variants.map((variant) => ({
weight: variant.weight,
style: variant.style,
fonts: family.variants.map((variant) => {
const infer = variant.weight === undefined || variant.style === undefined;

// We prepare the data
const data: unifont.FontFaceData = {
// If it should be inferred, we don't want to set the value
weight: variant.weight,
style: variant.style,
src: [],
unicodeRange: variant.unicodeRange,
display: variant.display,
stretch: variant.stretch,
featureSettings: variant.featureSettings,
variationSettings: variant.variationSettings,
};
// We proxy each source
src: variant.src.map((source, index) => ({
originalURL: source.url,
url: urlProxy.proxy({
url: source.url,
// We only use the first source for preloading. For example if woff2 and woff
// are available, we only keep woff2.
collectPreload: index === 0,
data: {
weight: variant.weight,
style: variant.style,
},
init: null,
}),
format: FONT_FORMAT_MAP[fontTypeExtractor.extract(source.url)],
tech: source.tech,
})),
display: variant.display,
unicodeRange: variant.unicodeRange,
stretch: variant.stretch,
featureSettings: variant.featureSettings,
variationSettings: variant.variationSettings,
})),
data.src = variant.src.map((source, index) => {
// We only try to infer for the first source
if (infer && index === 0) {
const result = fontFileReader.extract({ family: family.name, url: source.url });
if (variant.weight === undefined) data.weight = result.weight;
if (variant.style === undefined) data.style = result.style;
}

return {
originalURL: source.url,
url: urlProxy.proxy({
url: source.url,
// We only use the first source for preloading. For example if woff2 and woff
// are available, we only keep woff2.
collectPreload: index === 0,
data: {
weight: data.weight,
style: data.style,
},
init: null,
}),
format: FONT_FORMAT_MAP[fontTypeExtractor.extract(source.url)],
tech: source.tech,
};
});
return data;
}),
};
}
5 changes: 4 additions & 1 deletion packages/astro/src/assets/fonts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
fontProviderSchema,
localFontFamilySchema,
remoteFontFamilySchema,
styleSchema,
} from './config.js';
import type { FONT_TYPES, GENERIC_FALLBACK_NAMES } from './constants.js';
import type { CollectedFontForMetrics } from './logic/optimize-fallbacks.js';
Expand All @@ -28,7 +29,7 @@ export interface ResolvedLocalFontFamily
Omit<LocalFontFamily, 'variants'> {
variants: Array<
Omit<LocalFontFamily['variants'][number], 'weight' | 'src'> & {
weight: string;
weight?: string;
src: Array<{ url: string; tech?: string }>;
}
>;
Expand Down Expand Up @@ -101,3 +102,5 @@ export type FontFileDataMap = Map<FontFileData['hash'], Pick<FontFileData, 'url'
* Holds associations of CSS variables and preloadData/css to be passed to the virtual module.
*/
export type ConsumableMap = Map<string, { preloadData: Array<PreloadData>; css: string }>;

export type Style = z.output<typeof styleSchema>;
2 changes: 1 addition & 1 deletion packages/astro/src/assets/fonts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function unifontFontFaceDataToProperties(
return {
src: font.src ? renderFontSrc(font.src) : undefined,
'font-display': font.display ?? 'swap',
'unicode-range': font.unicodeRange?.join(','),
'unicode-range': font.unicodeRange?.length ? font.unicodeRange.join(',') : undefined,
'font-weight': Array.isArray(font.weight) ? font.weight.join(' ') : font.weight?.toString(),
'font-style': font.style,
'font-stretch': font.stretch,
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/assets/fonts/vite-plugin-fonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
import { createUrlProxy } from './implementations/url-proxy.js';
import { orchestrate } from './orchestrate.js';
import type { ConsumableMap, FontFileDataMap } from './types.js';
import { createFontaceFontFileReader } from './implementations/font-file-reader.js';

interface Options {
settings: AstroSettings;
Expand Down Expand Up @@ -132,6 +133,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
fontFetcher = createCachedFontFetcher({ storage, errorHandler, fetch, readFile });
const fontMetricsResolver = createCapsizeFontMetricsResolver({ fontFetcher, cssRenderer });
fontTypeExtractor = createFontTypeExtractor({ errorHandler });
const fontFileReader = createFontaceFontFileReader({ errorHandler });

const res = await orchestrate({
families: settings.config.experimental.fonts!,
Expand All @@ -143,6 +145,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
systemFallbacksProvider,
fontMetricsResolver,
fontTypeExtractor,
fontFileReader,
logger,
createUrlProxy: ({ local, ...params }) => {
const dataCollector = createDataCollector(params);
Expand Down
14 changes: 14 additions & 0 deletions packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,20 @@ export const CannotExtractFontType = {
hint: 'Open an issue at https://github.com/withastro/astro/issues.',
} satisfies ErrorData;

/**
* @docs
* @description
* Cannot extract the font data from the given URL.
* @message
* An error occured while trying to extract the font data from the given URL.
*/
export const CannotExtractFontData = {
name: 'CannotExtractFontType',
title: 'Cannot extract the font data from the given URL.',
message: (family: string, url: string) => `An error occurred while trying to extract the font data from local font family "${family}" (url: ${url})`,
hint: 'Update your family config and set \`weight\` and \`style\` manually instead.',
} satisfies ErrorData;

/**
* @docs
* @description
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/test/units/assets/fonts/orchestrate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createBuildRemoteFontProviderModResolver } from '../../../../dist/asset
import { createRemoteFontProviderResolver } from '../../../../dist/assets/fonts/implementations/remote-font-provider-resolver.js';
import { createSystemFallbacksProvider } from '../../../../dist/assets/fonts/implementations/system-fallbacks-provider.js';
import { createRemoteUrlProxyContentResolver } from '../../../../dist/assets/fonts/implementations/url-proxy-content-resolver.js';
import { createFontaceFontFileReader } from '../../../../dist/assets/fonts/implementations/font-file-reader.js';
import { createUrlProxy } from '../../../../dist/assets/fonts/implementations/url-proxy.js';
import { orchestrate } from '../../../../dist/assets/fonts/orchestrate.js';
import { defineAstroFontProvider } from '../../../../dist/assets/fonts/providers/index.js';
Expand Down Expand Up @@ -56,6 +57,7 @@ describe('fonts orchestrate()', () => {
systemFallbacksProvider: createSystemFallbacksProvider(),
fontMetricsResolver: fakeFontMetricsResolver,
fontTypeExtractor,
fontFileReader: createFontaceFontFileReader({ errorHandler }),
createUrlProxy: ({ local, ...params }) => {
const dataCollector = createDataCollector(params);
const contentResolver = createRemoteUrlProxyContentResolver();
Expand Down Expand Up @@ -156,6 +158,7 @@ describe('fonts orchestrate()', () => {
systemFallbacksProvider: createSystemFallbacksProvider(),
fontMetricsResolver: fakeFontMetricsResolver,
fontTypeExtractor,
fontFileReader: createFontaceFontFileReader({ errorHandler }),
createUrlProxy: ({ local, ...params }) => {
const dataCollector = createDataCollector(params);
const contentResolver = createRemoteUrlProxyContentResolver();
Expand Down
Loading