@flexireact/core 2.0.1 → 2.2.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/cli/index.ts CHANGED
@@ -856,10 +856,13 @@ ${pc.cyan(' ╰─────────────────────
856
856
  // Build Command
857
857
  // ============================================================================
858
858
 
859
- async function runBuild(): Promise<void> {
859
+ async function runBuild(options: { analyze?: boolean } = {}): Promise<void> {
860
860
  console.log(MINI_LOGO);
861
861
  log.blank();
862
862
  log.info('Building for production...');
863
+ if (options.analyze) {
864
+ log.info('Bundle analysis enabled');
865
+ }
863
866
  log.blank();
864
867
 
865
868
  const spinner = ora({ text: 'Compiling...', color: 'cyan' }).start();
@@ -876,16 +879,50 @@ async function runBuild(): Promise<void> {
876
879
  const rawConfig = await configModule.loadConfig(projectRoot);
877
880
  const config = configModule.resolvePaths(rawConfig, projectRoot);
878
881
 
879
- await buildModule.build({
882
+ const result = await buildModule.build({
880
883
  projectRoot,
881
884
  config,
882
- mode: 'production'
885
+ mode: 'production',
886
+ analyze: options.analyze
883
887
  });
884
888
 
885
889
  spinner.succeed('Build complete!');
886
890
  log.blank();
887
891
  log.success(`Output: ${pc.cyan('.flexi/')}`);
888
892
 
893
+ // Show bundle analysis if enabled
894
+ if (options.analyze && result?.analysis) {
895
+ log.blank();
896
+ log.info('📊 Bundle Analysis:');
897
+ log.blank();
898
+
899
+ const analysis = result.analysis;
900
+
901
+ // Sort by size
902
+ const sorted = Object.entries(analysis.files || {})
903
+ .sort((a: any, b: any) => b[1].size - a[1].size);
904
+
905
+ console.log(pc.dim(' ─────────────────────────────────────────────────'));
906
+ console.log(` ${pc.bold('File')}${' '.repeat(35)}${pc.bold('Size')}`);
907
+ console.log(pc.dim(' ─────────────────────────────────────────────────'));
908
+
909
+ for (const [file, info] of sorted.slice(0, 15) as any) {
910
+ const name = file.length > 35 ? '...' + file.slice(-32) : file;
911
+ const size = formatBytes(info.size);
912
+ const gzip = info.gzipSize ? pc.dim(` (${formatBytes(info.gzipSize)} gzip)`) : '';
913
+ console.log(` ${name.padEnd(38)} ${pc.cyan(size)}${gzip}`);
914
+ }
915
+
916
+ console.log(pc.dim(' ─────────────────────────────────────────────────'));
917
+ console.log(` ${pc.bold('Total:')}${' '.repeat(31)} ${pc.green(formatBytes(analysis.totalSize || 0))}`);
918
+
919
+ if (analysis.totalGzipSize) {
920
+ console.log(` ${pc.dim('Gzipped:')}${' '.repeat(29)} ${pc.dim(formatBytes(analysis.totalGzipSize))}`);
921
+ }
922
+
923
+ log.blank();
924
+ }
925
+
889
926
  } catch (error: any) {
890
927
  spinner.fail('Build failed');
891
928
  log.error(error.message);
@@ -893,6 +930,14 @@ async function runBuild(): Promise<void> {
893
930
  }
894
931
  }
895
932
 
933
+ function formatBytes(bytes: number): string {
934
+ if (bytes === 0) return '0 B';
935
+ const k = 1024;
936
+ const sizes = ['B', 'KB', 'MB', 'GB'];
937
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
938
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
939
+ }
940
+
896
941
  // ============================================================================
897
942
  // Start Command
898
943
  // ============================================================================
@@ -1078,7 +1123,8 @@ async function main(): Promise<void> {
1078
1123
  break;
1079
1124
 
1080
1125
  case 'build':
1081
- await runBuild();
1126
+ const analyzeFlag = args.includes('--analyze') || args.includes('-a');
1127
+ await runBuild({ analyze: analyzeFlag });
1082
1128
  break;
1083
1129
 
1084
1130
  case 'start':
@@ -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
+ };
@@ -28,7 +28,8 @@ export async function build(options) {
28
28
  const {
29
29
  projectRoot,
30
30
  config,
31
- mode = BuildMode.PRODUCTION
31
+ mode = BuildMode.PRODUCTION,
32
+ analyze = false
32
33
  } = options;
33
34
 
34
35
  const startTime = Date.now();
@@ -95,12 +96,79 @@ export async function build(options) {
95
96
  console.log(` Server modules: ${serverResult.outputs.length}`);
96
97
  console.log('');
97
98
 
99
+ // Generate bundle analysis if requested
100
+ let analysis = null;
101
+ if (analyze) {
102
+ analysis = generateBundleAnalysis(clientResult, serverResult, outDir);
103
+ }
104
+
98
105
  return {
99
106
  success: true,
100
107
  duration,
101
108
  manifest,
102
109
  clientResult,
103
- serverResult
110
+ serverResult,
111
+ analysis
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Generates bundle analysis data
117
+ */
118
+ function generateBundleAnalysis(clientResult, serverResult, outDir) {
119
+ const files: Record<string, { size: number; gzipSize?: number }> = {};
120
+ let totalSize = 0;
121
+ let totalGzipSize = 0;
122
+
123
+ // Analyze client outputs
124
+ for (const output of clientResult.outputs || []) {
125
+ if (output.path && fs.existsSync(output.path)) {
126
+ const stat = fs.statSync(output.path);
127
+ const relativePath = path.relative(outDir, output.path);
128
+
129
+ // Estimate gzip size (roughly 30% of original for JS)
130
+ const gzipSize = Math.round(stat.size * 0.3);
131
+
132
+ files[relativePath] = {
133
+ size: stat.size,
134
+ gzipSize
135
+ };
136
+
137
+ totalSize += stat.size;
138
+ totalGzipSize += gzipSize;
139
+ }
140
+ }
141
+
142
+ // Analyze server outputs
143
+ for (const output of serverResult.outputs || []) {
144
+ if (output.path && fs.existsSync(output.path)) {
145
+ const stat = fs.statSync(output.path);
146
+ const relativePath = path.relative(outDir, output.path);
147
+
148
+ files[relativePath] = {
149
+ size: stat.size
150
+ };
151
+
152
+ totalSize += stat.size;
153
+ }
154
+ }
155
+
156
+ return {
157
+ files,
158
+ totalSize,
159
+ totalGzipSize,
160
+ clientSize: clientResult.outputs?.reduce((sum, o) => {
161
+ if (o.path && fs.existsSync(o.path)) {
162
+ return sum + fs.statSync(o.path).size;
163
+ }
164
+ return sum;
165
+ }, 0) || 0,
166
+ serverSize: serverResult.outputs?.reduce((sum, o) => {
167
+ if (o.path && fs.existsSync(o.path)) {
168
+ return sum + fs.statSync(o.path).size;
169
+ }
170
+ return sum;
171
+ }, 0) || 0
104
172
  };
105
173
  }
106
174