@flexireact/core 2.2.0 → 2.4.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.
@@ -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
+ };