diff --git a/.changeset/soft-memes-sin.md b/.changeset/soft-memes-sin.md new file mode 100644 index 000000000000..3af8a74f496a --- /dev/null +++ b/.changeset/soft-memes-sin.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes the experimental fonts API to correctly take `config.base`, `config.build.assets` and `config.build.assetsPrefix` into account diff --git a/packages/astro/src/assets/fonts/constants.ts b/packages/astro/src/assets/fonts/constants.ts index 09beec38245a..33993ef9c0b3 100644 --- a/packages/astro/src/assets/fonts/constants.ts +++ b/packages/astro/src/assets/fonts/constants.ts @@ -14,8 +14,7 @@ export const DEFAULTS: Defaults = { export const VIRTUAL_MODULE_ID = 'virtual:astro:assets/fonts/internal'; export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; -// Requires a trailing slash -export const URL_PREFIX = '/_astro/fonts/'; +export const ASSETS_DIR = 'fonts'; export const CACHE_DIR = './fonts/'; export const FONT_TYPES = ['woff2', 'woff', 'otf', 'ttf', 'eot'] as const; diff --git a/packages/astro/src/assets/fonts/definitions.ts b/packages/astro/src/assets/fonts/definitions.ts index 52ad80aa09b8..4c3b840c5d69 100644 --- a/packages/astro/src/assets/fonts/definitions.ts +++ b/packages/astro/src/assets/fonts/definitions.ts @@ -60,6 +60,10 @@ export interface UrlProxy { ) => string; } +export interface UrlResolver { + resolve: (hash: string) => string; +} + export interface UrlProxyContentResolver { resolve: (url: string) => string; } diff --git a/packages/astro/src/assets/fonts/implementations/url-proxy.ts b/packages/astro/src/assets/fonts/implementations/url-proxy.ts index a12e7b986f76..d41152c6b18a 100644 --- a/packages/astro/src/assets/fonts/implementations/url-proxy.ts +++ b/packages/astro/src/assets/fonts/implementations/url-proxy.ts @@ -1,20 +1,26 @@ -import type { DataCollector, Hasher, UrlProxy, UrlProxyContentResolver } from '../definitions.js'; +import type { + DataCollector, + Hasher, + UrlProxy, + UrlProxyContentResolver, + UrlResolver, +} from '../definitions.js'; export function createUrlProxy({ - base, contentResolver, hasher, dataCollector, + urlResolver, }: { - base: string; contentResolver: UrlProxyContentResolver; hasher: Hasher; dataCollector: DataCollector; + urlResolver: UrlResolver; }): UrlProxy { return { proxy({ url: originalUrl, type, data, collectPreload, init }) { const hash = `${hasher.hashString(contentResolver.resolve(originalUrl))}.${type}`; - const url = base + hash; + const url = urlResolver.resolve(hash); dataCollector.collect({ url: originalUrl, diff --git a/packages/astro/src/assets/fonts/implementations/url-resolver.ts b/packages/astro/src/assets/fonts/implementations/url-resolver.ts new file mode 100644 index 000000000000..288f05b8802a --- /dev/null +++ b/packages/astro/src/assets/fonts/implementations/url-resolver.ts @@ -0,0 +1,27 @@ +import type { UrlResolver } from '../definitions.js'; +import { fileExtension, joinPaths, prependForwardSlash } from '../../../core/path.js'; +import type { AssetsPrefix } from '../../../types/public/index.js'; +import { getAssetsPrefix } from '../../utils/getAssetsPrefix.js'; + +export function createDevUrlResolver({ base }: { base: string }): UrlResolver { + return { + resolve(hash) { + return prependForwardSlash(joinPaths(base, hash)); + }, + }; +} + +export function createBuildUrlResolver({ + base, + assetsPrefix, +}: { base: string; assetsPrefix: AssetsPrefix }): UrlResolver { + return { + resolve(hash) { + const prefix = assetsPrefix ? getAssetsPrefix(fileExtension(hash), assetsPrefix) : undefined; + if (prefix) { + return joinPaths(prefix, base, hash); + } + return prependForwardSlash(joinPaths(base, hash)); + }, + }; +} diff --git a/packages/astro/src/assets/fonts/vite-plugin-fonts.ts b/packages/astro/src/assets/fonts/vite-plugin-fonts.ts index 44ebcd6f012e..2e076c0de50e 100644 --- a/packages/astro/src/assets/fonts/vite-plugin-fonts.ts +++ b/packages/astro/src/assets/fonts/vite-plugin-fonts.ts @@ -2,7 +2,6 @@ import { mkdirSync, writeFileSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { isAbsolute } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { removeTrailingForwardSlash } from '@astrojs/internal-helpers/path'; import type { Plugin } from 'vite'; import { collectErrorMetadata } from '../../core/errors/dev/utils.js'; import { AstroError, AstroErrorData, isAstroError } from '../../core/errors/index.js'; @@ -11,10 +10,10 @@ import { formatErrorMessage } from '../../core/messages.js'; import { getClientOutputDirectory } from '../../prerender/utils.js'; import type { AstroSettings } from '../../types/astro.js'; import { + ASSETS_DIR, CACHE_DIR, DEFAULTS, RESOLVED_VIRTUAL_MODULE_ID, - URL_PREFIX, VIRTUAL_MODULE_ID, } from './constants.js'; import type { @@ -22,6 +21,7 @@ import type { FontFetcher, FontTypeExtractor, RemoteFontProviderModResolver, + UrlResolver, } from './definitions.js'; import { createMinifiableCssRenderer } from './implementations/css-renderer.js'; import { createDataCollector } from './implementations/data-collector.js'; @@ -46,6 +46,8 @@ import { import { createUrlProxy } from './implementations/url-proxy.js'; import { orchestrate } from './orchestrate.js'; import type { ConsumableMap, FontFileDataMap } from './types.js'; +import { appendForwardSlash, joinPaths, prependForwardSlash } from '../../core/path.js'; +import { createBuildUrlResolver, createDevUrlResolver } from './implementations/url-resolver.js'; interface Options { settings: AstroSettings; @@ -75,10 +77,12 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { }; } - // We don't need to take the trailing slash and build output configuration options - // into account because we only serve (dev) or write (build) static assets (equivalent - // to trailingSlash: never) - const baseUrl = removeTrailingForwardSlash(settings.config.base) + URL_PREFIX; + // We don't need to worry about config.trailingSlash because we are dealing with + // static assets only, ie. trailingSlash: 'never' + const assetsDir = prependForwardSlash( + appendForwardSlash(joinPaths(settings.config.build.assets, ASSETS_DIR)), + ); + const baseUrl = joinPaths(settings.config.base, assetsDir); let fontFileDataMap: FontFileDataMap | null = null; let consumableMap: ConsumableMap | null = null; @@ -96,10 +100,12 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { cacheDir, modResolver, cssRenderer, + urlResolver, }: { cacheDir: URL; modResolver: RemoteFontProviderModResolver; cssRenderer: CssRenderer; + urlResolver: UrlResolver; }) { const { root } = settings.config; // Dependencies. Once extracted to a dedicated vite plugin, those may be passed as @@ -153,7 +159,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { ? createLocalUrlProxyContentResolver({ errorHandler }) : createRemoteUrlProxyContentResolver(); return createUrlProxy({ - base: baseUrl, + urlResolver, contentResolver, hasher, dataCollector, @@ -178,6 +184,10 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { cacheDir: new URL(CACHE_DIR, settings.config.cacheDir), modResolver: createBuildRemoteFontProviderModResolver(), cssRenderer: createMinifiableCssRenderer({ minify: true }), + urlResolver: createBuildUrlResolver({ + base: baseUrl, + assetsPrefix: settings.config.build.assetsPrefix, + }), }); } }, @@ -187,6 +197,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { cacheDir: new URL(CACHE_DIR, settings.dotAstroDir), modResolver: createDevServerRemoteFontProviderModResolver({ server }), cssRenderer: createMinifiableCssRenderer({ minify: false }), + urlResolver: createDevUrlResolver({ base: baseUrl }), }); // 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 @@ -209,9 +220,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { } }); - // Base is taken into account by default. The prefix contains a traling slash, - // so it matches correctly any hash, eg. /_astro/fonts/abc.woff => abc.woff - server.middlewares.use(URL_PREFIX, async (req, res, next) => { + server.middlewares.use(assetsDir, async (req, res, next) => { if (!req.url) { return next(); } @@ -269,7 +278,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin { try { const dir = getClientOutputDirectory(settings); - const fontsDir = new URL('.' + baseUrl, dir); + const fontsDir = new URL(`.${assetsDir}`, dir); try { mkdirSync(fontsDir, { recursive: true }); } catch (cause) { diff --git a/packages/astro/test/fonts.test.js b/packages/astro/test/fonts.test.js index 47faef1c4510..bb8fdb53b1f3 100644 --- a/packages/astro/test/fonts.test.js +++ b/packages/astro/test/fonts.test.js @@ -1,76 +1,101 @@ // @ts-check import assert from 'node:assert/strict'; -import { after, before, describe, it } from 'node:test'; +import { describe, it } from 'node:test'; import { fontProviders } from 'astro/config'; import * as cheerio from 'cheerio'; import { loadFixture } from './test-utils.js'; +import { readdir } from 'node:fs/promises'; -describe('astro:fonts', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; - /** @type {import('./test-utils.js').DevServer} */ - let devServer; +/** + * @param {Omit} inlineConfig + */ +async function createDevFixture(inlineConfig) { + const fixture = await loadFixture({ root: './fixtures/fonts/', ...inlineConfig }); + const devServer = await fixture.startDevServer(); - describe(' component', () => { - // TODO: remove once fonts are stabilized - describe('Fonts are not enabled', () => { - before(async () => { - fixture = await loadFixture({ - root: './fixtures/fonts/', - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { + return { + fixture, + devServer, + run: async (/** @type {() => any} */ cb) => { + try { + return await cb(); + } finally { await devServer.stop(); - }); - - it('Throws an error if fonts are not enabled', async () => { - const res = await fixture.fetch('/'); - const body = await res.text(); - assert.equal( - body.includes('