Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/red-parents-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Updates fallback font generation to always read font files returned by font providers
1 change: 0 additions & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@
"@astrojs/internal-helpers": "workspace:*",
"@astrojs/markdown-remark": "workspace:*",
"@astrojs/telemetry": "workspace:*",
"@capsizecss/metrics": "^3.5.0",
"@capsizecss/unpack": "^2.4.0",
"@oslojs/encoding": "^1.1.0",
"@rollup/pluginutils": "^5.1.4",
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/assets/fonts/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export async function loadFonts({
for (const family of families) {
const preloadData: PreloadData = [];
let css = '';
let fallbackFontData: GetMetricsForFamilyFont = null;
let fallbackFontData: GetMetricsForFamilyFont | null = null;

// When going through the urls/filepaths returned by providers,
// We save the hash and the associated original value so we can use
Expand Down
30 changes: 0 additions & 30 deletions packages/astro/src/assets/fonts/metrics.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
// Adapted from https://github.com/unjs/fontaine/
import { fontFamilyToCamelCase } from '@capsizecss/metrics';
import { type Font, fromBuffer } from '@capsizecss/unpack';

const QUOTES_RE = /^["']|["']$/g;

const withoutQuotes = (str: string) => str.trim().replace(QUOTES_RE, '');

export type FontFaceMetrics = Pick<
Font,
'ascent' | 'descent' | 'lineGap' | 'unitsPerEm' | 'xWidthAvg'
Expand All @@ -29,30 +23,6 @@ function filterRequiredMetrics({
};
}

export async function getMetricsForFamily(family: string) {
family = withoutQuotes(family);

if (family in metricCache) return metricCache[family];

try {
const name = fontFamilyToCamelCase(family);
const { entireMetricsCollection } = await import('@capsizecss/metrics/entireMetricsCollection');
const metrics = entireMetricsCollection[name as keyof typeof entireMetricsCollection];

if (!('descent' in metrics)) {
metricCache[family] = null;
return null;
}

const filteredMetrics = filterRequiredMetrics(metrics);
metricCache[family] = filteredMetrics;
return filteredMetrics;
} catch {
metricCache[family] = null;
return null;
}
}

export async function readMetrics(family: string, buffer: Buffer) {
const metrics = await fromBuffer(buffer);

Expand Down
14 changes: 7 additions & 7 deletions packages/astro/src/assets/fonts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ export async function cache(
storage: Storage,
key: string,
cb: () => Promise<Buffer>,
): Promise<{ cached: boolean; data: Buffer }> {
): Promise<Buffer> {
const existing = await storage.getItemRaw(key);
if (existing) {
return { cached: true, data: existing };
return existing;
}
const data = await cb();
await storage.setItemRaw(key, data);
return { cached: false, data };
return data;
}

export interface ProxyURLOptions {
Expand Down Expand Up @@ -133,13 +133,13 @@ export function isGenericFontFamily(str: string): str is keyof typeof DEFAULT_FA
export type GetMetricsForFamilyFont = {
hash: string;
url: string;
} | null;
};

export type GetMetricsForFamily = (
name: string,
/** A remote url or local filepath to a font file. Used if metrics can't be resolved purely from the family name */
font: GetMetricsForFamilyFont,
) => Promise<FontFaceMetrics | null>;
) => Promise<FontFaceMetrics>;

/**
* Generates CSS for a given family fallbacks if possible.
Expand All @@ -157,7 +157,7 @@ export async function generateFallbacksCSS({
family: Pick<ResolvedFontFamily, 'name' | 'nameWithHash'>;
/** The family fallbacks */
fallbacks: Array<string>;
font: GetMetricsForFamilyFont;
font: GetMetricsForFamilyFont | null;
metrics: {
getMetricsForFamily: GetMetricsForFamily;
generateFontFace: typeof generateFallbackFontFace;
Expand All @@ -171,7 +171,7 @@ export async function generateFallbacksCSS({

let css = '';

if (!metrics) {
if (!fontData || !metrics) {
return { css, fallbacks };
}

Expand Down
13 changes: 4 additions & 9 deletions packages/astro/src/assets/fonts/vite-plugin-fonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
VIRTUAL_MODULE_ID,
} from './constants.js';
import { loadFonts } from './load.js';
import { generateFallbackFontFace, getMetricsForFamily, readMetrics } from './metrics.js';
import { generateFallbackFontFace, readMetrics } from './metrics.js';
import type { ResolveMod } from './providers/utils.js';
import type { PreloadData, ResolvedFontFamily } from './types.js';
import { cache, extractFontType, resolveFontFamily, sortObjectByKey } from './utils.js';
Expand Down Expand Up @@ -130,12 +130,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
hashString: h64ToString,
generateFallbackFontFace,
getMetricsForFamily: async (name, font) => {
let metrics = await getMetricsForFamily(name);
if (font && !metrics) {
const { data } = await cache(storage!, font.hash, () => fetchFont(font.url));
metrics = await readMetrics(name, data);
}
return metrics;
return await readMetrics(name, await cache(storage!, font.hash, () => fetchFont(font.url)));
},
log: (message) => logger.info('assets', message),
});
Expand Down Expand Up @@ -200,7 +195,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
// Storage should be defined at this point since initialize it called before registering
// the middleware. hashToUrlMap is defined at the same time so if it's not set by now,
// no url will be matched and this line will not be reached.
const { data } = await cache(storage!, hash, () => fetchFont(url));
const data = await cache(storage!, hash, () => fetchFont(url));

res.setHeader('Content-Length', data.length);
res.setHeader('Content-Type', `font/${extractFontType(hash)}`);
Expand Down Expand Up @@ -249,7 +244,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
logger.info('assets', 'Copying fonts...');
await Promise.all(
Array.from(hashToUrlMap.entries()).map(async ([hash, url]) => {
const { data } = await cache(storage!, hash, () => fetchFont(url));
const data = await cache(storage!, hash, () => fetchFont(url));
try {
writeFileSync(new URL(hash, fontsDir), data);
} catch (cause) {
Expand Down
60 changes: 0 additions & 60 deletions packages/astro/test/units/assets/fonts/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,12 @@ import {
extractFontType,
familiesToUnifontProviders,
generateFallbacksCSS,
cache as internalCache,
isFontType,
isGenericFontFamily,
proxyURL,
resolveFontFamily,
} from '../../../../dist/assets/fonts/utils.js';

function createSpyCache() {
/** @type {Map<string, Buffer>} */
const store = new Map();

const storage = {
/**
* @param {string} key
* @returns {Promise<Buffer | null>}
*/
getItemRaw: async (key) => {
return store.get(key) ?? null;
},
/**
* @param {string} key
* @param {Buffer} value
* @returns {Promise<void>}
*/
setItemRaw: async (key, value) => {
store.set(key, value);
},
};

return {
/**
*
* @param {Parameters<typeof internalCache>[1]} key
* @param {Parameters<typeof internalCache>[2]} cb
* @returns
*/
cache: (key, cb) =>
internalCache(
// @ts-expect-error we only mock the required hooks
storage,
key,
cb,
),
getKeys: () => Array.from(store.keys()),
};
}

/**
*
* @param {string} id
Expand Down Expand Up @@ -120,25 +79,6 @@ describe('fonts utils', () => {
}
});

it('cache()', async () => {
const { cache, getKeys } = createSpyCache();

assert.deepStrictEqual(getKeys(), []);

let buffer = Buffer.from('foo');
let res = await cache('foo', async () => buffer);
assert.equal(res.cached, false);
assert.equal(res.data, buffer);

assert.deepStrictEqual(getKeys(), ['foo']);

res = await cache('foo', async () => buffer);
assert.equal(res.cached, true);
assert.equal(res.data, buffer);

assert.deepStrictEqual(getKeys(), ['foo']);
});

it('proxyURL()', () => {
let { url, collected } = proxyURLSpy(
'foo',
Expand Down
9 changes: 1 addition & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.