@flexireact/core 2.1.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/actions/index.ts +364 -0
- package/core/font/index.ts +306 -0
- package/core/image/index.ts +413 -0
- package/core/index.ts +59 -2
- package/core/metadata/index.ts +622 -0
- package/core/render/index.ts +158 -0
- package/core/server/index.ts +61 -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,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact Server Actions
|
|
3
|
+
*
|
|
4
|
+
* Server Actions allow you to define server-side functions that can be called
|
|
5
|
+
* directly from client components. They are automatically serialized and executed
|
|
6
|
+
* on the server.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```tsx
|
|
10
|
+
* // In a server file (actions.ts)
|
|
11
|
+
* 'use server';
|
|
12
|
+
*
|
|
13
|
+
* export async function createUser(formData: FormData) {
|
|
14
|
+
* const name = formData.get('name');
|
|
15
|
+
* // Save to database...
|
|
16
|
+
* return { success: true, id: 123 };
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* // In a client component
|
|
20
|
+
* 'use client';
|
|
21
|
+
* import { createUser } from './actions';
|
|
22
|
+
*
|
|
23
|
+
* function Form() {
|
|
24
|
+
* return (
|
|
25
|
+
* <form action={createUser}>
|
|
26
|
+
* <input name="name" />
|
|
27
|
+
* <button type="submit">Create</button>
|
|
28
|
+
* </form>
|
|
29
|
+
* );
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { cookies, headers, redirect, notFound, RedirectError, NotFoundError } from '../helpers.js';
|
|
35
|
+
|
|
36
|
+
// Global action registry
|
|
37
|
+
declare global {
|
|
38
|
+
var __FLEXI_ACTIONS__: Record<string, ServerActionFunction>;
|
|
39
|
+
var __FLEXI_ACTION_CONTEXT__: ActionContext | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
globalThis.__FLEXI_ACTIONS__ = globalThis.__FLEXI_ACTIONS__ || {};
|
|
43
|
+
globalThis.__FLEXI_ACTION_CONTEXT__ = null;
|
|
44
|
+
|
|
45
|
+
export interface ActionContext {
|
|
46
|
+
request: Request;
|
|
47
|
+
cookies: typeof cookies;
|
|
48
|
+
headers: typeof headers;
|
|
49
|
+
redirect: typeof redirect;
|
|
50
|
+
notFound: typeof notFound;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type ServerActionFunction = (...args: any[]) => Promise<any>;
|
|
54
|
+
|
|
55
|
+
export interface ActionResult<T = any> {
|
|
56
|
+
success: boolean;
|
|
57
|
+
data?: T;
|
|
58
|
+
error?: string;
|
|
59
|
+
redirect?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Decorator to mark a function as a server action
|
|
64
|
+
*/
|
|
65
|
+
export function serverAction<T extends ServerActionFunction>(
|
|
66
|
+
fn: T,
|
|
67
|
+
actionId?: string
|
|
68
|
+
): T {
|
|
69
|
+
const id = actionId || `action_${fn.name}_${generateActionId()}`;
|
|
70
|
+
|
|
71
|
+
// Register the action
|
|
72
|
+
globalThis.__FLEXI_ACTIONS__[id] = fn;
|
|
73
|
+
|
|
74
|
+
// Create a proxy that will be serialized for the client
|
|
75
|
+
const proxy = (async (...args: any[]) => {
|
|
76
|
+
// If we're on the server, execute directly
|
|
77
|
+
if (typeof window === 'undefined') {
|
|
78
|
+
return await executeAction(id, args);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// If we're on the client, make a fetch request
|
|
82
|
+
return await callServerAction(id, args);
|
|
83
|
+
}) as T;
|
|
84
|
+
|
|
85
|
+
// Mark as server action
|
|
86
|
+
(proxy as any).$$typeof = Symbol.for('react.server.action');
|
|
87
|
+
(proxy as any).$$id = id;
|
|
88
|
+
(proxy as any).$$bound = null;
|
|
89
|
+
|
|
90
|
+
return proxy;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Register a server action
|
|
95
|
+
*/
|
|
96
|
+
export function registerAction(id: string, fn: ServerActionFunction): void {
|
|
97
|
+
globalThis.__FLEXI_ACTIONS__[id] = fn;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get a registered action
|
|
102
|
+
*/
|
|
103
|
+
export function getAction(id: string): ServerActionFunction | undefined {
|
|
104
|
+
return globalThis.__FLEXI_ACTIONS__[id];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Execute a server action on the server
|
|
109
|
+
*/
|
|
110
|
+
export async function executeAction(
|
|
111
|
+
actionId: string,
|
|
112
|
+
args: any[],
|
|
113
|
+
context?: Partial<ActionContext>
|
|
114
|
+
): Promise<ActionResult> {
|
|
115
|
+
const action = globalThis.__FLEXI_ACTIONS__[actionId];
|
|
116
|
+
|
|
117
|
+
if (!action) {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
error: `Server action not found: ${actionId}`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Set up action context
|
|
125
|
+
const actionContext: ActionContext = {
|
|
126
|
+
request: context?.request || new Request('http://localhost'),
|
|
127
|
+
cookies,
|
|
128
|
+
headers,
|
|
129
|
+
redirect,
|
|
130
|
+
notFound
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
globalThis.__FLEXI_ACTION_CONTEXT__ = actionContext;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const result = await action(...args);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
success: true,
|
|
140
|
+
data: result
|
|
141
|
+
};
|
|
142
|
+
} catch (error: any) {
|
|
143
|
+
// Handle redirect
|
|
144
|
+
if (error instanceof RedirectError) {
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
redirect: error.url
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Handle not found
|
|
152
|
+
if (error instanceof NotFoundError) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
error: 'Not found'
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
error: error.message || 'Action failed'
|
|
162
|
+
};
|
|
163
|
+
} finally {
|
|
164
|
+
globalThis.__FLEXI_ACTION_CONTEXT__ = null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Call a server action from the client
|
|
170
|
+
*/
|
|
171
|
+
export async function callServerAction(
|
|
172
|
+
actionId: string,
|
|
173
|
+
args: any[]
|
|
174
|
+
): Promise<ActionResult> {
|
|
175
|
+
try {
|
|
176
|
+
const response = await fetch('/_flexi/action', {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: {
|
|
179
|
+
'Content-Type': 'application/json',
|
|
180
|
+
'X-Flexi-Action': actionId
|
|
181
|
+
},
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
actionId,
|
|
184
|
+
args: serializeArgs(args)
|
|
185
|
+
}),
|
|
186
|
+
credentials: 'same-origin'
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new Error(`Action failed: ${response.statusText}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const result = await response.json();
|
|
194
|
+
|
|
195
|
+
// Handle redirect
|
|
196
|
+
if (result.redirect) {
|
|
197
|
+
window.location.href = result.redirect;
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return result;
|
|
202
|
+
} catch (error: any) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: error.message || 'Network error'
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Serialize action arguments for transmission
|
|
212
|
+
*/
|
|
213
|
+
function serializeArgs(args: any[]): any[] {
|
|
214
|
+
return args.map(arg => {
|
|
215
|
+
// Handle FormData
|
|
216
|
+
if (arg instanceof FormData) {
|
|
217
|
+
const obj: Record<string, any> = {};
|
|
218
|
+
arg.forEach((value, key) => {
|
|
219
|
+
if (obj[key]) {
|
|
220
|
+
// Handle multiple values
|
|
221
|
+
if (Array.isArray(obj[key])) {
|
|
222
|
+
obj[key].push(value);
|
|
223
|
+
} else {
|
|
224
|
+
obj[key] = [obj[key], value];
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
obj[key] = value;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
return { $$type: 'FormData', data: obj };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Handle File
|
|
234
|
+
if (typeof File !== 'undefined' && arg instanceof File) {
|
|
235
|
+
return { $$type: 'File', name: arg.name, type: arg.type, size: arg.size };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handle Date
|
|
239
|
+
if (arg instanceof Date) {
|
|
240
|
+
return { $$type: 'Date', value: arg.toISOString() };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Handle regular objects
|
|
244
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
245
|
+
return JSON.parse(JSON.stringify(arg));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return arg;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Deserialize action arguments on the server
|
|
254
|
+
*/
|
|
255
|
+
export function deserializeArgs(args: any[]): any[] {
|
|
256
|
+
return args.map(arg => {
|
|
257
|
+
if (arg && typeof arg === 'object') {
|
|
258
|
+
// Handle FormData
|
|
259
|
+
if (arg.$$type === 'FormData') {
|
|
260
|
+
const formData = new FormData();
|
|
261
|
+
for (const [key, value] of Object.entries(arg.data)) {
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
value.forEach(v => formData.append(key, v as string));
|
|
264
|
+
} else {
|
|
265
|
+
formData.append(key, value as string);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return formData;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Handle Date
|
|
272
|
+
if (arg.$$type === 'Date') {
|
|
273
|
+
return new Date(arg.value);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return arg;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Generate a unique action ID
|
|
283
|
+
*/
|
|
284
|
+
function generateActionId(): string {
|
|
285
|
+
return Math.random().toString(36).substring(2, 10);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Hook to get the current action context
|
|
290
|
+
*/
|
|
291
|
+
export function useActionContext(): ActionContext | null {
|
|
292
|
+
return globalThis.__FLEXI_ACTION_CONTEXT__;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Create a form action handler
|
|
297
|
+
* Wraps a server action for use with HTML forms
|
|
298
|
+
*/
|
|
299
|
+
export function formAction<T>(
|
|
300
|
+
action: (formData: FormData) => Promise<T>
|
|
301
|
+
): (formData: FormData) => Promise<ActionResult<T>> {
|
|
302
|
+
return async (formData: FormData) => {
|
|
303
|
+
try {
|
|
304
|
+
const result = await action(formData);
|
|
305
|
+
return { success: true, data: result };
|
|
306
|
+
} catch (error: any) {
|
|
307
|
+
if (error instanceof RedirectError) {
|
|
308
|
+
return { success: true, redirect: error.url };
|
|
309
|
+
}
|
|
310
|
+
return { success: false, error: error.message };
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* useFormState hook for progressive enhancement
|
|
317
|
+
* Works with server actions and provides loading/error states
|
|
318
|
+
*/
|
|
319
|
+
export function createFormState<T>(
|
|
320
|
+
action: (formData: FormData) => Promise<ActionResult<T>>,
|
|
321
|
+
initialState: T | null = null
|
|
322
|
+
) {
|
|
323
|
+
return {
|
|
324
|
+
action,
|
|
325
|
+
initialState,
|
|
326
|
+
// This will be enhanced on the client
|
|
327
|
+
pending: false,
|
|
328
|
+
error: null as string | null,
|
|
329
|
+
data: initialState
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Bind arguments to a server action
|
|
335
|
+
* Creates a new action with pre-filled arguments
|
|
336
|
+
*/
|
|
337
|
+
export function bindArgs<T extends ServerActionFunction>(
|
|
338
|
+
action: T,
|
|
339
|
+
...boundArgs: any[]
|
|
340
|
+
): T {
|
|
341
|
+
const boundAction = (async (...args: any[]) => {
|
|
342
|
+
return await (action as any)(...boundArgs, ...args);
|
|
343
|
+
}) as T;
|
|
344
|
+
|
|
345
|
+
// Copy action metadata
|
|
346
|
+
(boundAction as any).$$typeof = (action as any).$$typeof;
|
|
347
|
+
(boundAction as any).$$id = (action as any).$$id;
|
|
348
|
+
(boundAction as any).$$bound = boundArgs;
|
|
349
|
+
|
|
350
|
+
return boundAction;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export default {
|
|
354
|
+
serverAction,
|
|
355
|
+
registerAction,
|
|
356
|
+
getAction,
|
|
357
|
+
executeAction,
|
|
358
|
+
callServerAction,
|
|
359
|
+
deserializeArgs,
|
|
360
|
+
useActionContext,
|
|
361
|
+
formAction,
|
|
362
|
+
createFormState,
|
|
363
|
+
bindArgs
|
|
364
|
+
};
|