@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 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
- Create `middleware.js` in your project root:
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
- if (!request.cookie('token')) {
413
- return MiddlewareResponse.redirect('/login');
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> # Create new project
500
- flexi dev # Start dev server
501
- flexi build # Build for production
502
- flexi start # Start production server
503
- flexi doctor # Check project health
504
- flexi --version # Show version
505
- flexi help # Show help
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
+ };