feat(fonts): config (#12777)

This commit is contained in:
Florian Lefebvre 2025-01-21 16:09:48 +01:00 committed by GitHub
parent 21b6f35c32
commit 95673fcd21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 466 additions and 33 deletions

View file

@ -113,7 +113,7 @@
"test:e2e:match": "playwright test -g", "test:e2e:match": "playwright test -g",
"test:e2e:chrome": "playwright test", "test:e2e:chrome": "playwright test",
"test:e2e:firefox": "playwright test --config playwright.firefox.config.js", "test:e2e:firefox": "playwright test --config playwright.firefox.config.js",
"test:types": "tsc --project tsconfig.tests.json", "test:types": "tsc --project test/types/tsconfig.json",
"test:unit": "astro-scripts test \"test/units/**/*.test.js\" --teardown ./test/units/teardown.js", "test:unit": "astro-scripts test \"test/units/**/*.test.js\" --teardown ./test/units/teardown.js",
"test:integration": "astro-scripts test \"test/*.test.js\"" "test:integration": "astro-scripts test \"test/*.test.js\""
}, },

View file

@ -0,0 +1,4 @@
import { GOOGLE_PROVIDER_NAME } from "./providers/google.js";
import { LOCAL_PROVIDER_NAME } from "./providers/local.js";
export const BUILTIN_PROVIDERS = [GOOGLE_PROVIDER_NAME, LOCAL_PROVIDER_NAME] as const;

View file

@ -0,0 +1,5 @@
import type { FontProvider } from './types.js';
export function defineFontProvider<TName extends string>(provider: FontProvider<TName>) {
return provider;
}

View file

@ -0,0 +1,6 @@
import { adobe } from './providers/adobe.js';
/** TODO: */
export const fontProviders = {
adobe,
};

View file

@ -0,0 +1,9 @@
import { defineFontProvider } from '../helpers.js';
export function adobe(config: { apiKey: string }) {
return defineFontProvider({
name: 'adobe',
entrypoint: 'astro/assets/fonts/providers/adobe',
config,
});
}

View file

@ -0,0 +1,10 @@
import { defineFontProvider } from '../helpers.js';
export const GOOGLE_PROVIDER_NAME = 'google';
export function google() {
return defineFontProvider({
name: GOOGLE_PROVIDER_NAME,
entrypoint: 'astro/assets/fonts/providers/google',
});
}

View file

@ -0,0 +1,10 @@
import { defineFontProvider } from '../helpers.js';
export const LOCAL_PROVIDER_NAME = 'local';
export function local() {
return defineFontProvider({
name: LOCAL_PROVIDER_NAME,
entrypoint: 'astro/assets/fonts/providers/google',
});
}

View file

@ -0,0 +1,27 @@
import type { BUILTIN_PROVIDERS } from './constants.js';
import type { GOOGLE_PROVIDER_NAME } from './providers/google.js';
import type { LOCAL_PROVIDER_NAME } from './providers/local.js';
export interface FontProvider<TName extends string> {
name: TName;
entrypoint: string;
config?: Record<string, any>;
}
type LocalFontFamily = {
provider: LocalProviderName;
// TODO: refine type
src: string;
};
type StandardFontFamily<TProvider extends string> = {
provider: TProvider;
};
export type FontFamily<TProvider extends string> = TProvider extends LocalProviderName
? LocalFontFamily
: StandardFontFamily<TProvider>;
export type LocalProviderName = typeof LOCAL_PROVIDER_NAME;
export type GoogleProviderName = typeof GOOGLE_PROVIDER_NAME;
export type BuiltInProvider = (typeof BUILTIN_PROVIDERS)[number];

View file

@ -5,6 +5,7 @@ import type { ImageServiceConfig } from '../types/public/index.js';
export { defineConfig, getViteConfig } from './index.js'; export { defineConfig, getViteConfig } from './index.js';
export { envField } from '../env/config.js'; export { envField } from '../env/config.js';
export { fontProviders } from '../assets/fonts/providers.js';
/** /**
* Return the configuration needed to use the Sharp-based image service * Return the configuration needed to use the Sharp-based image service

View file

@ -7,6 +7,7 @@ import type {
SessionDriverName, SessionDriverName,
} from '../types/public/config.js'; } from '../types/public/config.js';
import { createDevelopmentManifest } from '../vite-plugin-astro-server/plugin.js'; import { createDevelopmentManifest } from '../vite-plugin-astro-server/plugin.js';
import type { BuiltInProvider, FontFamily, FontProvider } from '../assets/fonts/types.js';
/** /**
* See the full Astro Configuration API Documentation * See the full Astro Configuration API Documentation
@ -15,7 +16,11 @@ import { createDevelopmentManifest } from '../vite-plugin-astro-server/plugin.js
export function defineConfig< export function defineConfig<
const TLocales extends Locales = never, const TLocales extends Locales = never,
const TDriver extends SessionDriverName = never, const TDriver extends SessionDriverName = never,
>(config: AstroUserConfig<TLocales, TDriver>) { const TFontProviders extends FontProvider<string>[] = never,
const TFontFamilies extends FontFamily<
(TFontProviders extends never ? [] : TFontProviders)[number]['name'] | BuiltInProvider
>[] = never,
>(config: AstroUserConfig<TLocales, TDriver, TFontProviders, TFontFamilies>) {
return config; return config;
} }

View file

@ -14,6 +14,7 @@ import type { SvgRenderMode } from '../../assets/utils/svg.js';
import { EnvSchema } from '../../env/schema.js'; import { EnvSchema } from '../../env/schema.js';
import type { AstroUserConfig, ViteUserConfig } from '../../types/public/config.js'; import type { AstroUserConfig, ViteUserConfig } from '../../types/public/config.js';
import { appendForwardSlash, prependForwardSlash, removeTrailingForwardSlash } from '../path.js'; import { appendForwardSlash, prependForwardSlash, removeTrailingForwardSlash } from '../path.js';
import { BUILTIN_PROVIDERS } from '../../assets/fonts/constants.js';
// The below types are required boilerplate to workaround a Zod issue since v3.21.2. Since that version, // The below types are required boilerplate to workaround a Zod issue since v3.21.2. Since that version,
// Zod's compiled TypeScript would "simplify" certain values to their base representation, causing references // Zod's compiled TypeScript would "simplify" certain values to their base representation, causing references
@ -589,6 +590,53 @@ export const AstroConfigSchema = z.object({
} }
return svgConfig; return svgConfig;
}), }),
fonts: z
.object({
providers: z
.array(
z
.object({
name: z.string().superRefine((name, ctx) => {
if (BUILTIN_PROVIDERS.includes(name as any)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `"${name}" is a reserved provider name`,
});
}
}),
entrypoint: z.string(),
config: z.record(z.string(), z.any()).optional(),
})
.strict(),
)
.optional(),
families: z.array(
z
.object({
provider: z.string(),
})
.strict(),
),
})
.strict()
.optional()
.superRefine((fonts, ctx) => {
if (!fonts) {
return;
}
const providersNames = [
...BUILTIN_PROVIDERS,
...(fonts.providers ?? []).map((provider) => provider.name),
];
for (const family of fonts.families) {
if (!providersNames.includes(family.provider)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid provider "${family.provider}". Please use of the following: ${providersNames.map((name) => `"${name}"`).join(', ')}`,
});
}
}
}),
}) })
.strict( .strict(
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/experimental-flags/ for a list of all current experiments.`, `Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/experimental-flags/ for a list of all current experiments.`,

View file

@ -17,8 +17,11 @@ import type { AstroCookieSetOptions } from '../../core/cookies/cookies.js';
import type { Logger, LoggerLevel } from '../../core/logger/core.js'; import type { Logger, LoggerLevel } from '../../core/logger/core.js';
import type { EnvSchema } from '../../env/schema.js'; import type { EnvSchema } from '../../env/schema.js';
import type { AstroIntegration } from './integrations.js'; import type { AstroIntegration } from './integrations.js';
import type { BuiltInProvider, FontFamily, FontProvider } from '../../assets/fonts/types.js';
export type Locales = (string | { codes: string[]; path: string })[]; export type Locales = (string | { codes: string[]; path: string })[];
export type { FontProvider };
type NormalizeLocales<T extends Locales> = { type NormalizeLocales<T extends Locales> = {
[K in keyof T]: T[K] extends string [K in keyof T]: T[K] extends string
? T[K] ? T[K]
@ -164,6 +167,10 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
*/ export interface AstroUserConfig< */ export interface AstroUserConfig<
TLocales extends Locales = never, TLocales extends Locales = never,
TSession extends SessionDriverName = never, TSession extends SessionDriverName = never,
TFontProviders extends FontProvider<string>[] = never,
TFontFamilies extends FontFamily<
(TFontProviders extends never ? [] : TFontProviders)[number]['name'] | BuiltInProvider
>[] = never,
> { > {
/** /**
* @docs * @docs
@ -2059,6 +2066,45 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
*/ */
mode: SvgRenderMode; mode: SvgRenderMode;
}; };
/**
*
* @name experimental.fonts
* @type {object}
* @default `undefined`
* @version 5.x
* @description
*
* TODO:
*/
fonts?: {
/**
*
* @name experimental.fonts.providers
* @type {FontProvider[]}
* @version 5.x
* @description
*
* TODO:
*/
providers?: [TFontProviders] extends [never] ? FontProvider<string>[] : TFontProviders;
/**
*
* @name experimental.fonts.families
* @type {FontFamily[]}
* @version 5.x
* @description
*
* TODO:
*/
families: [TFontFamilies] extends [never]
? FontFamily<
| ([TFontProviders] extends [never] ? [] : TFontProviders)[number]['name']
| BuiltInProvider
>[]
: TFontFamilies;
};
}; };
} }

View file

@ -2,40 +2,234 @@ import { describe, it } from 'node:test';
import { expectTypeOf } from 'expect-type'; import { expectTypeOf } from 'expect-type';
import { defineConfig } from '../../dist/config/index.js'; import { defineConfig } from '../../dist/config/index.js';
import type { AstroUserConfig } from '../../dist/types/public/index.js'; import type { AstroUserConfig } from '../../dist/types/public/index.js';
import type { FontFamily, FontProvider } from '../../dist/assets/fonts/types.js';
function assertType<T>(data: T, cb: (data: NoInfer<T>) => void) {
cb(data);
}
describe('defineConfig()', () => { describe('defineConfig()', () => {
it('Infers generics correctly', () => { it('Infers i18n generics correctly', () => {
const config_0 = defineConfig({}); assertType(defineConfig({}), (config) => {
expectTypeOf(config_0).toEqualTypeOf<AstroUserConfig<never>>(); expectTypeOf(config).toEqualTypeOf<AstroUserConfig<never, never, never, never>>();
expectTypeOf(config_0.i18n!.defaultLocale).toEqualTypeOf<string>(); expectTypeOf(config.i18n!.defaultLocale).toEqualTypeOf<string>();
const config_1 = defineConfig({
i18n: {
locales: ['en'],
defaultLocale: 'en',
},
}); });
expectTypeOf(config_1).toEqualTypeOf<AstroUserConfig<['en']>>();
expectTypeOf(config_1.i18n!.defaultLocale).toEqualTypeOf<'en'>();
const config_2 = defineConfig({ assertType(
i18n: { defineConfig({
locales: ['en', 'fr'], i18n: {
defaultLocale: 'fr', locales: ['en'],
defaultLocale: 'en',
},
}),
(config) => {
expectTypeOf(config).toEqualTypeOf<AstroUserConfig<['en'], never, never, never>>();
expectTypeOf(config.i18n!.defaultLocale).toEqualTypeOf<'en'>();
}, },
}); );
expectTypeOf(config_2).toEqualTypeOf<AstroUserConfig<['en', 'fr']>>();
expectTypeOf(config_2.i18n!.defaultLocale).toEqualTypeOf<'en' | 'fr'>();
const config_3 = defineConfig({ assertType(
i18n: { defineConfig({
locales: ['en', { path: 'french', codes: ['fr', 'fr-FR'] }], i18n: {
defaultLocale: 'en', locales: ['en', 'fr'],
defaultLocale: 'fr',
},
}),
(config) => {
expectTypeOf(config).toEqualTypeOf<AstroUserConfig<['en', 'fr'], never, never, never>>();
expectTypeOf(config.i18n!.defaultLocale).toEqualTypeOf<'en' | 'fr'>();
}, },
);
assertType(
defineConfig({
i18n: {
locales: ['en', { path: 'french', codes: ['fr', 'fr-FR'] }],
defaultLocale: 'en',
},
}),
(config) => {
expectTypeOf(config).toEqualTypeOf<
AstroUserConfig<
['en', { readonly path: 'french'; readonly codes: ['fr', 'fr-FR'] }],
never,
never,
never
>
>();
expectTypeOf(config.i18n!.defaultLocale).toEqualTypeOf<'en' | 'fr' | 'fr-FR'>();
},
);
});
it('Infers fonts generics correctly', () => {
assertType(defineConfig({}), (config) => {
expectTypeOf(config).toEqualTypeOf<AstroUserConfig<never, never, never, never>>();
expectTypeOf(config.experimental!.fonts!.providers!).toEqualTypeOf<FontProvider<string>[]>();
expectTypeOf(config.experimental!.fonts!.families).toEqualTypeOf<
FontFamily<'google' | 'local'>[]
>();
}); });
expectTypeOf(config_3).toEqualTypeOf<
AstroUserConfig<['en', { readonly path: 'french'; readonly codes: ['fr', 'fr-FR'] }]> assertType(
>(); defineConfig({
expectTypeOf(config_3.i18n!.defaultLocale).toEqualTypeOf<'en' | 'fr' | 'fr-FR'>(); experimental: {
fonts: {
families: [],
},
},
}),
(config) => {
expectTypeOf(config).toEqualTypeOf<AstroUserConfig<never, never, never, []>>();
expectTypeOf(config.experimental!.fonts!.providers!).toEqualTypeOf<
FontProvider<string>[]
>();
expectTypeOf(config.experimental!.fonts!.families).toEqualTypeOf<[]>();
},
);
assertType(
defineConfig({
experimental: {
fonts: {
families: [{ provider: 'google' }, { provider: 'local', src: 'test' }],
},
},
}),
(config) => {
expectTypeOf(config).toEqualTypeOf<
AstroUserConfig<
never,
never,
never,
[
{ readonly provider: 'google' },
{
readonly provider: 'local';
readonly src: 'test';
},
]
>
>();
expectTypeOf(config.experimental!.fonts!.providers!).toEqualTypeOf<
FontProvider<string>[]
>();
expectTypeOf(config.experimental!.fonts!.families).toEqualTypeOf<
[
{ readonly provider: 'google' },
{
readonly provider: 'local';
readonly src: 'test';
},
]
>();
},
);
assertType(
defineConfig({
experimental: {
fonts: {
providers: [],
families: [],
},
},
}),
(config) => {
expectTypeOf(config).toEqualTypeOf<AstroUserConfig<never, never, [], []>>();
expectTypeOf(config.experimental!.fonts!.providers!).toEqualTypeOf<[]>();
expectTypeOf(config.experimental!.fonts!.families).toEqualTypeOf<[]>();
},
);
assertType(
defineConfig({
experimental: {
fonts: {
providers: [{ name: 'adobe', entrypoint: '' }],
families: [],
},
},
}),
(config) => {
expectTypeOf(config).toEqualTypeOf<
AstroUserConfig<
never,
never,
[
{
readonly name: 'adobe';
readonly entrypoint: '';
},
],
[]
>
>();
expectTypeOf(config.experimental!.fonts!.providers!).toEqualTypeOf<
[
{
readonly name: 'adobe';
readonly entrypoint: '';
},
]
>();
expectTypeOf(config.experimental!.fonts!.families).toEqualTypeOf<[]>();
},
);
assertType(
defineConfig({
experimental: {
fonts: {
providers: [{ name: 'adobe', entrypoint: '' }],
families: [
{ provider: 'google' },
{ provider: 'local', src: 'test' },
{ provider: 'adobe' },
],
},
},
}),
(config) => {
expectTypeOf(config).toEqualTypeOf<
AstroUserConfig<
never,
never,
[
{
readonly name: 'adobe';
readonly entrypoint: '';
},
],
[
{ readonly provider: 'google' },
{
readonly provider: 'local';
readonly src: 'test';
},
{ readonly provider: 'adobe' },
]
>
>();
expectTypeOf(config.experimental!.fonts!.providers!).toEqualTypeOf<
[
{
readonly name: 'adobe';
readonly entrypoint: '';
},
]
>();
expectTypeOf(config.experimental!.fonts!.families).toEqualTypeOf<
[
{ readonly provider: 'google' },
{
readonly provider: 'local';
readonly src: 'test';
},
{ readonly provider: 'adobe' },
]
>();
},
);
}); });
}); });

View file

@ -1,6 +1,5 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../../../tsconfig.base.json",
"include": ["test/types"],
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"emitDeclarationOnly": false, "emitDeclarationOnly": false,

View file

@ -370,7 +370,7 @@ describe('Config Validation', () => {
}, },
}, },
process.cwd(), process.cwd(),
).catch((err) => err), ),
); );
}); });
@ -385,7 +385,7 @@ describe('Config Validation', () => {
}, },
}, },
process.cwd(), process.cwd(),
).catch((err) => err), ),
); );
}); });
@ -427,4 +427,73 @@ describe('Config Validation', () => {
); );
}); });
}); });
describe('fonts', () => {
it('Should allow empty providers and families', () => {
assert.doesNotThrow(() =>
validateConfig(
{
experimental: {
fonts: {
providers: [],
families: [],
},
},
},
process.cwd(),
),
);
});
it('Should not allow providers with reserved names', async () => {
let configError = await validateConfig(
{
experimental: {
fonts: {
providers: [{ name: 'google', entrypoint: '' }],
families: [],
},
},
},
process.cwd(),
).catch((err) => err);
assert.equal(configError instanceof z.ZodError, true);
assert.equal(
configError.errors[0].message.includes('"google" is a reserved provider name'),
true,
);
configError = await validateConfig(
{
experimental: {
fonts: {
providers: [{ name: 'local', entrypoint: '' }],
families: [],
},
},
},
process.cwd(),
).catch((err) => err);
assert.equal(configError instanceof z.ZodError, true);
assert.equal(
configError.errors[0].message.includes('"local" is a reserved provider name'),
true,
);
});
it('Should not allow using non registed providers', async () => {
const configError = await validateConfig(
{
experimental: {
fonts: {
families: [{ provider: 'custom' }],
},
},
},
process.cwd(),
).catch((err) => err);
assert.equal(configError instanceof z.ZodError, true);
assert.equal(configError.errors[0].message.includes('Invalid provider "custom"'), true);
});
});
}); });