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
7 changes: 0 additions & 7 deletions .changeset/add-favicon-ico-fallbacks.md

This file was deleted.

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, when 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 @@ -55,6 +55,7 @@ declare module 'astro:assets' {
Picture: typeof import('./components/Picture.astro').default;
Font: typeof import('./components/Font.astro').default;
fontData: Record<import('astro:assets').CssVariable, Array<import('astro:assets').FontData>>;
getFontBuffer: (url: string) => Promise<Buffer>;
};

type ImgAttributes = import('./dist/type-utils.js').WithRequired<
Expand All @@ -77,6 +78,7 @@ declare module 'astro:assets' {
Font,
inferRemoteSize,
fontData,
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 fontData: import('./src/assets/fonts/types.js').FontDataRecord;
export const bufferImports: import('./src/assets/fonts/types.js').BufferImports;
}

declare module 'virtual:astro:adapter-config/client' {
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
"./assets/endpoint/*": "./dist/assets/endpoint/*.js",
"./assets/services/sharp": "./dist/assets/services/sharp.js",
"./assets/services/noop": "./dist/assets/services/noop.js",
"./assets/fonts/runtime": "./dist/assets/fonts/runtime.js",
"./assets/fonts/runtime/server.js": "./dist/assets/fonts/runtime/server.js",
"./assets/fonts/runtime/client.js": "./dist/assets/fonts/runtime/client.js",
"./loaders": "./dist/content/loaders/index.js",
"./content/config": "./dist/content/config.js",
"./content/runtime": "./dist/content/runtime.js",
Expand Down
7 changes: 6 additions & 1 deletion packages/astro/src/assets/fonts/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ export const DEFAULTS: Defaults = {
formats: ['woff2'],
};

/** 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 RUNTIME_VIRTUAL_MODULE_ID = 'virtual:astro:assets/fonts/runtime';
export const RESOLVED_RUNTIME_VIRTUAL_MODULE_ID = '\0' + RUNTIME_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: 36 additions & 0 deletions packages/astro/src/assets/fonts/core/create-get-font-buffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
import type { BufferImports } from './../types.js';

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;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ import * as fontsMod from 'virtual:astro:assets/fonts/internal';

// TODO: remove default when stabilizing
export const fontData = fontsMod.fontData ?? {};

export async function getFontBuffer() {
throw new Error('[astro:assets] `getFontBuffer()` is not available on the client.');
}
7 changes: 7 additions & 0 deletions packages/astro/src/assets/fonts/runtime/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as fontsMod from 'virtual:astro:assets/fonts/internal';
import { createGetFontBuffer } from '../core/create-get-font-buffer.js';

// TODO: remove default when stabilizing
export const fontData = fontsMod.fontData ?? {};

export const getFontBuffer = createGetFontBuffer(fontsMod);
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 @@ -335,3 +335,5 @@ export interface ResolveFontOptions<
formats: FontType[];
options: [FamilyOptions] extends [never] ? undefined : FamilyOptions | undefined;
}

export type BufferImports = Record<string, () => Promise<{ default: Buffer | null }>>;
76 changes: 71 additions & 5 deletions packages/astro/src/assets/fonts/vite-plugin-fonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ 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_RUNTIME_VIRTUAL_MODULE_ID,
RESOLVED_VIRTUAL_MODULE_ID,
RUNTIME_VIRTUAL_MODULE_ID,
VIRTUAL_MODULE_ID,
} from './constants.js';
import type {
Expand Down Expand Up @@ -73,9 +77,19 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
if (id === VIRTUAL_MODULE_ID) {
return RESOLVED_VIRTUAL_MODULE_ID;
}
if (id === RUNTIME_VIRTUAL_MODULE_ID) {
return RESOLVED_RUNTIME_VIRTUAL_MODULE_ID;
}
if (id.startsWith(BUFFER_VIRTUAL_MODULE_ID_PREFIX)) {
return `\0` + id;
}
},
load(id) {
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
if (
id === RESOLVED_VIRTUAL_MODULE_ID ||
id === RESOLVED_RUNTIME_VIRTUAL_MODULE_ID ||
id.startsWith(RESOLVED_BUFFER_VIRTUAL_MODULE_ID_PREFIX)
) {
return {
code: '',
};
Expand Down Expand Up @@ -278,12 +292,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.byteLength);
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 @@ -301,16 +315,68 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
if (id === VIRTUAL_MODULE_ID) {
return RESOLVED_VIRTUAL_MODULE_ID;
}
if (id === RUNTIME_VIRTUAL_MODULE_ID) {
return RESOLVED_RUNTIME_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 fontData = ${JSON.stringify(fontData ?? {})};
export const bufferImports = {${[...(fontFileDataMap?.keys() ?? [])].map((key) => `"${key}": () => import("${BUFFER_VIRTUAL_MODULE_ID_PREFIX}${key}")`).join(',')}};
`,
};
}

if (id === RESOLVED_RUNTIME_VIRTUAL_MODULE_ID) {
// TODO: use ASTRO_VITE_ENVIRONMENT_NAMES.client
if (this.environment.name === 'client') {
return {
code: `export * from 'astro/assets/fonts/runtime/client.js';`,
};
}
return {
code: `export * from 'astro/assets/fonts/runtime/server.js';`,
};
}

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 || !isBuild) {
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 @@ -14,6 +14,7 @@ import {
import { normalizePath } from '../core/viteUtils.js';
import type { AstroSettings } from '../types/astro.js';
import { VALID_INPUT_FORMATS, VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js';
import { RUNTIME_VIRTUAL_MODULE_ID } from './fonts/constants.js';
import { fontsPlugin } from './fonts/vite-plugin-fonts.js';
import type { ImageTransform } from './types.js';
import { getAssetsPrefix } from './utils/getAssetsPrefix.js';
Expand Down Expand Up @@ -149,7 +150,7 @@ export default function assets({ fs, settings, sync, logger }: Options): vite.Pl
export { inferRemoteSize } from "astro/assets/utils/inferRemoteSize.js";

export { default as Font } from "astro/components/Font.astro";
export * from "astro/assets/fonts/runtime";
export * from "${RUNTIME_VIRTUAL_MODULE_ID}";

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

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 is 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 \`fontData\` object.',
} satisfies ErrorData;

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

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

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