Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
1517163
comment
florian-lefebvre Jan 15, 2026
421fa7f
feat: work on new provider
florian-lefebvre Jan 15, 2026
fcccc4e
cleanup local specific code
florian-lefebvre Jan 15, 2026
7ae0181
fix: build
florian-lefebvre Jan 15, 2026
0556c4e
wip
florian-lefebvre Jan 15, 2026
2da3bb6
unify remote and local sources handling
florian-lefebvre Jan 15, 2026
d2e8d80
inline normalizeRemoteFontFaces
florian-lefebvre Jan 15, 2026
6e2d0c5
inline some proxy things
florian-lefebvre Jan 15, 2026
4db0091
remove url proxy
florian-lefebvre Jan 15, 2026
a12699c
inline data collector
florian-lefebvre Jan 15, 2026
56041df
unify remote and local content resolver handling
florian-lefebvre Jan 15, 2026
07f4852
improve abstraction
florian-lefebvre Jan 15, 2026
6b2f868
improve abstraction
florian-lefebvre Jan 15, 2026
eae1d00
improve abstraction
florian-lefebvre Jan 15, 2026
2e419dc
simplify types
florian-lefebvre Jan 15, 2026
5ed7b2f
narrow type
florian-lefebvre Jan 15, 2026
bd413d9
remove useless things
florian-lefebvre Jan 15, 2026
d9c3ead
extract
florian-lefebvre Jan 15, 2026
b4d9b68
extract
florian-lefebvre Jan 15, 2026
caeb29c
extract
florian-lefebvre Jan 15, 2026
cfecf9a
wip
florian-lefebvre Jan 15, 2026
b46e6f4
extract
florian-lefebvre Jan 15, 2026
bdabdb1
wip
florian-lefebvre Jan 15, 2026
a820d11
rename
florian-lefebvre Jan 15, 2026
6dc1527
rename
florian-lefebvre Jan 15, 2026
40c3d85
extract
florian-lefebvre Jan 15, 2026
0cc32c5
rename
florian-lefebvre Jan 15, 2026
be24cf7
extract
florian-lefebvre Jan 15, 2026
10e9760
collaborators
florian-lefebvre Jan 15, 2026
1be0b45
collaborators
florian-lefebvre Jan 15, 2026
64d2ede
wip
florian-lefebvre Jan 15, 2026
eb6576f
renam
florian-lefebvre Jan 16, 2026
20608e3
extract
florian-lefebvre Jan 16, 2026
b0ad7d5
extract
florian-lefebvre Jan 16, 2026
6eb69c8
rename
florian-lefebvre Jan 16, 2026
b07f0c6
extract
florian-lefebvre Jan 16, 2026
5be5dac
clean
florian-lefebvre Jan 16, 2026
cbd39eb
inline
florian-lefebvre Jan 16, 2026
0c3012f
rename
florian-lefebvre Jan 16, 2026
1e4c90b
clean
florian-lefebvre Jan 16, 2026
ad90934
fix: lint
florian-lefebvre Jan 16, 2026
6da3395
chore: format
florian-lefebvre Jan 16, 2026
8b2ee43
test: resolveFamily
florian-lefebvre Jan 16, 2026
155ffb3
test utils
florian-lefebvre Jan 16, 2026
ffda71e
test: computeFontFamiliesAssets
florian-lefebvre Jan 16, 2026
3c8325b
test: getOrCreateFontFamilyAssets
florian-lefebvre Jan 16, 2026
26224ab
test: filterAndTransformFontFaces
florian-lefebvre Jan 16, 2026
f2f8995
test: collectFontAssetsFromFaces
florian-lefebvre Jan 16, 2026
7bbca19
test: collectFontData
florian-lefebvre Jan 16, 2026
5faad6d
test: optimizeFallbacks
florian-lefebvre Jan 16, 2026
4b7299a
test: collectComponentData
florian-lefebvre Jan 16, 2026
bd5b1ed
fix: infra tests
florian-lefebvre Jan 16, 2026
7b2aa9e
fix: providers test
florian-lefebvre Jan 16, 2026
cd9ef0e
fix: pass updated fonts
florian-lefebvre Jan 16, 2026
51c8ea1
fix: ensure same var is used
florian-lefebvre Jan 16, 2026
b307773
move cache()
florian-lefebvre Jan 16, 2026
c7f1fe6
test: font file content resolver
florian-lefebvre Jan 16, 2026
f840b63
test: local provider
florian-lefebvre Jan 16, 2026
fa69f88
test: e2e and fixes
florian-lefebvre Jan 16, 2026
5f868ba
fix: tests
florian-lefebvre Jan 16, 2026
e93b820
try fix test
florian-lefebvre Jan 16, 2026
0bbae39
try fix windows path
florian-lefebvre Jan 16, 2026
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
8 changes: 4 additions & 4 deletions packages/astro/components/Font.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { filterPreloads } from 'astro/assets/fonts/runtime';
import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';

// TODO: remove check when fonts are stabilized
const { internalConsumableMap } = mod;
if (!internalConsumableMap) {
const { componentDataByCssVariable } = mod;
if (!componentDataByCssVariable) {
throw new AstroError(AstroErrorData.ExperimentalFontsNotEnabled);
}

Expand All @@ -17,15 +17,15 @@ interface Props {
}

const { cssVariable, preload = false } = Astro.props as Props;
const data = internalConsumableMap.get(cssVariable);
const data = componentDataByCssVariable.get(cssVariable);
if (!data) {
throw new AstroError({
...AstroErrorData.FontFamilyNotFound,
message: AstroErrorData.FontFamilyNotFound.message(cssVariable),
});
}

const filteredPreloadData = filterPreloads(data.preloadData, preload);
const filteredPreloadData = filterPreloads(data.preloads, preload);
---

<style set:html={data.css}></style>
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/dev-only.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ declare module 'virtual:astro:env/internal' {
}

declare module 'virtual:astro:assets/fonts/internal' {
export const internalConsumableMap: import('./src/assets/fonts/types.js').InternalConsumableMap;
export const consumableMap: import('./src/assets/fonts/types.js').ConsumableMap;
export const componentDataByCssVariable: import('./src/assets/fonts/types.js').ComponentDataByCssVariable;
export const fontDataByCssVariable: import('./src/assets/fonts/types.js').FontDataByCssVariable;
}

declare module 'virtual:astro:adapter-config/client' {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/assets/fonts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Here is an overview of the architecture of the fonts in Astro:

- [`orchestrate()`](./orchestrate.ts) combines sub steps and takes care of getting useful data from the config
- It resolves font families (eg. import remote font providers)
- It resolves font families (eg. deduplication)
- It initializes the font resolver
- For each family, it resolves fonts data and normalizes them
- For each family, optimized fallbacks (and related CSS) are generated if applicable
Expand Down
32 changes: 2 additions & 30 deletions packages/astro/src/assets/fonts/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { FONT_TYPES, LOCAL_PROVIDER_NAME } from './constants.js';
import { FONT_TYPES } from './constants.js';
import type { FontProvider } from './types.js';

export const weightSchema = z.union([z.string(), z.number()]);
Expand All @@ -26,34 +26,6 @@ const requiredFamilyAttributesSchema = z.object({
cssVariable: z.string(),
});

const entrypointSchema = z.union([z.string(), z.instanceof(URL)]);

export const localFontFamilySchema = z
.object({
...requiredFamilyAttributesSchema.shape,
...fallbacksSchema.shape,
provider: z.literal(LOCAL_PROVIDER_NAME),
variants: z
.array(
z
.object({
...familyPropertiesSchema.shape,
src: z
.array(
z.union([
entrypointSchema,
z.object({ url: entrypointSchema, tech: z.string().optional() }).strict(),
]),
)
.nonempty(),
// TODO: find a way to support subsets (through fontkit?)
})
.strict(),
)
.nonempty(),
})
.strict();

export const fontProviderSchema = z
.object({
name: z.string(),
Expand All @@ -64,7 +36,7 @@ export const fontProviderSchema = z
})
.strict();

export const remoteFontFamilySchema = z
export const fontFamilySchema = z
.object({
...requiredFamilyAttributesSchema.shape,
...fallbacksSchema.shape,
Expand Down
2 changes: 0 additions & 2 deletions packages/astro/src/assets/fonts/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { Defaults, FontType } from './types.js';

export const LOCAL_PROVIDER_NAME = 'local';

export const DEFAULTS: Defaults = {
weights: ['400'],
styles: ['normal', 'italic'],
Expand Down
72 changes: 72 additions & 0 deletions packages/astro/src/assets/fonts/core/collect-component-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { CssRenderer } from '../definitions.js';
import type {
Collaborator,
ComponentDataByCssVariable,
Defaults,
FontFamilyAssets,
} from '../types.js';
import { unifontFontFaceDataToProperties } from '../utils.js';
import type { optimizeFallbacks as _optimizeFallbacks } from './optimize-fallbacks.js';

export async function collectComponentData({
fontFamilyAssets,
cssRenderer,
defaults,
optimizeFallbacks,
}: {
fontFamilyAssets: Array<FontFamilyAssets>;
cssRenderer: CssRenderer;
defaults: Pick<Defaults, 'fallbacks' | 'optimizedFallbacks'>;
optimizeFallbacks: Collaborator<
typeof _optimizeFallbacks,
'family' | 'fallbacks' | 'collectedFonts'
>;
}) {
const componentDataByCssVariable: ComponentDataByCssVariable = new Map();

for (const { family, fonts, collectedFontsForMetricsByUniqueKey, preloads } of fontFamilyAssets) {
let css = '';

for (const data of fonts) {
css += cssRenderer.generateFontFace(
family.uniqueName,
unifontFontFaceDataToProperties({
src: data.src,
weight: data.weight,
style: data.style,
// User settings override the generated font settings
display: data.display ?? family.display,
unicodeRange: data.unicodeRange ?? family.unicodeRange,
stretch: data.stretch ?? family.stretch,
featureSettings: data.featureSettings ?? family.featureSettings,
variationSettings: data.variationSettings ?? family.variationSettings,
}),
);
}

const fallbacks = family.fallbacks ?? defaults.fallbacks;
const cssVarValues = [family.uniqueName];
const optimizeFallbacksResult =
(family.optimizedFallbacks ?? defaults.optimizedFallbacks)
? await optimizeFallbacks({
family,
fallbacks,
collectedFonts: Array.from(collectedFontsForMetricsByUniqueKey.values()),
})
: null;

if (optimizeFallbacksResult) {
css += optimizeFallbacksResult.css;
cssVarValues.push(...optimizeFallbacksResult.fallbacks);
} else {
// If there are no optimized fallbacks, we pass the provided fallbacks as is.
cssVarValues.push(...fallbacks);
}

css += cssRenderer.generateCssVariable(family.cssVariable, cssVarValues);

componentDataByCssVariable.set(family.cssVariable, { preloads, css });
}

return componentDataByCssVariable;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type * as unifont from 'unifont';
import { FONT_FORMATS } from '../constants.js';
import type { FontFileIdGenerator, Hasher } from '../definitions.js';
import type { Defaults, FontFileById, PreloadData, ResolvedFontFamily } from '../types.js';
import { renderFontWeight } from '../utils.js';
import type { CollectedFontForMetrics } from './optimize-fallbacks.js';

export function collectFontAssetsFromFaces({
fonts,
fontFileIdGenerator,
family,
fontFilesIds,
collectedFontsIds,
hasher,
defaults,
}: {
fonts: Array<unifont.FontFaceData>;
fontFileIdGenerator: FontFileIdGenerator;
family: Pick<ResolvedFontFamily, 'cssVariable' | 'fallbacks'>;
fontFilesIds: Set<string>;
collectedFontsIds: Set<string>;
hasher: Hasher;
defaults: Pick<Defaults, 'fallbacks'>;
}) {
const fontFileById: FontFileById = new Map();
const collectedFontsForMetricsByUniqueKey = new Map<string, CollectedFontForMetrics>();
const preloads: Array<PreloadData> = [];

for (const font of fonts) {
// The index keeps track of encountered URLs. We can't use a regular for loop
// below because it may contain sources without urls, which would prevent preloading completely
let index = 0;
for (const source of font.src) {
if ('name' in source) {
continue;
}
const format = FONT_FORMATS.find((e) => e.format === source.format)!;
const originalUrl = source.originalURL!;
const id = fontFileIdGenerator.generate({
cssVariable: family.cssVariable,
font,
originalUrl,
type: format.type,
});

if (!fontFilesIds.has(id) && !fontFileById.has(id)) {
fontFileById.set(id, { url: source.url, init: font.meta?.init });
// We only collect the first URL to avoid preloading fallback sources (eg. we only
// preload woff2 if woff is available)
if (index === 0) {
preloads.push({
style: font.style,
subset: font.meta?.subset,
type: format.type,
url: source.url,
weight: renderFontWeight(font.weight),
});
}
}

const collected: CollectedFontForMetrics = {
hash: id,
url: originalUrl,
init: font.meta?.init,
data: {
weight: font.weight,
style: font.style,
meta: {
subset: font.meta?.subset,
},
},
};
const collectedKey = hasher.hashObject(collected.data);
const fallbacks = family.fallbacks ?? defaults.fallbacks;
if (
fallbacks.length > 0 &&
// If the same data has already been sent for this family, we don't want to have
// duplicated fallbacks. Such scenario can occur with unicode ranges.
!collectedFontsIds.has(collectedKey) &&
!collectedFontsForMetricsByUniqueKey.has(collectedKey)
) {
// If a family has fallbacks, we store the first url we get that may
// be used for the fallback generation.
collectedFontsForMetricsByUniqueKey.set(collectedKey, collected);
}

index++;
}
}

return {
fontFileById,
preloads,
collectedFontsForMetricsByUniqueKey,
};
}
31 changes: 31 additions & 0 deletions packages/astro/src/assets/fonts/core/collect-font-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { FontData, FontDataByCssVariable, FontFamilyAssets } from '../types.js';
import { renderFontWeight } from '../utils.js';

export function collectFontData(
fontFamilyAssets: Array<
Pick<FontFamilyAssets, 'fonts'> & { family: Pick<FontFamilyAssets['family'], 'cssVariable'> }
>,
) {
const fontDataByCssVariable: FontDataByCssVariable = new Map();

for (const { family, fonts } of fontFamilyAssets) {
const fontData: Array<FontData> = [];
for (const data of fonts) {
fontData.push({
weight: renderFontWeight(data.weight),
style: data.style,
src: data.src
.filter((src) => 'url' in src)
.map((src) => ({
url: src.url,
format: src.format,
tech: src.tech,
})),
});
}

fontDataByCssVariable.set(family.cssVariable, fontData);
}

return fontDataByCssVariable;
}
Loading
Loading