mirror of
https://github.com/withastro/astro.git
synced 2025-03-10 23:01:26 -05:00
refactor(fonts): url proxy (#13274)
This commit is contained in:
parent
8bc05a0a13
commit
287b789ec6
5 changed files with 118 additions and 28 deletions
|
@ -2,7 +2,7 @@ import type * as unifont from 'unifont';
|
|||
import type { LocalFontFamily } from '../types.js';
|
||||
import { DEFAULTS } from '../constants.js';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { extractFontType } from '../utils.js';
|
||||
import { extractFontType, type URLProxy } from '../utils.js';
|
||||
|
||||
// https://fonts.nuxt.com/get-started/providers#local
|
||||
// https://github.com/nuxt/fonts/blob/main/src/providers/local.ts
|
||||
|
@ -19,7 +19,7 @@ interface Options {
|
|||
}
|
||||
|
||||
interface ResolveOptions {
|
||||
proxySourceURL: (value: string) => string;
|
||||
proxyURL: URLProxy;
|
||||
}
|
||||
|
||||
// TODO: dev watcher and ways to update during dev
|
||||
|
@ -27,7 +27,7 @@ export function createLocalProvider({ root }: Options) {
|
|||
return {
|
||||
resolveFont: async (
|
||||
family: LocalFontFamily,
|
||||
{ proxySourceURL }: ResolveOptions,
|
||||
{ proxyURL }: ResolveOptions,
|
||||
): Promise<ResolveFontResult> => {
|
||||
const fonts: ResolveFontResult['fonts'] = [];
|
||||
|
||||
|
@ -40,7 +40,7 @@ export function createLocalProvider({ root }: Options) {
|
|||
weight,
|
||||
style,
|
||||
src: src.paths.map((path) => ({
|
||||
url: proxySourceURL(fileURLToPath(new URL(path, root))),
|
||||
url: proxyURL(fileURLToPath(new URL(path, root))),
|
||||
format: extractFontType(path),
|
||||
})),
|
||||
});
|
||||
|
|
|
@ -72,4 +72,35 @@ export function createCache(storage: Storage) {
|
|||
};
|
||||
}
|
||||
|
||||
export type CacheHandler = ReturnType<typeof createCache>;
|
||||
export type CacheHandler = ReturnType<typeof createCache>;
|
||||
|
||||
/**
|
||||
* The fonts data we receive contains urls or file paths we do no control.
|
||||
* However, we will emit font files ourselves so we store the original value
|
||||
* and replace it with a url we control. For example with the value "https://foo.bar/file.woff2":
|
||||
* - font type is woff2
|
||||
* - hash will be "<hash>.woff2"
|
||||
* - `collect` will save the association of the original url and the new hash for later use
|
||||
* - the returned url will be `/_astro/fonts/<hash>.woff2`
|
||||
*/
|
||||
export function createURLProxy({
|
||||
hashString,
|
||||
collect,
|
||||
}: {
|
||||
hashString: (value: string) => string;
|
||||
collect: (data: {
|
||||
hash: string;
|
||||
type: FontType;
|
||||
value: string;
|
||||
}) => string;
|
||||
}) {
|
||||
return function proxyURL(value: string): string {
|
||||
const type = extractFontType(value);
|
||||
const hash = `${hashString(value)}.${type}`;
|
||||
const url = collect({ hash, type, value });
|
||||
// Now that we collected the original url, we return our proxy so the consumer can override it
|
||||
return url;
|
||||
};
|
||||
}
|
||||
|
||||
export type URLProxy = ReturnType<typeof createURLProxy>;
|
||||
|
|
|
@ -4,10 +4,10 @@ import { resolveProviders, type ResolveMod } from './providers/utils.js';
|
|||
import * as unifont from 'unifont';
|
||||
import type { FontFamily, FontProvider, FontType } from './types.js';
|
||||
import xxhash from 'xxhash-wasm';
|
||||
import { extname, isAbsolute } from 'node:path';
|
||||
import { isAbsolute } from 'node:path';
|
||||
import { getClientOutputDirectory } from '../../prerender/utils.js';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { extractFontType, generateFontFace, createCache, type CacheHandler } from './utils.js';
|
||||
import { generateFontFace, createCache, type CacheHandler, createURLProxy } from './utils.js';
|
||||
import {
|
||||
DEFAULTS,
|
||||
VIRTUAL_MODULE_ID,
|
||||
|
@ -110,7 +110,7 @@ async function fetchFont(url: string): Promise<Buffer> {
|
|||
}
|
||||
}
|
||||
|
||||
export function fonts({ settings, sync, logger }: Options): Plugin {
|
||||
export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
|
||||
if (!settings.config.experimental.fonts) {
|
||||
// this is required because the virtual module does not exist
|
||||
// when fonts are not enabled, and that prevents rollup from building
|
||||
|
@ -172,58 +172,65 @@ export function fonts({ settings, sync, logger }: Options): Plugin {
|
|||
resolved.map((e) => e.provider(e.config)),
|
||||
{ storage },
|
||||
);
|
||||
const { resolveFont: resolveLocalFont } = createLocalProvider({ root: settings.config.root });
|
||||
|
||||
// We initialize shared variables here and reset them in buildEnd
|
||||
// to avoid locking memory
|
||||
resolvedMap = new Map();
|
||||
hashToUrlMap = new Map();
|
||||
const preloadData: PreloadData = [];
|
||||
let css = '';
|
||||
|
||||
const { resolveFont: resolveLocalFont } = createLocalProvider({ root: settings.config.root });
|
||||
// TODO: investigate using fontaine for fallbacks
|
||||
for (const family of families) {
|
||||
const preloadData: PreloadData = [];
|
||||
let css = '';
|
||||
|
||||
function proxySourceURL(value: string) {
|
||||
const hash = h64ToString(value) + extname(value);
|
||||
const proxyURL = createURLProxy({
|
||||
hashString: h64ToString,
|
||||
collect: ({ hash, type, value }) => {
|
||||
const url = baseUrl + hash;
|
||||
if (!hashToUrlMap!.has(hash)) {
|
||||
hashToUrlMap!.set(hash, value);
|
||||
preloadData.push({ url, type: extractFontType(hash) });
|
||||
preloadData.push({ url, type });
|
||||
}
|
||||
// Now that we collected the original url, we override it with our proxy
|
||||
return url;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: investigate using fontaine for fallbacks
|
||||
for (const family of families) {
|
||||
// Reset
|
||||
preloadData.length = 0;
|
||||
css = '';
|
||||
|
||||
if (family.provider === LOCAL_PROVIDER_NAME) {
|
||||
const { fonts: fontsData, fallbacks } = await resolveLocalFont(family, { proxySourceURL });
|
||||
for (const data of fontsData) {
|
||||
const { fonts, fallbacks } = await resolveLocalFont(family, { proxyURL });
|
||||
for (const data of fonts) {
|
||||
css += generateFontFace(family.name, data);
|
||||
}
|
||||
} else {
|
||||
const { fonts: fontsData, fallbacks } = await resolveFont(
|
||||
const { fonts, fallbacks } = await resolveFont(
|
||||
family.name,
|
||||
// We do not merge the defaults, we only provide defaults as a fallback
|
||||
{
|
||||
weights: family.weights ?? DEFAULTS.weights,
|
||||
styles: family.styles ?? DEFAULTS.styles,
|
||||
subsets: family.subsets ?? DEFAULTS.subsets,
|
||||
fallbacks: family.fallbacks ?? DEFAULTS.fallbacks,
|
||||
},
|
||||
// By default, fontaine goes through all providers. We use a different approach
|
||||
// where we specify a provider per font (default to google)
|
||||
[family.provider],
|
||||
);
|
||||
|
||||
for (const data of fontsData) {
|
||||
for (const data of fonts) {
|
||||
for (const source of data.src) {
|
||||
if ('name' in source) {
|
||||
continue;
|
||||
}
|
||||
source.url = proxySourceURL(source.url);
|
||||
source.url = proxyURL(source.url);
|
||||
}
|
||||
// TODO: support optional as prop
|
||||
css += generateFontFace(family.name, data);
|
||||
}
|
||||
}
|
||||
resolvedMap.set(family.name, { preloadData, css });
|
||||
resolvedMap.set(family.name, { preloadData: [...preloadData], css });
|
||||
}
|
||||
logger.info('assets', 'Fonts initialized');
|
||||
}
|
||||
|
@ -299,6 +306,8 @@ export function fonts({ settings, sync, logger }: Options): Plugin {
|
|||
return;
|
||||
}
|
||||
|
||||
// TODO: properly cleanup in case of failure
|
||||
|
||||
const logManager = createLogManager(logger);
|
||||
const dir = getClientOutputDirectory(settings);
|
||||
const fontsDir = new URL('.' + baseUrl, dir);
|
||||
|
|
|
@ -19,7 +19,7 @@ import { emitESMImage } from './utils/node/emitAsset.js';
|
|||
import { getProxyCode } from './utils/proxy.js';
|
||||
import { makeSvgComponent } from './utils/svg.js';
|
||||
import { hashTransform, propsToFilename } from './utils/transformToPath.js';
|
||||
import { fonts } from './fonts/vite-plugin-fonts.js';
|
||||
import { fontsPlugin } from './fonts/vite-plugin-fonts.js';
|
||||
import type { Logger } from '../core/logger/core.js';
|
||||
|
||||
const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
|
||||
|
@ -252,6 +252,6 @@ export default function assets({ settings, sync, logger }: Options): vite.Plugin
|
|||
}
|
||||
},
|
||||
},
|
||||
fonts({ settings, sync, logger }),
|
||||
fontsPlugin({ settings, sync, logger }),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
// @ts-check
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { isFontType, extractFontType, createCache } from '../../../../dist/assets/fonts/utils.js';
|
||||
import {
|
||||
isFontType,
|
||||
extractFontType,
|
||||
createCache,
|
||||
createURLProxy,
|
||||
} from '../../../../dist/assets/fonts/utils.js';
|
||||
|
||||
function createSpyCache() {
|
||||
/** @type {Map<string, Buffer>} */
|
||||
|
@ -32,6 +37,29 @@ function createSpyCache() {
|
|||
return { cache, getKeys: () => Array.from(store.keys()) };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {string} value
|
||||
*/
|
||||
function proxyURLSpy(id, value) {
|
||||
/** @type {Parameters<Parameters<typeof createURLProxy>[0]['collect']>[0]} */
|
||||
let collected = /** @type {any} */ (undefined);
|
||||
const proxyURL = createURLProxy({
|
||||
hashString: () => id,
|
||||
collect: (data) => {
|
||||
collected = data;
|
||||
return 'base/' + data.hash;
|
||||
},
|
||||
});
|
||||
const url = proxyURL(value);
|
||||
|
||||
return {
|
||||
url,
|
||||
collected,
|
||||
};
|
||||
}
|
||||
|
||||
describe('fonts utils', () => {
|
||||
it('isFontType()', () => {
|
||||
assert.equal(isFontType('woff2'), true);
|
||||
|
@ -93,4 +121,26 @@ describe('fonts utils', () => {
|
|||
|
||||
assert.deepStrictEqual(getKeys(), ['foo']);
|
||||
});
|
||||
|
||||
it('createURLProxy()', () => {
|
||||
let { url, collected } = proxyURLSpy(
|
||||
'foo',
|
||||
'https://fonts.gstatic.com/s/roboto/v47/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaSTbQWt4N.woff2',
|
||||
);
|
||||
assert.equal(url, 'base/foo.woff2');
|
||||
assert.deepStrictEqual(collected, {
|
||||
hash: 'foo.woff2',
|
||||
type: 'woff2',
|
||||
value:
|
||||
'https://fonts.gstatic.com/s/roboto/v47/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaSTbQWt4N.woff2',
|
||||
});
|
||||
|
||||
({ url, collected } = proxyURLSpy('bar', '/home/documents/project/font.ttf'));
|
||||
assert.equal(url, 'base/bar.ttf');
|
||||
assert.deepStrictEqual(collected, {
|
||||
hash: 'bar.ttf',
|
||||
type: 'ttf',
|
||||
value: '/home/documents/project/font.ttf',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue