Skip to content

Commit 9b726c4

Browse files
feat(fonts)!: update font provider API (#15130)
Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com>
1 parent 967f69d commit 9b726c4

29 files changed

Lines changed: 613 additions & 648 deletions

.changeset/forty-zebras-enter.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
**BREAKING CHANGE to the experimental Fonts API only**
6+
7+
Changes how font providers are implemented with updates to the `FontProvider` type
8+
9+
This is an implementation detail that changes how font providers are created. This process allows Astro to take more control rather than relying directly on `unifont` types. **All of Astro's built-in font providers have been updated to reflect this new type, and can be configured as before**. However, using third-party unifont providers that rely on `unifont` types will require an update to your project code.
10+
11+
Previously, an Astro `FontProvider` was made of a config and a runtime part. It relied directly on `unifont` types, which allowed a simple configuration for third-party unifont providers, but also coupled Astro's implementation to unifont, which was limiting.
12+
13+
Astro's font provider implementation is now only made of a config part with dedicated hooks. This allows for the separation of config and runtime, but requires you to create a font provider object in order to use custom font providers (e.g. third-party unifont providers, or private font registeries).
14+
15+
#### What should I do?
16+
17+
If you were using a 3rd-party `unifont` font provider, you will now need to write an Astro `FontProvider` using it under the hood. For example:
18+
19+
```diff
20+
// astro.config.ts
21+
import { defineConfig } from "astro/config";
22+
import { acmeProvider, type AcmeOptions } from '@acme/unifont-provider'
23+
+import type { FontProvider } from "astro";
24+
+import type { InitializedProvider } from 'unifont';
25+
26+
+function acme(config?: AcmeOptions): FontProvider {
27+
+ const provider = acmeProvider(config);
28+
+ let initializedProvider: InitializedProvider | undefined;
29+
+ return {
30+
+ name: provider._name,
31+
+ config,
32+
+ async init(context) {
33+
+ initializedProvider = await provider(context);
34+
+ },
35+
+ async resolveFont({ familyName, ...rest }) {
36+
+ return await initializedProvider?.resolveFont(familyName, rest);
37+
+ },
38+
+ async listFonts() {
39+
+ return await initializedProvider?.listFonts?.();
40+
+ },
41+
+ };
42+
+}
43+
44+
export default defineConfig({
45+
experimental: {
46+
fonts: [{
47+
- provider: acmeProvider({ /* ... */ }),
48+
+ provider: acme({ /* ... */ }),
49+
name: "Material Symbols Outlined",
50+
cssVariable: "--font-material"
51+
}]
52+
}
53+
});
54+
```

packages/astro/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
"./assets/endpoint/*": "./dist/assets/endpoint/*.js",
6464
"./assets/services/sharp": "./dist/assets/services/sharp.js",
6565
"./assets/services/noop": "./dist/assets/services/noop.js",
66-
"./assets/fonts/providers/*": "./dist/assets/fonts/providers/entrypoints/*.js",
6766
"./assets/fonts/runtime": "./dist/assets/fonts/runtime.js",
6867
"./loaders": "./dist/content/loaders/index.js",
6968
"./content/config": "./dist/content/config.js",

packages/astro/src/assets/fonts/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ Here is an overview of the architecture of the fonts in Astro:
44

55
- [`orchestrate()`](./orchestrate.ts) combines sub steps and takes care of getting useful data from the config
66
- It resolves font families (eg. import remote font providers)
7-
- It prepares [`unifont`](https://github.com/unjs/unifont) providers
8-
- It initializes `unifont`
7+
- It initializes the font resolver
98
- For each family, it resolves fonts data and normalizes them
109
- For each family, optimized fallbacks (and related CSS) are generated if applicable
1110
- It returns the data

packages/astro/src/assets/fonts/config.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod';
22
import { FONT_TYPES, LOCAL_PROVIDER_NAME } from './constants.js';
3+
import type { FontProvider } from './types.js';
34

45
export const weightSchema = z.union([z.string(), z.number()]);
56
export const styleSchema = z.enum(['normal', 'italic', 'oblique']);
@@ -53,6 +54,16 @@ export const localFontFamilySchema = z
5354
})
5455
.strict();
5556

57+
export const fontProviderSchema = z
58+
.object({
59+
name: z.string(),
60+
config: z.record(z.string(), z.any()).optional(),
61+
init: z.custom<FontProvider['init']>((v) => typeof v === 'function').optional(),
62+
resolveFont: z.custom<FontProvider['resolveFont']>((v) => typeof v === 'function'),
63+
listFonts: z.custom<FontProvider['listFonts']>((v) => typeof v === 'function').optional(),
64+
})
65+
.strict();
66+
5667
export const remoteFontFamilySchema = z
5768
.object({
5869
...requiredFamilyAttributesSchema.shape,
@@ -61,12 +72,7 @@ export const remoteFontFamilySchema = z
6172
weight: true,
6273
style: true,
6374
}).shape,
64-
provider: z
65-
.object({
66-
entrypoint: entrypointSchema,
67-
config: z.record(z.string(), z.any()).optional(),
68-
})
69-
.strict(),
75+
provider: fontProviderSchema,
7076
weights: z.array(weightSchema).nonempty().optional(),
7177
styles: z.array(styleSchema).nonempty().optional(),
7278
subsets: z.array(z.string()).nonempty().optional(),

packages/astro/src/assets/fonts/core/resolve-families.ts

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import { LOCAL_PROVIDER_NAME } from '../constants.js';
2-
import type {
3-
Hasher,
4-
LocalProviderUrlResolver,
5-
RemoteFontProviderResolver,
6-
} from '../definitions.js';
2+
import type { Hasher, LocalProviderUrlResolver } from '../definitions.js';
73
import type {
84
FontFamily,
95
LocalFontFamily,
@@ -38,17 +34,15 @@ function resolveVariants({
3834
/**
3935
* Dedupes properties if applicable and resolves entrypoints.
4036
*/
41-
export async function resolveFamily({
37+
export function resolveFamily({
4238
family,
4339
hasher,
44-
remoteFontProviderResolver,
4540
localProviderUrlResolver,
4641
}: {
4742
family: FontFamily;
4843
hasher: Hasher;
49-
remoteFontProviderResolver: RemoteFontProviderResolver;
5044
localProviderUrlResolver: LocalProviderUrlResolver;
51-
}): Promise<ResolvedFontFamily> {
45+
}): ResolvedFontFamily {
5246
// We remove quotes from the name so they can be properly resolved by providers.
5347
const name = withoutQuotes(family.name);
5448
// This will be used in CSS font faces. Quotes are added by the CSS renderer if
@@ -75,26 +69,23 @@ export async function resolveFamily({
7569
formats: family.formats ? dedupe(family.formats) : undefined,
7670
fallbacks: family.fallbacks ? dedupe(family.fallbacks) : undefined,
7771
unicodeRange: family.unicodeRange ? dedupe(family.unicodeRange) : undefined,
78-
// This will be Astro specific eventually
79-
provider: await remoteFontProviderResolver.resolve(family.provider),
8072
};
8173
}
8274

8375
/**
8476
* A function for convenience. The actual logic lives in resolveFamily
8577
*/
86-
export async function resolveFamilies({
78+
export function resolveFamilies({
8779
families,
8880
...dependencies
89-
}: { families: Array<FontFamily> } & Omit<Parameters<typeof resolveFamily>[0], 'family'>): Promise<
90-
Array<ResolvedFontFamily>
91-
> {
92-
return await Promise.all(
93-
families.map((family) =>
94-
resolveFamily({
95-
family,
96-
...dependencies,
97-
}),
98-
),
81+
}: { families: Array<FontFamily> } & Omit<
82+
Parameters<typeof resolveFamily>[0],
83+
'family'
84+
>): Array<ResolvedFontFamily> {
85+
return families.map((family) =>
86+
resolveFamily({
87+
family,
88+
...dependencies,
89+
}),
9990
);
10091
}

packages/astro/src/assets/fonts/definitions.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ import type { CollectedFontForMetrics } from './core/optimize-fallbacks.js';
33
import type {
44
FontFaceMetrics,
55
FontFileData,
6-
FontProvider,
76
FontType,
87
GenericFallbackName,
98
PreloadData,
10-
ResolvedFontProvider,
119
ResolveFontOptions,
1210
Style,
1311
} from './types.js';
@@ -17,14 +15,6 @@ export interface Hasher {
1715
hashObject: (input: Record<string, any>) => string;
1816
}
1917

20-
export interface RemoteFontProviderModResolver {
21-
resolve: (id: string) => Promise<any>;
22-
}
23-
24-
export interface RemoteFontProviderResolver {
25-
resolve: (provider: FontProvider) => Promise<ResolvedFontProvider>;
26-
}
27-
2818
export interface LocalProviderUrlResolver {
2919
resolve: (input: string) => string;
3020
}

packages/astro/src/assets/fonts/infra/build-remote-font-provider-mod-resolver.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/astro/src/assets/fonts/infra/dev-remote-font-provider-mod-resolver.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

packages/astro/src/assets/fonts/infra/remote-font-provider-resolver.ts

Lines changed: 0 additions & 62 deletions
This file was deleted.

packages/astro/src/assets/fonts/infra/require-local-provider-url-resolver.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { fileURLToPath } from 'node:url';
1+
import { createRequire } from 'node:module';
2+
import { fileURLToPath, pathToFileURL } from 'node:url';
23
import type { LocalProviderUrlResolver } from '../definitions.js';
3-
import { resolveEntrypoint } from '../utils.js';
44

55
export class RequireLocalProviderUrlResolver implements LocalProviderUrlResolver {
66
readonly #root: URL;
@@ -18,10 +18,20 @@ export class RequireLocalProviderUrlResolver implements LocalProviderUrlResolver
1818
this.#intercept = intercept;
1919
}
2020

21+
#resolveEntrypoint(root: URL, entrypoint: string): URL {
22+
const require = createRequire(root);
23+
24+
try {
25+
return pathToFileURL(require.resolve(entrypoint));
26+
} catch {
27+
return new URL(entrypoint, root);
28+
}
29+
}
30+
2131
resolve(input: string): string {
2232
// fileURLToPath is important so that the file can be read
2333
// by createLocalUrlProxyContentResolver
24-
const path = fileURLToPath(resolveEntrypoint(this.#root, input));
34+
const path = fileURLToPath(this.#resolveEntrypoint(this.#root, input));
2535
this.#intercept?.(path);
2636
return path;
2737
}

0 commit comments

Comments
 (0)