@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.
Files changed (54) hide show
  1. package/package.json +10 -39
  2. package/src/actions.ts +0 -196
  3. package/src/adapters/bun.ts +0 -114
  4. package/src/adapters/cloudflare.ts +0 -166
  5. package/src/adapters/index.ts +0 -61
  6. package/src/adapters/netlify.ts +0 -154
  7. package/src/adapters/node.ts +0 -163
  8. package/src/adapters/static.ts +0 -42
  9. package/src/adapters/validate.ts +0 -23
  10. package/src/adapters/vercel.ts +0 -182
  11. package/src/adapters/warn-missing-env.ts +0 -49
  12. package/src/ai.ts +0 -623
  13. package/src/api-routes.ts +0 -219
  14. package/src/app.ts +0 -92
  15. package/src/cache.ts +0 -136
  16. package/src/client.ts +0 -143
  17. package/src/compression.ts +0 -116
  18. package/src/config.ts +0 -35
  19. package/src/cors.ts +0 -94
  20. package/src/csp.ts +0 -226
  21. package/src/entry-server.ts +0 -224
  22. package/src/env.ts +0 -344
  23. package/src/error-overlay.ts +0 -118
  24. package/src/favicon.ts +0 -841
  25. package/src/font.ts +0 -511
  26. package/src/fs-router.ts +0 -1519
  27. package/src/i18n-routing.ts +0 -533
  28. package/src/icon.tsx +0 -182
  29. package/src/icons-plugin.ts +0 -296
  30. package/src/image-plugin.ts +0 -751
  31. package/src/image-types.ts +0 -60
  32. package/src/image.tsx +0 -340
  33. package/src/index.ts +0 -92
  34. package/src/isr.ts +0 -394
  35. package/src/link.tsx +0 -304
  36. package/src/logger.ts +0 -144
  37. package/src/manifest.ts +0 -787
  38. package/src/meta.tsx +0 -354
  39. package/src/middleware.ts +0 -65
  40. package/src/not-found.ts +0 -44
  41. package/src/og-image.ts +0 -378
  42. package/src/rate-limit.ts +0 -140
  43. package/src/script.tsx +0 -260
  44. package/src/seo.ts +0 -617
  45. package/src/server.ts +0 -89
  46. package/src/sharp.d.ts +0 -22
  47. package/src/ssg-plugin.ts +0 -1582
  48. package/src/testing.ts +0 -146
  49. package/src/theme.tsx +0 -257
  50. package/src/types.ts +0 -624
  51. package/src/utils/use-intersection-observer.ts +0 -36
  52. package/src/utils/with-headers.ts +0 -13
  53. package/src/vercel-revalidate-handler.ts +0 -204
  54. 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
- }