@flexireact/core 2.2.0 → 2.3.0
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/README.md +203 -15
- package/core/font/index.ts +306 -0
- package/core/image/index.ts +413 -0
- package/core/index.ts +44 -1
- package/core/metadata/index.ts +622 -0
- package/core/server/index.ts +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,8 +20,21 @@
|
|
|
20
20
|
<a href="#"><img src="https://img.shields.io/badge/Tailwind-v4-38B2AC.svg" alt="Tailwind CSS v4" /></a>
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
|
-
## 🆕 What's New in v2
|
|
23
|
+
## 🆕 What's New in v2.2
|
|
24
24
|
|
|
25
|
+
### v2.2.0 (Latest)
|
|
26
|
+
- **🌊 Streaming SSR** — Progressive HTML rendering with React 18 `renderToPipeableStream`
|
|
27
|
+
- **⚡ Server Actions** — Call server functions directly from client components
|
|
28
|
+
- **🔗 Link Prefetching** — Automatic prefetch on hover/viewport visibility
|
|
29
|
+
|
|
30
|
+
### v2.1.0
|
|
31
|
+
- **🛠️ Server Helpers** — `redirect()`, `notFound()`, `json()`, `cookies`, `headers`
|
|
32
|
+
- **🚧 Error/Loading Boundaries** — Per-segment `error.tsx` and `loading.tsx`
|
|
33
|
+
- **🔐 Route Middleware** — `_middleware.ts` for per-route logic
|
|
34
|
+
- **📊 Bundle Analyzer** — `flexi build --analyze`
|
|
35
|
+
- **🔄 CI/CD** — GitHub Actions workflow
|
|
36
|
+
|
|
37
|
+
### v2.0.0
|
|
25
38
|
- **TypeScript Native** — Core rewritten in TypeScript for better DX
|
|
26
39
|
- **Tailwind CSS v4** — New `@import "tailwindcss"` and `@theme` syntax
|
|
27
40
|
- **Routes Directory** — New `routes/` directory with route groups, dynamic segments
|
|
@@ -401,21 +414,117 @@ export function post(req, res) {
|
|
|
401
414
|
}
|
|
402
415
|
```
|
|
403
416
|
|
|
417
|
+
## ⚡ Server Actions (v2.2+)
|
|
418
|
+
|
|
419
|
+
Call server functions directly from client components:
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
// actions.ts
|
|
423
|
+
'use server';
|
|
424
|
+
import { serverAction, redirect, cookies } from '@flexireact/core';
|
|
425
|
+
|
|
426
|
+
export const createUser = serverAction(async (formData: FormData) => {
|
|
427
|
+
const name = formData.get('name') as string;
|
|
428
|
+
const user = await db.users.create({ name });
|
|
429
|
+
|
|
430
|
+
// Set a cookie
|
|
431
|
+
cookies.set('userId', user.id);
|
|
432
|
+
|
|
433
|
+
// Redirect after action
|
|
434
|
+
redirect('/users');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Form.tsx
|
|
438
|
+
'use client';
|
|
439
|
+
import { createUser } from './actions';
|
|
440
|
+
|
|
441
|
+
export function CreateUserForm() {
|
|
442
|
+
return (
|
|
443
|
+
<form action={createUser}>
|
|
444
|
+
<input name="name" placeholder="Name" required />
|
|
445
|
+
<button type="submit">Create User</button>
|
|
446
|
+
</form>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## 🔗 Link with Prefetching (v2.1+)
|
|
452
|
+
|
|
453
|
+
Enhanced Link component with automatic prefetching:
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
import { Link } from '@flexireact/core/client';
|
|
457
|
+
|
|
458
|
+
// Prefetch on hover (default)
|
|
459
|
+
<Link href="/about">About</Link>
|
|
460
|
+
|
|
461
|
+
// Prefetch when visible in viewport
|
|
462
|
+
<Link href="/products" prefetch="viewport">Products</Link>
|
|
463
|
+
|
|
464
|
+
// Replace history instead of push
|
|
465
|
+
<Link href="/login" replace>Login</Link>
|
|
466
|
+
|
|
467
|
+
// Programmatic navigation
|
|
468
|
+
import { useRouter } from '@flexireact/core/client';
|
|
469
|
+
|
|
470
|
+
function MyComponent() {
|
|
471
|
+
const router = useRouter();
|
|
472
|
+
|
|
473
|
+
return (
|
|
474
|
+
<button onClick={() => router.push('/dashboard')}>
|
|
475
|
+
Go to Dashboard
|
|
476
|
+
</button>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## 🛠️ Server Helpers (v2.1+)
|
|
482
|
+
|
|
483
|
+
Utility functions for server-side operations:
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
import { redirect, notFound, json, cookies, headers } from '@flexireact/core';
|
|
487
|
+
|
|
488
|
+
// Redirect
|
|
489
|
+
redirect('/dashboard');
|
|
490
|
+
redirect('/login', 'permanent'); // 308 redirect
|
|
491
|
+
|
|
492
|
+
// Not Found
|
|
493
|
+
notFound(); // Throws 404
|
|
494
|
+
|
|
495
|
+
// JSON Response (in API routes)
|
|
496
|
+
return json({ data: 'hello' }, { status: 200 });
|
|
497
|
+
|
|
498
|
+
// Cookies
|
|
499
|
+
const token = cookies.get(request, 'token');
|
|
500
|
+
const setCookie = cookies.set('session', 'abc123', {
|
|
501
|
+
httpOnly: true,
|
|
502
|
+
maxAge: 86400
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Headers
|
|
506
|
+
const auth = headers.bearerToken(request);
|
|
507
|
+
const corsHeaders = headers.cors({ origin: '*' });
|
|
508
|
+
const securityHeaders = headers.security();
|
|
509
|
+
```
|
|
510
|
+
|
|
404
511
|
## 🛡️ Middleware
|
|
405
512
|
|
|
406
|
-
|
|
513
|
+
### Global Middleware
|
|
514
|
+
|
|
515
|
+
Create `middleware.ts` in your project root:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
import { redirect, cookies } from '@flexireact/core';
|
|
407
519
|
|
|
408
|
-
```js
|
|
409
520
|
export default function middleware(request) {
|
|
410
521
|
// Protect routes
|
|
411
522
|
if (request.pathname.startsWith('/admin')) {
|
|
412
|
-
|
|
413
|
-
|
|
523
|
+
const token = cookies.get(request, 'token');
|
|
524
|
+
if (!token) {
|
|
525
|
+
redirect('/login');
|
|
414
526
|
}
|
|
415
527
|
}
|
|
416
|
-
|
|
417
|
-
// Continue
|
|
418
|
-
return MiddlewareResponse.next();
|
|
419
528
|
}
|
|
420
529
|
|
|
421
530
|
export const config = {
|
|
@@ -423,6 +532,32 @@ export const config = {
|
|
|
423
532
|
};
|
|
424
533
|
```
|
|
425
534
|
|
|
535
|
+
### Route Middleware (v2.1+)
|
|
536
|
+
|
|
537
|
+
Create `_middleware.ts` in any route directory:
|
|
538
|
+
|
|
539
|
+
```
|
|
540
|
+
routes/
|
|
541
|
+
admin/
|
|
542
|
+
_middleware.ts # Runs for all /admin/* routes
|
|
543
|
+
dashboard.tsx
|
|
544
|
+
settings.tsx
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
// routes/admin/_middleware.ts
|
|
549
|
+
export default async function middleware(req, res, { route, params }) {
|
|
550
|
+
const user = await getUser(req);
|
|
551
|
+
|
|
552
|
+
if (!user?.isAdmin) {
|
|
553
|
+
return { redirect: '/login' };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Continue to route
|
|
557
|
+
return { user };
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
426
561
|
## 🔧 Configuration
|
|
427
562
|
|
|
428
563
|
Create `flexireact.config.js`:
|
|
@@ -496,15 +631,68 @@ export default {
|
|
|
496
631
|
## 🖥️ CLI Commands
|
|
497
632
|
|
|
498
633
|
```bash
|
|
499
|
-
flexi create <name>
|
|
500
|
-
flexi dev
|
|
501
|
-
flexi build
|
|
502
|
-
flexi
|
|
503
|
-
flexi
|
|
504
|
-
flexi
|
|
505
|
-
flexi
|
|
634
|
+
flexi create <name> # Create new project
|
|
635
|
+
flexi dev # Start dev server
|
|
636
|
+
flexi build # Build for production
|
|
637
|
+
flexi build --analyze # Build with bundle analysis
|
|
638
|
+
flexi start # Start production server
|
|
639
|
+
flexi doctor # Check project health
|
|
640
|
+
flexi --version # Show version
|
|
641
|
+
flexi help # Show help
|
|
506
642
|
```
|
|
507
643
|
|
|
644
|
+
### Bundle Analysis (v2.1+)
|
|
645
|
+
|
|
646
|
+
```bash
|
|
647
|
+
flexi build --analyze
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Output:
|
|
651
|
+
```
|
|
652
|
+
📊 Bundle Analysis:
|
|
653
|
+
|
|
654
|
+
─────────────────────────────────────────────────
|
|
655
|
+
File Size
|
|
656
|
+
─────────────────────────────────────────────────
|
|
657
|
+
client/main.js 45.2 KB (13.56 KB gzip)
|
|
658
|
+
client/vendor.js 120.5 KB (38.2 KB gzip)
|
|
659
|
+
server/pages.js 12.3 KB
|
|
660
|
+
─────────────────────────────────────────────────
|
|
661
|
+
Total: 178 KB
|
|
662
|
+
Gzipped: 51.76 KB
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
## 🌊 Streaming SSR (v2.2+)
|
|
666
|
+
|
|
667
|
+
Progressive HTML rendering with React 18:
|
|
668
|
+
|
|
669
|
+
```tsx
|
|
670
|
+
import { renderPageStream, streamToResponse } from '@flexireact/core';
|
|
671
|
+
|
|
672
|
+
// In your server handler
|
|
673
|
+
const { stream, shellReady } = await renderPageStream({
|
|
674
|
+
Component: MyPage,
|
|
675
|
+
props: { data },
|
|
676
|
+
loading: LoadingSpinner,
|
|
677
|
+
error: ErrorBoundary,
|
|
678
|
+
title: 'My Page',
|
|
679
|
+
styles: ['/styles.css']
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// Wait for shell (initial HTML) to be ready
|
|
683
|
+
await shellReady;
|
|
684
|
+
|
|
685
|
+
// Stream to response
|
|
686
|
+
res.setHeader('Content-Type', 'text/html');
|
|
687
|
+
streamToResponse(res, stream);
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
Benefits:
|
|
691
|
+
- **Faster Time to First Byte (TTFB)** — Send HTML as it's ready
|
|
692
|
+
- **Progressive Loading** — Users see content immediately
|
|
693
|
+
- **Suspense Support** — Loading states stream in as data resolves
|
|
694
|
+
- **Better UX** — No blank screen while waiting for data
|
|
695
|
+
|
|
508
696
|
## 📚 Concepts Explained
|
|
509
697
|
|
|
510
698
|
### React Server Components (RSC)
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact Font Optimization
|
|
3
|
+
*
|
|
4
|
+
* Optimized font loading with:
|
|
5
|
+
* - Automatic font subsetting
|
|
6
|
+
* - Preload hints generation
|
|
7
|
+
* - Font-display: swap by default
|
|
8
|
+
* - Self-hosted Google Fonts
|
|
9
|
+
* - Variable font support
|
|
10
|
+
* - CSS variable generation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import crypto from 'crypto';
|
|
16
|
+
|
|
17
|
+
// Font configuration
|
|
18
|
+
export interface FontConfig {
|
|
19
|
+
family: string;
|
|
20
|
+
weight?: string | number | (string | number)[];
|
|
21
|
+
style?: 'normal' | 'italic' | 'oblique';
|
|
22
|
+
subsets?: string[];
|
|
23
|
+
display?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional';
|
|
24
|
+
preload?: boolean;
|
|
25
|
+
fallback?: string[];
|
|
26
|
+
variable?: string;
|
|
27
|
+
adjustFontFallback?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FontResult {
|
|
31
|
+
className: string;
|
|
32
|
+
style: {
|
|
33
|
+
fontFamily: string;
|
|
34
|
+
fontWeight?: number | string;
|
|
35
|
+
fontStyle?: string;
|
|
36
|
+
};
|
|
37
|
+
variable?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Google Fonts API
|
|
41
|
+
const GOOGLE_FONTS_API = 'https://fonts.googleapis.com/css2';
|
|
42
|
+
|
|
43
|
+
// Popular Google Fonts with their weights
|
|
44
|
+
export const googleFonts = {
|
|
45
|
+
Inter: {
|
|
46
|
+
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
47
|
+
variable: true,
|
|
48
|
+
subsets: ['latin', 'latin-ext', 'cyrillic', 'greek', 'vietnamese']
|
|
49
|
+
},
|
|
50
|
+
Roboto: {
|
|
51
|
+
weights: [100, 300, 400, 500, 700, 900],
|
|
52
|
+
variable: false,
|
|
53
|
+
subsets: ['latin', 'latin-ext', 'cyrillic', 'greek', 'vietnamese']
|
|
54
|
+
},
|
|
55
|
+
'Open Sans': {
|
|
56
|
+
weights: [300, 400, 500, 600, 700, 800],
|
|
57
|
+
variable: true,
|
|
58
|
+
subsets: ['latin', 'latin-ext', 'cyrillic', 'greek', 'vietnamese']
|
|
59
|
+
},
|
|
60
|
+
Poppins: {
|
|
61
|
+
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
62
|
+
variable: false,
|
|
63
|
+
subsets: ['latin', 'latin-ext']
|
|
64
|
+
},
|
|
65
|
+
Montserrat: {
|
|
66
|
+
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
67
|
+
variable: true,
|
|
68
|
+
subsets: ['latin', 'latin-ext', 'cyrillic', 'vietnamese']
|
|
69
|
+
},
|
|
70
|
+
'Fira Code': {
|
|
71
|
+
weights: [300, 400, 500, 600, 700],
|
|
72
|
+
variable: true,
|
|
73
|
+
subsets: ['latin', 'latin-ext', 'cyrillic', 'greek']
|
|
74
|
+
},
|
|
75
|
+
'JetBrains Mono': {
|
|
76
|
+
weights: [100, 200, 300, 400, 500, 600, 700, 800],
|
|
77
|
+
variable: true,
|
|
78
|
+
subsets: ['latin', 'latin-ext', 'cyrillic', 'greek']
|
|
79
|
+
},
|
|
80
|
+
Geist: {
|
|
81
|
+
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
82
|
+
variable: true,
|
|
83
|
+
subsets: ['latin', 'latin-ext']
|
|
84
|
+
},
|
|
85
|
+
'Geist Mono': {
|
|
86
|
+
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
|
87
|
+
variable: true,
|
|
88
|
+
subsets: ['latin', 'latin-ext']
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Generate font CSS
|
|
93
|
+
export function generateFontCSS(config: FontConfig): string {
|
|
94
|
+
const {
|
|
95
|
+
family,
|
|
96
|
+
weight = 400,
|
|
97
|
+
style = 'normal',
|
|
98
|
+
display = 'swap',
|
|
99
|
+
fallback = ['system-ui', 'sans-serif'],
|
|
100
|
+
variable
|
|
101
|
+
} = config;
|
|
102
|
+
|
|
103
|
+
const weights = Array.isArray(weight) ? weight : [weight];
|
|
104
|
+
const fallbackStr = fallback.map(f => f.includes(' ') ? `"${f}"` : f).join(', ');
|
|
105
|
+
|
|
106
|
+
let css = '';
|
|
107
|
+
|
|
108
|
+
// Generate @font-face for each weight
|
|
109
|
+
for (const w of weights) {
|
|
110
|
+
css += `
|
|
111
|
+
@font-face {
|
|
112
|
+
font-family: '${family}';
|
|
113
|
+
font-style: ${style};
|
|
114
|
+
font-weight: ${w};
|
|
115
|
+
font-display: ${display};
|
|
116
|
+
src: local('${family}'),
|
|
117
|
+
url('/_flexi/font/${encodeURIComponent(family)}?weight=${w}&style=${style}') format('woff2');
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Generate CSS variable if specified
|
|
123
|
+
if (variable) {
|
|
124
|
+
css += `
|
|
125
|
+
:root {
|
|
126
|
+
${variable}: '${family}', ${fallbackStr};
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return css;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Generate preload link tags
|
|
135
|
+
export function generateFontPreloadTags(fonts: FontConfig[]): string {
|
|
136
|
+
return fonts
|
|
137
|
+
.filter(f => f.preload !== false)
|
|
138
|
+
.map(f => {
|
|
139
|
+
const weights = Array.isArray(f.weight) ? f.weight : [f.weight || 400];
|
|
140
|
+
return weights.map(w =>
|
|
141
|
+
`<link rel="preload" href="/_flexi/font/${encodeURIComponent(f.family)}?weight=${w}&style=${f.style || 'normal'}" as="font" type="font/woff2" crossorigin>`
|
|
142
|
+
).join('\n');
|
|
143
|
+
})
|
|
144
|
+
.join('\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Create a font loader function (like next/font)
|
|
148
|
+
export function createFont(config: FontConfig): FontResult {
|
|
149
|
+
const {
|
|
150
|
+
family,
|
|
151
|
+
weight = 400,
|
|
152
|
+
style = 'normal',
|
|
153
|
+
fallback = ['system-ui', 'sans-serif'],
|
|
154
|
+
variable
|
|
155
|
+
} = config;
|
|
156
|
+
|
|
157
|
+
// Generate unique class name
|
|
158
|
+
const hash = crypto
|
|
159
|
+
.createHash('md5')
|
|
160
|
+
.update(`${family}-${weight}-${style}`)
|
|
161
|
+
.digest('hex')
|
|
162
|
+
.slice(0, 8);
|
|
163
|
+
|
|
164
|
+
const className = `__font_${hash}`;
|
|
165
|
+
const fallbackStr = fallback.map(f => f.includes(' ') ? `"${f}"` : f).join(', ');
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
className,
|
|
169
|
+
style: {
|
|
170
|
+
fontFamily: `'${family}', ${fallbackStr}`,
|
|
171
|
+
fontWeight: Array.isArray(weight) ? undefined : weight,
|
|
172
|
+
fontStyle: style
|
|
173
|
+
},
|
|
174
|
+
variable: variable || undefined
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Google Font loader
|
|
179
|
+
export function googleFont(config: FontConfig): FontResult {
|
|
180
|
+
return createFont({
|
|
181
|
+
...config,
|
|
182
|
+
// Google Fonts are always preloaded
|
|
183
|
+
preload: true
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Local font loader
|
|
188
|
+
export function localFont(config: FontConfig & { src: string | { path: string; weight?: string | number; style?: string }[] }): FontResult {
|
|
189
|
+
return createFont(config);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Handle font requests
|
|
193
|
+
export async function handleFontRequest(
|
|
194
|
+
req: any,
|
|
195
|
+
res: any
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
198
|
+
const pathParts = url.pathname.split('/');
|
|
199
|
+
const fontFamily = decodeURIComponent(pathParts[pathParts.length - 1] || '');
|
|
200
|
+
const weight = url.searchParams.get('weight') || '400';
|
|
201
|
+
const style = url.searchParams.get('style') || 'normal';
|
|
202
|
+
|
|
203
|
+
if (!fontFamily) {
|
|
204
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
205
|
+
res.end('Missing font family');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
// Check local cache first
|
|
211
|
+
const cacheDir = path.join(process.cwd(), '.flexi', 'font-cache');
|
|
212
|
+
const cacheKey = crypto
|
|
213
|
+
.createHash('md5')
|
|
214
|
+
.update(`${fontFamily}-${weight}-${style}`)
|
|
215
|
+
.digest('hex');
|
|
216
|
+
const cachePath = path.join(cacheDir, `${cacheKey}.woff2`);
|
|
217
|
+
|
|
218
|
+
if (fs.existsSync(cachePath)) {
|
|
219
|
+
const fontData = fs.readFileSync(cachePath);
|
|
220
|
+
res.writeHead(200, {
|
|
221
|
+
'Content-Type': 'font/woff2',
|
|
222
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
223
|
+
'X-Flexi-Font-Cache': 'HIT'
|
|
224
|
+
});
|
|
225
|
+
res.end(fontData);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Fetch from Google Fonts
|
|
230
|
+
const googleUrl = `${GOOGLE_FONTS_API}?family=${encodeURIComponent(fontFamily)}:wght@${weight}&display=swap`;
|
|
231
|
+
|
|
232
|
+
const cssResponse = await fetch(googleUrl, {
|
|
233
|
+
headers: {
|
|
234
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (!cssResponse.ok) {
|
|
239
|
+
throw new Error('Failed to fetch font CSS');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const css = await cssResponse.text();
|
|
243
|
+
|
|
244
|
+
// Extract woff2 URL from CSS
|
|
245
|
+
const woff2Match = css.match(/url\((https:\/\/fonts\.gstatic\.com[^)]+\.woff2)\)/);
|
|
246
|
+
|
|
247
|
+
if (!woff2Match) {
|
|
248
|
+
throw new Error('Could not find woff2 URL');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Fetch the actual font file
|
|
252
|
+
const fontResponse = await fetch(woff2Match[1]);
|
|
253
|
+
|
|
254
|
+
if (!fontResponse.ok) {
|
|
255
|
+
throw new Error('Failed to fetch font file');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const fontBuffer = Buffer.from(await fontResponse.arrayBuffer());
|
|
259
|
+
|
|
260
|
+
// Cache the font
|
|
261
|
+
if (!fs.existsSync(cacheDir)) {
|
|
262
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
263
|
+
}
|
|
264
|
+
fs.writeFileSync(cachePath, fontBuffer);
|
|
265
|
+
|
|
266
|
+
// Serve the font
|
|
267
|
+
res.writeHead(200, {
|
|
268
|
+
'Content-Type': 'font/woff2',
|
|
269
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
270
|
+
'X-Flexi-Font-Cache': 'MISS'
|
|
271
|
+
});
|
|
272
|
+
res.end(fontBuffer);
|
|
273
|
+
|
|
274
|
+
} catch (error: any) {
|
|
275
|
+
console.error('Font loading error:', error);
|
|
276
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
277
|
+
res.end('Font loading failed');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Pre-built font configurations
|
|
282
|
+
export const fonts = {
|
|
283
|
+
// Sans-serif
|
|
284
|
+
inter: (config: Partial<FontConfig> = {}) => googleFont({ family: 'Inter', variable: '--font-inter', ...config }),
|
|
285
|
+
roboto: (config: Partial<FontConfig> = {}) => googleFont({ family: 'Roboto', variable: '--font-roboto', ...config }),
|
|
286
|
+
openSans: (config: Partial<FontConfig> = {}) => googleFont({ family: 'Open Sans', variable: '--font-open-sans', ...config }),
|
|
287
|
+
poppins: (config: Partial<FontConfig> = {}) => googleFont({ family: 'Poppins', variable: '--font-poppins', ...config }),
|
|
288
|
+
montserrat: (config: Partial<FontConfig> = {}) => googleFont({ family: 'Montserrat', variable: '--font-montserrat', ...config }),
|
|
289
|
+
geist: (config: Partial<FontConfig> = {}) => googleFont({ family: 'Geist', variable: '--font-geist', ...config }),
|
|
290
|
+
|
|
291
|
+
// Monospace
|
|
292
|
+
firaCode: (config: Partial<FontConfig> = {}) => googleFont({ family: 'Fira Code', variable: '--font-fira-code', fallback: ['monospace'], ...config }),
|
|
293
|
+
jetbrainsMono: (config: Partial<FontConfig> = {}) => googleFont({ family: 'JetBrains Mono', variable: '--font-jetbrains', fallback: ['monospace'], ...config }),
|
|
294
|
+
geistMono: (config: Partial<FontConfig> = {}) => googleFont({ family: 'Geist Mono', variable: '--font-geist-mono', fallback: ['monospace'], ...config })
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export default {
|
|
298
|
+
createFont,
|
|
299
|
+
googleFont,
|
|
300
|
+
localFont,
|
|
301
|
+
generateFontCSS,
|
|
302
|
+
generateFontPreloadTags,
|
|
303
|
+
handleFontRequest,
|
|
304
|
+
fonts,
|
|
305
|
+
googleFonts
|
|
306
|
+
};
|