Skip to content
Draft
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
45 changes: 45 additions & 0 deletions .changeset/dirty-pears-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
'astro': patch
---

Adds a new `getFontBuffer()` method to retrieve font file buffers when using the experimental Fonts API

The `getFontData()` helper function from `astro:assets` was introduced in 5.14.0 to provide access to font family data for use outside of Astro. One of the goals of this API was to be able to retrieve buffers using URLs.

However, it turned out to be impactical and even impossible during prerendering.

Astro now exports a new `getFontBuffer()` helper function from `astro:assets` to retrieve font file buffers from URL returned by `getFontData()`. For example, using [satori](https://github.com/vercel/satori) to generate OpenGraph images:

```diff
// src/pages/og.png.ts

import type{ APIRoute } from "astro"
-import { getFontData } from "astro:assets"
+import { getFontData, getFontBuffer } from "astro:assets"
import satori from "satori"

export const GET: APIRoute = (context) => {
const data = getFontData("--font-roboto")

const svg = await satori(
<div style={{ color: "black" }}>hello, world</div>,
{
width: 600,
height: 400,
fonts: [
{
name: "Roboto",
- data: await fetch(new URL(data[0].src[0].url, context.url.origin)).then(res => res.arrayBuffer()),
+ data: await getFontBuffer(data[0].src[0].url),
weight: 400,
style: "normal",
},
],
},
)

// ...
}
```

See the [experimental Fonts API documentation](https://docs.astro.build/en/reference/experimental-flags/fonts/#accessing-font-data-programmatically) for more information.
2 changes: 2 additions & 0 deletions packages/astro/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ declare module 'astro:assets' {
getFontData: (
cssVariable: import('astro:assets').CssVariable,
) => Array<import('astro:assets').FontData>;
getFontBuffer: (url: string) => Promise<Buffer>;
};

type ImgAttributes = import('./dist/type-utils.js').WithRequired<
Expand All @@ -79,6 +80,7 @@ declare module 'astro:assets' {
Font,
inferRemoteSize,
getFontData,
getFontBuffer,
}: AstroAssets;
}

Expand Down
1 change: 1 addition & 0 deletions packages/astro/dev-only.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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 bufferImports: import('./src/assets/fonts/types.js').BufferImports;
}

declare module 'virtual:astro:adapter-config/client' {
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/assets/fonts/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const DEFAULTS: Defaults = {
/** Used to serialize data, to be used by public APIs */
export const VIRTUAL_MODULE_ID = 'virtual:astro:assets/fonts/internal';
export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
export const BUFFER_VIRTUAL_MODULE_ID_PREFIX = 'virtual:astro:assets/fonts/file/';
export const RESOLVED_BUFFER_VIRTUAL_MODULE_ID_PREFIX = '\0' + BUFFER_VIRTUAL_MODULE_ID_PREFIX;

export const ASSETS_DIR = 'fonts';
export const CACHE_DIR = './fonts/';
Expand Down
36 changes: 35 additions & 1 deletion packages/astro/src/assets/fonts/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
import type { ConsumableMap, PreloadData, PreloadFilter } from './types.js';
import type { BufferImports, ConsumableMap, PreloadData, PreloadFilter } from './types.js';

export function createGetFontData({ consumableMap }: { consumableMap?: ConsumableMap }) {
return function getFontData(cssVariable: string) {
Expand All @@ -18,6 +18,40 @@ export function createGetFontData({ consumableMap }: { consumableMap?: Consumabl
};
}

export function createGetFontBuffer({ bufferImports }: { bufferImports?: BufferImports }) {
return async function getFontBuffer(url: string) {
// TODO: remove once fonts are stabilized
if (!bufferImports) {
throw new AstroError(AstroErrorData.ExperimentalFontsNotEnabled);
}
// Should always be able to split but we default to a hash that will always fail
const hash = url.split('/').pop() ?? '';
const fn = bufferImports[hash];
if (!fn) {
throw new AstroError({
...AstroErrorData.FontBufferNotFound,
message: AstroErrorData.FontBufferNotFound.message(url),
});
}
let mod;
try {
mod = await fn();
} catch {
throw new AstroError({
...AstroErrorData.FontBufferNotFound,
message: AstroErrorData.FontBufferNotFound.message(url),
});
}
if (!mod?.default) {
throw new AstroError({
...AstroErrorData.FontBufferNotFound,
message: AstroErrorData.FontBufferNotFound.message(url),
});
}
return mod.default;
};
}

export function filterPreloads(
data: Array<PreloadData>,
preload: PreloadFilter,
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/assets/fonts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,5 @@ export interface ResolveFontOptions {
subsets: string[];
formats: FontType[];
}

export type BufferImports = Record<string, () => Promise<{ default: Buffer | null }>>;
46 changes: 42 additions & 4 deletions packages/astro/src/assets/fonts/vite-plugin-fonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import { getClientOutputDirectory } from '../../prerender/utils.js';
import type { AstroSettings } from '../../types/astro.js';
import {
ASSETS_DIR,
BUFFER_VIRTUAL_MODULE_ID_PREFIX,
CACHE_DIR,
DEFAULTS,
RESOLVED_BUFFER_VIRTUAL_MODULE_ID_PREFIX,
RESOLVED_VIRTUAL_MODULE_ID,
VIRTUAL_MODULE_ID,
} from './constants.js';
Expand Down Expand Up @@ -273,12 +275,12 @@ 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, ...associatedData });
const buffer = await fontFetcher!.fetch({ hash, ...associatedData });

res.setHeader('Content-Length', data.length);
res.setHeader('Content-Length', buffer.length);
res.setHeader('Content-Type', `font/${fontTypeExtractor!.extract(hash)}`);

res.end(data);
res.end(buffer);
} catch (err) {
logger.error('assets', 'Cannot download font file');
if (isAstroError(err)) {
Expand All @@ -296,16 +298,52 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
if (id === VIRTUAL_MODULE_ID) {
return RESOLVED_VIRTUAL_MODULE_ID;
}
if (id.startsWith(BUFFER_VIRTUAL_MODULE_ID_PREFIX)) {
return `\0` + id;
}
},
load(id) {
async load(id) {
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
return {
code: `
export const internalConsumableMap = new Map(${JSON.stringify(Array.from(internalConsumableMap?.entries() ?? []))});
export const consumableMap = new Map(${JSON.stringify(Array.from(consumableMap?.entries() ?? []))});
export const bufferImports = {${[...(fontFileDataMap?.keys() ?? [])].map((key) => `"${key}": () => import("virtual:astro:assets/fonts/file/${key}")`).join(',')}};
`,
};
}
if (id.startsWith(RESOLVED_BUFFER_VIRTUAL_MODULE_ID_PREFIX)) {
const hash = id.slice(RESOLVED_BUFFER_VIRTUAL_MODULE_ID_PREFIX.length);
const associatedData = fontFileDataMap?.get(hash);
if (!associatedData) {
return {
code: `export default null;`,
};
}

try {
// 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 buffer = await fontFetcher!.fetch({ hash, ...associatedData });

const bytes = Array.from(buffer);
return {
code: `export default Uint8Array.from(${JSON.stringify(bytes)});`,
};
} catch (err) {
logger.error('assets', 'Cannot download font file');
if (isAstroError(err)) {
logger.error(
'SKIP_FORMAT',
formatErrorMessage(collectErrorMetadata(err), logger.level() === 'debug'),
);
}
return {
code: `export default null;`,
};
}
}
},
async buildEnd() {
if (sync || settings.config.experimental.fonts!.length === 0) {
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/assets/vite-plugin-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export default function assets({ fs, settings, sync, logger }: Options): vite.Pl

export { default as Font } from "astro/components/Font.astro";
import * as fontsMod from 'virtual:astro:assets/fonts/internal';
import { createGetFontData } from "astro/assets/fonts/runtime";
import { createGetFontData, createGetFontBuffer } from "astro/assets/fonts/runtime";

export const viteFSConfig = ${JSON.stringify(resolvedConfig.server.fs ?? {})};

Expand Down Expand Up @@ -191,6 +191,7 @@ export default function assets({ fs, settings, sync, logger }: Options): vite.Pl
export const getImage = async (options) => await getImageInternal(options, imageConfig);

export const getFontData = createGetFontData(fontsMod);
export const getFontBuffer = createGetFontBuffer(fontsMod);
`,
};
}
Expand Down
15 changes: 15 additions & 0 deletions packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,21 @@ export const FontFamilyNotFound = {
hint: 'This is often caused by a typo. Check that the `<Font />` component or `getFontData()` function are using a `cssVariable` specified in your config.',
} satisfies ErrorData;

/**
* @docs
* @description
* Font buffer not found
* @message
* No buffer was found for the URL passed to the `getFontBuffer()` function.
*/
export const FontBufferNotFound = {
name: 'FontBufferNotFound',
title: 'Font buffer not found',
message: (url: string) =>
`No buffer was found for the \`"${url}"\` passed to the \`getFontBuffer()\` function.`,
hint: 'Make sure you pass a valid URL, obtained via the \`getFontData()\` function.',
} satisfies ErrorData;

/**
* @docs
* @description
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
import { getFontBuffer, getFontData } from 'astro:assets'

const buffer = await getFontBuffer(getFontData('--font-test')[0].src[0].url)
---

<pre id="length">{buffer.length}</pre>
Loading
Loading