@pyreon/zero 0.24.5 → 0.24.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -39
- package/src/actions.ts +0 -196
- package/src/adapters/bun.ts +0 -114
- package/src/adapters/cloudflare.ts +0 -166
- package/src/adapters/index.ts +0 -61
- package/src/adapters/netlify.ts +0 -154
- package/src/adapters/node.ts +0 -163
- package/src/adapters/static.ts +0 -42
- package/src/adapters/validate.ts +0 -23
- package/src/adapters/vercel.ts +0 -182
- package/src/adapters/warn-missing-env.ts +0 -49
- package/src/ai.ts +0 -623
- package/src/api-routes.ts +0 -219
- package/src/app.ts +0 -92
- package/src/cache.ts +0 -136
- package/src/client.ts +0 -143
- package/src/compression.ts +0 -116
- package/src/config.ts +0 -35
- package/src/cors.ts +0 -94
- package/src/csp.ts +0 -226
- package/src/entry-server.ts +0 -224
- package/src/env.ts +0 -344
- package/src/error-overlay.ts +0 -118
- package/src/favicon.ts +0 -841
- package/src/font.ts +0 -511
- package/src/fs-router.ts +0 -1519
- package/src/i18n-routing.ts +0 -533
- package/src/icon.tsx +0 -182
- package/src/icons-plugin.ts +0 -296
- package/src/image-plugin.ts +0 -751
- package/src/image-types.ts +0 -60
- package/src/image.tsx +0 -340
- package/src/index.ts +0 -92
- package/src/isr.ts +0 -394
- package/src/link.tsx +0 -304
- package/src/logger.ts +0 -144
- package/src/manifest.ts +0 -787
- package/src/meta.tsx +0 -354
- package/src/middleware.ts +0 -65
- package/src/not-found.ts +0 -44
- package/src/og-image.ts +0 -378
- package/src/rate-limit.ts +0 -140
- package/src/script.tsx +0 -260
- package/src/seo.ts +0 -617
- package/src/server.ts +0 -89
- package/src/sharp.d.ts +0 -22
- package/src/ssg-plugin.ts +0 -1582
- package/src/testing.ts +0 -146
- package/src/theme.tsx +0 -257
- package/src/types.ts +0 -624
- package/src/utils/use-intersection-observer.ts +0 -36
- package/src/utils/with-headers.ts +0 -13
- package/src/vercel-revalidate-handler.ts +0 -204
- package/src/vite-plugin.ts +0 -848
package/src/font.ts
DELETED
|
@@ -1,511 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import type { Plugin } from 'vite'
|
|
4
|
-
|
|
5
|
-
// ─── Font optimization ──────────────────────────────────────────────────────
|
|
6
|
-
//
|
|
7
|
-
// Zero provides automatic font optimization:
|
|
8
|
-
// - Downloads and self-hosts Google Fonts at build time (privacy + performance)
|
|
9
|
-
// - Falls back to CDN link in dev mode (for fast dev startup)
|
|
10
|
-
// - Injects preconnect/preload hints into the HTML
|
|
11
|
-
// - Sets font-display: swap to prevent FOIT (Flash of Invisible Text)
|
|
12
|
-
// - Generates optimized @font-face declarations
|
|
13
|
-
// - Size-adjusted fallback fonts to reduce CLS
|
|
14
|
-
|
|
15
|
-
export interface FontConfig {
|
|
16
|
-
/**
|
|
17
|
-
* Google Fonts families.
|
|
18
|
-
*
|
|
19
|
-
* Accepts both string shorthand and structured objects:
|
|
20
|
-
* - String: "Inter:wght@400;500;700" or "Inter:wght@100..900"
|
|
21
|
-
* - Object: { family: "Inter", weights: [400, 500, 700] }
|
|
22
|
-
* - Variable: { family: "Inter", variable: true, weightRange: [100, 900] }
|
|
23
|
-
*/
|
|
24
|
-
google?: GoogleFontInput[]
|
|
25
|
-
/** Local font files. */
|
|
26
|
-
local?: LocalFont[]
|
|
27
|
-
/** Default font-display strategy. Default: "swap" */
|
|
28
|
-
display?: FontDisplay
|
|
29
|
-
/** Preload critical fonts. Default: true */
|
|
30
|
-
preload?: boolean
|
|
31
|
-
/** Self-host Google Fonts at build time. Default: true */
|
|
32
|
-
selfHost?: boolean
|
|
33
|
-
/** Fallback font metrics for reducing CLS. */
|
|
34
|
-
fallbacks?: Record<string, FallbackMetrics>
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Static Google Font config. */
|
|
38
|
-
export interface GoogleFontStatic {
|
|
39
|
-
family: string
|
|
40
|
-
weights: number[]
|
|
41
|
-
italic?: boolean
|
|
42
|
-
variable?: false
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Variable Google Font config. */
|
|
46
|
-
export interface GoogleFontVariable {
|
|
47
|
-
family: string
|
|
48
|
-
/** Weight range as [min, max] tuple. e.g. [100, 900] */
|
|
49
|
-
weightRange: [number, number]
|
|
50
|
-
italic?: boolean
|
|
51
|
-
variable: true
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Google font input: structured object or string shorthand. */
|
|
55
|
-
export type GoogleFontInput = GoogleFontStatic | GoogleFontVariable | string
|
|
56
|
-
|
|
57
|
-
export interface LocalFont {
|
|
58
|
-
family: string
|
|
59
|
-
src: string
|
|
60
|
-
/** Single weight (400) or variable range ("100 900"). */
|
|
61
|
-
weight?: number | `${number} ${number}`
|
|
62
|
-
style?: 'normal' | 'italic'
|
|
63
|
-
display?: FontDisplay
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export type FontDisplay = 'auto' | 'block' | 'swap' | 'fallback' | 'optional'
|
|
67
|
-
|
|
68
|
-
/** Metrics for generating size-adjusted fallback fonts to reduce CLS. */
|
|
69
|
-
export interface FallbackMetrics {
|
|
70
|
-
/** The fallback font to adjust. e.g. "Arial", "Georgia" */
|
|
71
|
-
fallback: string
|
|
72
|
-
/** Size adjustment factor. e.g. 1.05 */
|
|
73
|
-
sizeAdjust?: number
|
|
74
|
-
/** Ascent override percentage. e.g. 90 */
|
|
75
|
-
ascentOverride?: number
|
|
76
|
-
/** Descent override percentage. e.g. 22 */
|
|
77
|
-
descentOverride?: number
|
|
78
|
-
/** Line gap override percentage. e.g. 0 */
|
|
79
|
-
lineGapOverride?: number
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
interface ResolvedFontBase {
|
|
83
|
-
family: string
|
|
84
|
-
italic: boolean
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface StaticFont extends ResolvedFontBase {
|
|
88
|
-
variable: false
|
|
89
|
-
weights: number[]
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
interface VariableFont extends ResolvedFontBase {
|
|
93
|
-
variable: true
|
|
94
|
-
weightRange: [number, number]
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
type ResolvedFont = StaticFont | VariableFont
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Normalize a GoogleFontInput (string or object) into a ResolvedFont.
|
|
101
|
-
*/
|
|
102
|
-
export function resolveGoogleFont(input: GoogleFontInput): ResolvedFont {
|
|
103
|
-
if (typeof input === 'string') {
|
|
104
|
-
return parseGoogleFamily(input)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (input.variable) {
|
|
108
|
-
return {
|
|
109
|
-
family: input.family,
|
|
110
|
-
italic: input.italic ?? false,
|
|
111
|
-
variable: true,
|
|
112
|
-
weightRange: input.weightRange,
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
family: input.family,
|
|
118
|
-
italic: input.italic ?? false,
|
|
119
|
-
variable: false,
|
|
120
|
-
weights: input.weights,
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Parse Google Fonts family string shorthand.
|
|
126
|
-
*
|
|
127
|
-
* Static weights: "Inter:wght@400;500;700"
|
|
128
|
-
* Variable range: "Inter:wght@100..900"
|
|
129
|
-
* Variable with italic: "Inter:ital,wght@100..900"
|
|
130
|
-
*/
|
|
131
|
-
export function parseGoogleFamily(input: string): ResolvedFont {
|
|
132
|
-
const parts = input.split(':')
|
|
133
|
-
const family = (parts[0] ?? '').trim()
|
|
134
|
-
const spec = parts[1]
|
|
135
|
-
let italic = false
|
|
136
|
-
|
|
137
|
-
if (spec) {
|
|
138
|
-
italic = spec.includes('ital')
|
|
139
|
-
|
|
140
|
-
// Variable font range syntax: wght@100..900
|
|
141
|
-
const rangeMatch = spec.match(/wght@(\d+)\.\.(\d+)/)
|
|
142
|
-
if (rangeMatch && rangeMatch[1] && rangeMatch[2]) {
|
|
143
|
-
return {
|
|
144
|
-
family,
|
|
145
|
-
italic,
|
|
146
|
-
variable: true,
|
|
147
|
-
weightRange: [Number(rangeMatch[1]), Number(rangeMatch[2])],
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Static weights — two formats:
|
|
152
|
-
// Simple: "wght@400;500;700"
|
|
153
|
-
// Tuples: "ital,wght@0,300;0,500;1,300;1,500" (ital_flag,weight pairs)
|
|
154
|
-
const afterAt = spec.split('@')[1]
|
|
155
|
-
if (afterAt) {
|
|
156
|
-
const entries = afterAt.split(';').filter(Boolean)
|
|
157
|
-
const weights = new Set<number>()
|
|
158
|
-
|
|
159
|
-
for (const entry of entries) {
|
|
160
|
-
if (entry.includes(',')) {
|
|
161
|
-
// Tuple format: "0,300" or "1,500" — last value is the weight
|
|
162
|
-
const tuple = entry.split(',')
|
|
163
|
-
const weight = Number(tuple[tuple.length - 1])
|
|
164
|
-
if (weight > 0) weights.add(weight)
|
|
165
|
-
// Detect italic from tuple: "1,xxx" means italic
|
|
166
|
-
if (tuple[0] === '1') italic = true
|
|
167
|
-
} else if (entry.includes('..')) {
|
|
168
|
-
// Variable range already handled above — skip
|
|
169
|
-
} else {
|
|
170
|
-
// Simple weight: "400"
|
|
171
|
-
const weight = Number(entry)
|
|
172
|
-
if (weight > 0) weights.add(weight)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (weights.size > 0) {
|
|
177
|
-
return {
|
|
178
|
-
family,
|
|
179
|
-
italic,
|
|
180
|
-
variable: false,
|
|
181
|
-
weights: [...weights].sort((a, b) => a - b),
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return { family, italic, variable: false, weights: [400] }
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Generate a Google Fonts CSS URL.
|
|
192
|
-
*/
|
|
193
|
-
export function googleFontsUrl(families: ResolvedFont[], display: FontDisplay = 'swap'): string {
|
|
194
|
-
const params = families
|
|
195
|
-
.map((f) => {
|
|
196
|
-
const axes = f.italic ? 'ital,wght' : 'wght'
|
|
197
|
-
const name = f.family.replace(/ /g, '+')
|
|
198
|
-
|
|
199
|
-
if (f.variable) {
|
|
200
|
-
const range = `${f.weightRange[0]}..${f.weightRange[1]}`
|
|
201
|
-
const value = f.italic ? `0,${range};1,${range}` : range
|
|
202
|
-
return `family=${name}:${axes}@${value}`
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const values = f.weights.map((w) => (f.italic ? `0,${w};1,${w}` : String(w))).join(';')
|
|
206
|
-
return `family=${name}:${axes}@${values}`
|
|
207
|
-
})
|
|
208
|
-
.join('&')
|
|
209
|
-
|
|
210
|
-
return `https://fonts.googleapis.com/css2?${params}&display=${display}`
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Generate @font-face CSS for local fonts.
|
|
215
|
-
*/
|
|
216
|
-
function localFontFaces(fonts: LocalFont[], display: FontDisplay): string {
|
|
217
|
-
return fonts
|
|
218
|
-
.map(
|
|
219
|
-
(f) => `@font-face {
|
|
220
|
-
font-family: "${f.family}";
|
|
221
|
-
src: url("${f.src}");
|
|
222
|
-
font-weight: ${f.weight ?? '400'};
|
|
223
|
-
font-style: ${f.style ?? 'normal'};
|
|
224
|
-
font-display: ${f.display ?? display};
|
|
225
|
-
}`,
|
|
226
|
-
)
|
|
227
|
-
.join('\n\n')
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Generate size-adjusted fallback @font-face declarations to reduce CLS.
|
|
232
|
-
*/
|
|
233
|
-
function fallbackFontFaces(fallbacks: Record<string, FallbackMetrics>): string {
|
|
234
|
-
return Object.entries(fallbacks)
|
|
235
|
-
.map(([family, metrics]) => {
|
|
236
|
-
const overrides: string[] = []
|
|
237
|
-
if (metrics.sizeAdjust != null) overrides.push(` size-adjust: ${metrics.sizeAdjust * 100}%;`)
|
|
238
|
-
if (metrics.ascentOverride != null)
|
|
239
|
-
overrides.push(` ascent-override: ${metrics.ascentOverride}%;`)
|
|
240
|
-
if (metrics.descentOverride != null)
|
|
241
|
-
overrides.push(` descent-override: ${metrics.descentOverride}%;`)
|
|
242
|
-
if (metrics.lineGapOverride != null)
|
|
243
|
-
overrides.push(` line-gap-override: ${metrics.lineGapOverride}%;`)
|
|
244
|
-
|
|
245
|
-
return `@font-face {
|
|
246
|
-
font-family: "${family} Fallback";
|
|
247
|
-
src: local("${metrics.fallback}");
|
|
248
|
-
${overrides.join('\n')}
|
|
249
|
-
}`
|
|
250
|
-
})
|
|
251
|
-
.join('\n\n')
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Generate preload link tags for critical font files.
|
|
256
|
-
*/
|
|
257
|
-
function preloadTags(fonts: LocalFont[]): string {
|
|
258
|
-
return fonts
|
|
259
|
-
.map((f) => {
|
|
260
|
-
const ext = f.src.split('.').pop()
|
|
261
|
-
const type =
|
|
262
|
-
ext === 'woff2'
|
|
263
|
-
? 'font/woff2'
|
|
264
|
-
: ext === 'woff'
|
|
265
|
-
? 'font/woff'
|
|
266
|
-
: ext === 'ttf'
|
|
267
|
-
? 'font/ttf'
|
|
268
|
-
: 'font/otf'
|
|
269
|
-
return `<link rel="preload" href="${f.src}" as="font" type="${type}" crossorigin>`
|
|
270
|
-
})
|
|
271
|
-
.join('\n')
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Download Google Fonts CSS with woff2 user agent.
|
|
276
|
-
*/
|
|
277
|
-
async function downloadGoogleFontsCSS(url: string): Promise<string> {
|
|
278
|
-
const response = await fetch(url, {
|
|
279
|
-
headers: {
|
|
280
|
-
'User-Agent':
|
|
281
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
282
|
-
},
|
|
283
|
-
})
|
|
284
|
-
if (!response.ok) {
|
|
285
|
-
throw new Error(`[Pyreon] Failed to fetch Google Fonts CSS: ${response.status}`)
|
|
286
|
-
}
|
|
287
|
-
return response.text()
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Download a font file.
|
|
292
|
-
*/
|
|
293
|
-
async function downloadFontFile(url: string): Promise<Buffer> {
|
|
294
|
-
const response = await fetch(url)
|
|
295
|
-
if (!response.ok) throw new Error(`[Pyreon] Failed to download font: ${url}`)
|
|
296
|
-
const arrayBuffer = await response.arrayBuffer()
|
|
297
|
-
return Buffer.from(arrayBuffer)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Extract font file URLs from Google Fonts CSS.
|
|
302
|
-
*/
|
|
303
|
-
function extractFontUrls(css: string): string[] {
|
|
304
|
-
const urls: string[] = []
|
|
305
|
-
const regex = /url\((https:\/\/fonts\.gstatic\.com\/[^)]+)\)/g
|
|
306
|
-
for (const match of css.matchAll(regex)) {
|
|
307
|
-
if (match[1]) urls.push(match[1])
|
|
308
|
-
}
|
|
309
|
-
return urls
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Self-host Google Fonts: download CSS + font files, rewrite URLs to local paths.
|
|
314
|
-
*/
|
|
315
|
-
async function selfHostFonts(
|
|
316
|
-
cssUrl: string,
|
|
317
|
-
fontsSubDir: string,
|
|
318
|
-
root: string,
|
|
319
|
-
): Promise<{
|
|
320
|
-
css: string
|
|
321
|
-
fontFiles: Array<{ name: string; content: Buffer }>
|
|
322
|
-
}> {
|
|
323
|
-
// Cache fonts between builds to avoid re-downloading (~6s penalty)
|
|
324
|
-
const cacheDir = join(root, 'node_modules', '.cache', 'zero-fonts')
|
|
325
|
-
const cacheKey = Buffer.from(cssUrl).toString('base64url')
|
|
326
|
-
const cachePath = join(cacheDir, `${cacheKey}.json`)
|
|
327
|
-
|
|
328
|
-
try {
|
|
329
|
-
const cached = JSON.parse(await readFile(cachePath, 'utf-8'))
|
|
330
|
-
if (cached.css && cached.fontFiles) {
|
|
331
|
-
return {
|
|
332
|
-
css: cached.css,
|
|
333
|
-
fontFiles: cached.fontFiles.map((f: any) => ({
|
|
334
|
-
name: f.name,
|
|
335
|
-
content: Buffer.from(f.content, 'base64'),
|
|
336
|
-
})),
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
} catch {
|
|
340
|
-
// No cache — download fresh
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const css = await downloadGoogleFontsCSS(cssUrl)
|
|
344
|
-
const fontUrls = extractFontUrls(css)
|
|
345
|
-
const fontFiles: Array<{ name: string; content: Buffer }> = []
|
|
346
|
-
|
|
347
|
-
let rewrittenCss = css
|
|
348
|
-
|
|
349
|
-
for (const url of fontUrls) {
|
|
350
|
-
const urlParts = url.split('/')
|
|
351
|
-
const fileName = urlParts.at(-1)?.split('?')[0] ?? 'font'
|
|
352
|
-
const content = await downloadFontFile(url)
|
|
353
|
-
|
|
354
|
-
fontFiles.push({ name: fileName, content })
|
|
355
|
-
rewrittenCss = rewrittenCss.replace(url, `/${fontsSubDir}/${fileName}`)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Write cache
|
|
359
|
-
try {
|
|
360
|
-
await mkdir(cacheDir, { recursive: true })
|
|
361
|
-
await writeFile(cachePath, JSON.stringify({
|
|
362
|
-
css: rewrittenCss,
|
|
363
|
-
fontFiles: fontFiles.map((f) => ({ name: f.name, content: f.content.toString('base64') })),
|
|
364
|
-
}))
|
|
365
|
-
} catch {
|
|
366
|
-
// Cache write failure is non-fatal
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return { css: rewrittenCss, fontFiles }
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Zero font optimization Vite plugin.
|
|
374
|
-
*
|
|
375
|
-
* Dev mode: injects Google Fonts CDN link for fast startup.
|
|
376
|
-
* Build mode: downloads and self-hosts fonts for maximum performance + privacy.
|
|
377
|
-
*
|
|
378
|
-
* @example
|
|
379
|
-
* import { fontPlugin } from "@pyreon/zero/font"
|
|
380
|
-
*
|
|
381
|
-
* export default {
|
|
382
|
-
* plugins: [
|
|
383
|
-
* pyreon(),
|
|
384
|
-
* zero(),
|
|
385
|
-
* fontPlugin({
|
|
386
|
-
* google: ["Inter:wght@400;500;600;700", "JetBrains Mono:wght@400"],
|
|
387
|
-
* fallbacks: {
|
|
388
|
-
* "Inter": { fallback: "Arial", sizeAdjust: 1.07, ascentOverride: 90 },
|
|
389
|
-
* },
|
|
390
|
-
* }),
|
|
391
|
-
* ],
|
|
392
|
-
* }
|
|
393
|
-
*/
|
|
394
|
-
export function fontPlugin(config: FontConfig = {}): Plugin {
|
|
395
|
-
const display = config.display ?? 'swap'
|
|
396
|
-
const shouldPreload = config.preload !== false
|
|
397
|
-
const shouldSelfHost = config.selfHost !== false
|
|
398
|
-
const googleFamilies = (config.google ?? []).map(resolveGoogleFont)
|
|
399
|
-
|
|
400
|
-
let isBuild = false
|
|
401
|
-
let root = ''
|
|
402
|
-
let selfHostedCSS = ''
|
|
403
|
-
let selfHostedFontFiles: Array<{ name: string; content: Buffer }> = []
|
|
404
|
-
|
|
405
|
-
return {
|
|
406
|
-
name: 'pyreon-zero-fonts',
|
|
407
|
-
|
|
408
|
-
configResolved(resolvedConfig) {
|
|
409
|
-
isBuild = resolvedConfig.command === 'build'
|
|
410
|
-
root = resolvedConfig.root
|
|
411
|
-
},
|
|
412
|
-
|
|
413
|
-
async buildStart() {
|
|
414
|
-
if (isBuild && shouldSelfHost && googleFamilies.length > 0) {
|
|
415
|
-
const cssUrl = googleFontsUrl(googleFamilies, display)
|
|
416
|
-
try {
|
|
417
|
-
const result = await selfHostFonts(cssUrl, 'assets/fonts', root)
|
|
418
|
-
selfHostedCSS = result.css
|
|
419
|
-
selfHostedFontFiles = result.fontFiles
|
|
420
|
-
} catch {
|
|
421
|
-
// Self-hosting failed — fall back to CDN link
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
},
|
|
425
|
-
|
|
426
|
-
generateBundle() {
|
|
427
|
-
// Emit self-hosted font files as assets
|
|
428
|
-
for (const file of selfHostedFontFiles) {
|
|
429
|
-
this.emitFile({
|
|
430
|
-
type: 'asset',
|
|
431
|
-
fileName: `assets/fonts/${file.name}`,
|
|
432
|
-
source: file.content,
|
|
433
|
-
})
|
|
434
|
-
}
|
|
435
|
-
},
|
|
436
|
-
|
|
437
|
-
transformIndexHtml(html) {
|
|
438
|
-
const tags: string[] = []
|
|
439
|
-
|
|
440
|
-
collectGoogleFontTags(tags, {
|
|
441
|
-
isBuild,
|
|
442
|
-
selfHostedCSS,
|
|
443
|
-
selfHostedFontFiles,
|
|
444
|
-
shouldPreload,
|
|
445
|
-
googleFamilies,
|
|
446
|
-
display,
|
|
447
|
-
})
|
|
448
|
-
collectLocalFontTags(tags, config, shouldPreload, display)
|
|
449
|
-
|
|
450
|
-
if (tags.length === 0) return html
|
|
451
|
-
return html.replace('</head>', `${tags.join('\n')}\n</head>`)
|
|
452
|
-
},
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function collectGoogleFontTags(
|
|
457
|
-
tags: string[],
|
|
458
|
-
opts: {
|
|
459
|
-
isBuild: boolean
|
|
460
|
-
selfHostedCSS: string
|
|
461
|
-
selfHostedFontFiles: Array<{ name: string; content: Buffer }>
|
|
462
|
-
shouldPreload: boolean
|
|
463
|
-
googleFamilies: ResolvedFont[]
|
|
464
|
-
display: FontDisplay
|
|
465
|
-
},
|
|
466
|
-
) {
|
|
467
|
-
if (opts.isBuild && opts.selfHostedCSS) {
|
|
468
|
-
tags.push(`<style>${opts.selfHostedCSS}</style>`)
|
|
469
|
-
if (opts.shouldPreload) {
|
|
470
|
-
for (const file of opts.selfHostedFontFiles.slice(0, opts.googleFamilies.length)) {
|
|
471
|
-
const ext = file.name.split('.').pop()
|
|
472
|
-
const type = ext === 'woff2' ? 'font/woff2' : 'font/woff'
|
|
473
|
-
tags.push(
|
|
474
|
-
`<link rel="preload" href="/assets/fonts/${file.name}" as="font" type="${type}" crossorigin>`,
|
|
475
|
-
)
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
} else if (opts.googleFamilies.length > 0) {
|
|
479
|
-
const cssUrl = googleFontsUrl(opts.googleFamilies, opts.display)
|
|
480
|
-
tags.push(`<link rel="preconnect" href="https://fonts.googleapis.com">`)
|
|
481
|
-
tags.push(`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>`)
|
|
482
|
-
tags.push(`<link rel="stylesheet" href="${cssUrl}">`)
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function collectLocalFontTags(
|
|
487
|
-
tags: string[],
|
|
488
|
-
config: FontConfig,
|
|
489
|
-
shouldPreload: boolean,
|
|
490
|
-
display: FontDisplay,
|
|
491
|
-
) {
|
|
492
|
-
if (shouldPreload && config.local?.length) {
|
|
493
|
-
tags.push(preloadTags(config.local))
|
|
494
|
-
}
|
|
495
|
-
if (config.local?.length) {
|
|
496
|
-
tags.push(`<style>${localFontFaces(config.local, display)}</style>`)
|
|
497
|
-
}
|
|
498
|
-
if (config.fallbacks && Object.keys(config.fallbacks).length > 0) {
|
|
499
|
-
tags.push(`<style>${fallbackFontFaces(config.fallbacks)}</style>`)
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Generate CSS variables for font families.
|
|
505
|
-
*/
|
|
506
|
-
export function fontVariables(families: Record<string, string>): string {
|
|
507
|
-
const vars = Object.entries(families)
|
|
508
|
-
.map(([key, value]) => ` --font-${key}: ${value};`)
|
|
509
|
-
.join('\n')
|
|
510
|
-
return `:root {\n${vars}\n}`
|
|
511
|
-
}
|