Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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/cyan-games-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Updates unifont to latest and adds support for `fetch` options from remote providers when using the experimental fonts API
2 changes: 1 addition & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
"tinyglobby": "^0.2.12",
"tsconfck": "^3.1.5",
"ultrahtml": "^1.6.0",
"unifont": "~0.2.0",
"unifont": "~0.4.0",
"unist-util-visit": "^5.0.0",
"unstorage": "^1.15.0",
"vfile": "^6.0.3",
Expand Down
33 changes: 20 additions & 13 deletions packages/astro/src/assets/fonts/definitions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type * as unifont from 'unifont';
import type { CollectedFontForMetrics } from './logic/optimize-fallbacks.js';
/* eslint-disable @typescript-eslint/no-empty-object-type */
import type { AstroFontProvider, FontType, PreloadData, ResolvedFontProvider } from './types.js';
import type {
AstroFontProvider,
FontFileData,
FontType,
PreloadData,
ResolvedFontProvider,
} from './types.js';
import type { FontFaceMetrics, GenericFallbackName } from './types.js';

export interface Hasher {
Expand Down Expand Up @@ -43,24 +49,25 @@ export interface ErrorHandler {
}

export interface UrlProxy {
proxy: (input: {
url: string;
collectPreload: boolean;
data: Partial<unifont.FontFaceData>;
}) => string;
proxy: (
input: Pick<FontFileData, 'url' | 'init'> & {
collectPreload: boolean;
data: Partial<unifont.FontFaceData>;
},
) => string;
}

export interface UrlProxyContentResolver {
resolve: (url: string) => string;
}

export interface DataCollector {
collect: (input: {
originalUrl: string;
hash: string;
data: Partial<unifont.FontFaceData>;
preload: PreloadData | null;
}) => void;
collect: (
input: FontFileData & {
data: Partial<unifont.FontFaceData>;
preload: PreloadData | null;
},
) => void;
}

export type CssProperties = Record<string, string | undefined>;
Expand All @@ -87,7 +94,7 @@ export interface SystemFallbacksProvider {
}

export interface FontFetcher {
fetch: (hash: string, url: string) => Promise<Buffer>;
fetch: (input: FontFileData) => Promise<Buffer>;
}

export interface FontTypeExtractor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,14 @@ export function createDataCollector({
saveFontData,
}: Omit<CreateUrlProxyParams, 'local'>): DataCollector {
return {
collect({ originalUrl, hash, preload, data }) {
collect({ hash, url, init, preload, data }) {
if (!hasUrl(hash)) {
saveUrl(hash, originalUrl);
saveUrl({ hash, url, init });
if (preload) {
savePreload(preload);
}
}
saveFontData({
hash,
url: originalUrl,
data,
});
saveFontData({ hash, url, data, init });
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,17 @@ export function createCachedFontFetcher({
}: {
storage: Storage;
errorHandler: ErrorHandler;
fetch: (url: string) => Promise<Response>;
fetch: (url: string, init?: RequestInit) => Promise<Response>;
readFile: (url: string) => Promise<Buffer>;
}): FontFetcher {
return {
async fetch(hash, url) {
async fetch({ hash, url, init }) {
return await cache(storage, hash, async () => {
try {
if (isAbsolute(url)) {
return await readFile(url);
}
// TODO: find a way to pass headers
// https://github.com/unjs/unifont/issues/143
const response = await fetch(url);
const response = await fetch(url, init ?? undefined);
if (!response.ok) {
throw new Error(`Response was not successful, received status code ${response.status}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export function createCapsizeFontMetricsResolver({
const cache: Record<string, FontFaceMetrics | null> = {};

return {
async getMetrics(name, { hash, url }) {
cache[name] ??= filterRequiredMetrics(await fromBuffer(await fontFetcher.fetch(hash, url)));
async getMetrics(name, input) {
cache[name] ??= filterRequiredMetrics(await fromBuffer(await fontFetcher.fetch(input)));
return cache[name];
},
// Source: https://github.com/unjs/fontaine/blob/f00f84032c5d5da72c8798eae4cd68d3ddfbf340/src/css.ts#L170
Expand Down
5 changes: 3 additions & 2 deletions packages/astro/src/assets/fonts/implementations/url-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@ export function createUrlProxy({
fontTypeExtractor: FontTypeExtractor;
}): UrlProxy {
return {
proxy({ url: originalUrl, data, collectPreload }) {
proxy({ url: originalUrl, data, collectPreload, init }) {
const type = fontTypeExtractor.extract(originalUrl);
const hash = `${hasher.hashString(contentResolver.resolve(originalUrl))}.${type}`;
const url = base + hash;

dataCollector.collect({
originalUrl,
url: originalUrl,
hash,
preload: collectPreload ? { url, type } : null,
data,
init,
});

return url;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function normalizeRemoteFontFaces({
weight: font.weight,
style: font.style,
},
init: font.meta?.init ?? null,
}),
};
index++;
Expand Down
14 changes: 6 additions & 8 deletions packages/astro/src/assets/fonts/logic/optimize-fallbacks.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type * as unifont from 'unifont';
import type { FontMetricsResolver, SystemFallbacksProvider } from '../definitions.js';
import type { ResolvedFontFamily } from '../types.js';
import type * as unifont from 'unifont';
import type { FontFileData, ResolvedFontFamily } from '../types.js';
import { isGenericFontFamily, unifontFontFaceDataToProperties } from '../utils.js';

export interface CollectedFontForMetrics {
hash: string;
url: string;
export interface CollectedFontForMetrics extends FontFileData {
data: Partial<unifont.FontFaceData>;
}

Expand Down Expand Up @@ -64,14 +62,14 @@ export async function optimizeFallbacks({
let css = '';

for (const { font, name } of localFontsMappings) {
for (const { hash, url, data } of collectedFonts) {
for (const collected of collectedFonts) {
// We generate a fallback for each font collected, which is per weight and style
css += fontMetricsResolver.generateFontFace({
metrics: await fontMetricsResolver.getMetrics(family.name, { hash, url, data }),
metrics: await fontMetricsResolver.getMetrics(family.name, collected),
fallbackMetrics: systemFallbacksProvider.getMetricsForLocalFont(font),
font,
name,
properties: unifontFontFaceDataToProperties(data),
properties: unifontFontFaceDataToProperties(collected.data),
});
}
}
Expand Down
30 changes: 15 additions & 15 deletions packages/astro/src/assets/fonts/orchestrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ import { normalizeRemoteFontFaces } from './logic/normalize-remote-font-faces.js
import { type CollectedFontForMetrics, optimizeFallbacks } from './logic/optimize-fallbacks.js';
import { resolveFamilies } from './logic/resolve-families.js';
import { resolveLocalFont } from './providers/local.js';
import type { CreateUrlProxyParams, Defaults, FontFamily, PreloadData } from './types.js';
import type {
ConsumableMap,
CreateUrlProxyParams,
Defaults,
FontFamily,
FontFileDataMap,
PreloadData,
} from './types.js';
import { pickFontFaceProperty, unifontFontFaceDataToProperties } from './utils.js';

/**
Expand Down Expand Up @@ -80,15 +87,8 @@ export async function orchestrate({
storage,
});

/**
* Holds associations of hash and original font file URLs, so they can be
* downloaded whenever the hash is requested.
*/
const hashToUrlMap = new Map<string, string>();
/**
* Holds associations of CSS variables and preloadData/css to be passed to the virtual module.
*/
const resolvedMap = new Map<string, { preloadData: Array<PreloadData>; css: string }>();
const fontFileDataMap: FontFileDataMap = new Map();
const consumableMap: ConsumableMap = new Map();

for (const family of resolvedFamilies) {
const preloadData: Array<PreloadData> = [];
Expand All @@ -106,9 +106,9 @@ export async function orchestrate({
*/
const urlProxy = createUrlProxy({
local: family.provider === LOCAL_PROVIDER_NAME,
hasUrl: (hash) => hashToUrlMap.has(hash),
saveUrl: (hash, url) => {
hashToUrlMap.set(hash, url);
hasUrl: (hash) => fontFileDataMap.has(hash),
saveUrl: ({ hash, url, init }) => {
fontFileDataMap.set(hash, { url, init });
},
savePreload: (preload) => {
preloadData.push(preload);
Expand Down Expand Up @@ -195,8 +195,8 @@ export async function orchestrate({

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

resolvedMap.set(family.cssVariable, { preloadData, css });
consumableMap.set(family.cssVariable, { preloadData, css });
}

return { hashToUrlMap, resolvedMap };
return { fontFileDataMap, consumableMap };
}
1 change: 1 addition & 0 deletions packages/astro/src/assets/fonts/providers/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function resolveLocalFont({ family, urlProxy, fontTypeExtractor }: Option
weight: variant.weight,
style: variant.style,
},
init: null
}),
format: FONT_FORMAT_MAP[fontTypeExtractor.extract(source.url)],
tech: source.tech,
Expand Down
19 changes: 18 additions & 1 deletion packages/astro/src/assets/fonts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,27 @@ export type Defaults = Partial<
>
>;

export interface FontFileData {
hash: string;
url: string;
init: RequestInit | null;
}

export interface CreateUrlProxyParams {
local: boolean;
hasUrl: (hash: string) => boolean;
saveUrl: (hash: string, url: string) => void;
saveUrl: (input: FontFileData) => void;
savePreload: (preload: PreloadData) => void;
saveFontData: (collected: CollectedFontForMetrics) => void;
}

/**
* Holds associations of hash and original font file URLs, so they can be
* downloaded whenever the hash is requested.
*/
export type FontFileDataMap = Map<FontFileData['hash'], Pick<FontFileData, 'url' | 'init'>>;

/**
* 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 }>;
35 changes: 17 additions & 18 deletions packages/astro/src/assets/fonts/vite-plugin-fonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
} from './implementations/url-proxy-content-resolver.js';
import { createUrlProxy } from './implementations/url-proxy.js';
import { orchestrate } from './orchestrate.js';
import type { PreloadData } from './types.js';
import type { ConsumableMap, FontFileDataMap } from './types.js';

interface Options {
settings: AstroSettings;
Expand Down Expand Up @@ -79,18 +79,15 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
// to trailingSlash: never)
const baseUrl = removeTrailingForwardSlash(settings.config.base) + URL_PREFIX;

let resolvedMap: Map<string, { preloadData: Array<PreloadData>; css: string }> | null = null;
// Key is `${hash}.${ext}`, value is a URL.
// When a font file is requested (eg. /_astro/fonts/abc.woff), we use the hash
// to download the original file, or retrieve it from cache
let hashToUrlMap: Map<string, string> | null = null;
let fontFileDataMap: FontFileDataMap | null = null;
let consumableMap: ConsumableMap | null = null;
let isBuild: boolean;
let fontFetcher: FontFetcher | null = null;
let fontTypeExtractor: FontTypeExtractor | null = null;

const cleanup = () => {
resolvedMap = null;
hashToUrlMap = null;
consumableMap = null;
fontFileDataMap = null;
fontFetcher = null;
};

Expand Down Expand Up @@ -163,8 +160,8 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
});
// We initialize shared variables here and reset them in buildEnd
// to avoid locking memory
hashToUrlMap = res.hashToUrlMap;
resolvedMap = res.resolvedMap;
fontFileDataMap = res.fontFileDataMap;
consumableMap = res.consumableMap;
}

return {
Expand All @@ -190,7 +187,9 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
});
// The map is always defined at this point. Its values contains urls from remote providers
// as well as local paths for the local provider. We filter them to only keep the filepaths
const localPaths = [...hashToUrlMap!.values()].filter((url) => isAbsolute(url));
const localPaths = [...fontFileDataMap!.values()]
.filter(({ url }) => isAbsolute(url))
.map((v) => v.url);
server.watcher.on('change', (path) => {
if (localPaths.includes(path)) {
logger.info('assets', 'Font file updated');
Expand All @@ -214,8 +213,8 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
return next();
}
const hash = req.url.slice(1);
const url = hashToUrlMap?.get(hash);
if (!url) {
const associatedData = fontFileDataMap?.get(hash);
if (!associatedData) {
return next();
}
// We don't want the request to be cached in dev because we cache it already internally,
Expand All @@ -228,7 +227,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 fontFetcher!.fetch(hash, url);
const data = await fontFetcher!.fetch({ hash, ...associatedData });

res.setHeader('Content-Length', data.length);
res.setHeader('Content-Type', `font/${fontTypeExtractor!.extract(hash)}`);
Expand All @@ -255,7 +254,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
load(id) {
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
return {
code: `export const fontsData = new Map(${JSON.stringify(Array.from(resolvedMap?.entries() ?? []))})`,
code: `export const fontsData = new Map(${JSON.stringify(Array.from(consumableMap?.entries() ?? []))})`,
};
}
},
Expand All @@ -273,11 +272,11 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
} catch (cause) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause });
}
if (hashToUrlMap) {
if (fontFileDataMap) {
logger.info('assets', 'Copying fonts...');
await Promise.all(
Array.from(hashToUrlMap.entries()).map(async ([hash, url]) => {
const data = await fontFetcher!.fetch(hash, url);
Array.from(fontFileDataMap.entries()).map(async ([hash, associatedData]) => {
const data = await fontFetcher!.fetch({ hash, ...associatedData });
try {
writeFileSync(new URL(hash, fontsDir), data);
} catch (cause) {
Expand Down
Loading