@flexireact/core 2.4.0 → 2.5.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,616 @@
1
+ /**
2
+ * FlexiReact CLI Generators
3
+ * Scaffolding commands for rapid development
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import pc from 'picocolors';
9
+ import prompts from 'prompts';
10
+
11
+ const log = {
12
+ info: (msg: string) => console.log(`${pc.cyan('ℹ')} ${msg}`),
13
+ success: (msg: string) => console.log(`${pc.green('✓')} ${msg}`),
14
+ warn: (msg: string) => console.log(`${pc.yellow('⚠')} ${pc.yellow(msg)}`),
15
+ error: (msg: string) => console.log(`${pc.red('✗')} ${pc.red(msg)}`),
16
+ blank: () => console.log(''),
17
+ };
18
+
19
+ // ============================================================================
20
+ // Templates
21
+ // ============================================================================
22
+
23
+ const templates = {
24
+ // Page template
25
+ page: (name: string, options: { client?: boolean }) => `${options.client ? "'use client';\n\n" : ''}import React from 'react';
26
+
27
+ export default function ${toPascalCase(name)}Page() {
28
+ return (
29
+ <div className="min-h-screen p-8">
30
+ <h1 className="text-4xl font-bold">${toPascalCase(name)}</h1>
31
+ <p className="text-gray-400 mt-4">Welcome to ${name} page</p>
32
+ </div>
33
+ );
34
+ }
35
+ `,
36
+
37
+ // Layout template
38
+ layout: (name: string) => `import React from 'react';
39
+
40
+ interface ${toPascalCase(name)}LayoutProps {
41
+ children: React.ReactNode;
42
+ }
43
+
44
+ export default function ${toPascalCase(name)}Layout({ children }: ${toPascalCase(name)}LayoutProps) {
45
+ return (
46
+ <div className="${name}-layout">
47
+ {/* Add your layout wrapper here */}
48
+ {children}
49
+ </div>
50
+ );
51
+ }
52
+ `,
53
+
54
+ // Component template
55
+ component: (name: string, options: { client?: boolean; props?: boolean }) => {
56
+ const propsInterface = options.props ? `
57
+ interface ${toPascalCase(name)}Props {
58
+ className?: string;
59
+ children?: React.ReactNode;
60
+ }
61
+ ` : '';
62
+ const propsType = options.props ? `{ className, children }: ${toPascalCase(name)}Props` : '{}';
63
+
64
+ return `${options.client ? "'use client';\n\n" : ''}import React from 'react';
65
+ import { cn } from '@/lib/utils';
66
+ ${propsInterface}
67
+ export function ${toPascalCase(name)}(${propsType}) {
68
+ return (
69
+ <div className={cn('${toKebabCase(name)}', ${options.props ? 'className' : "''"})}>
70
+ ${options.props ? '{children}' : `{/* ${toPascalCase(name)} content */}`}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ export default ${toPascalCase(name)};
76
+ `;
77
+ },
78
+
79
+ // Hook template
80
+ hook: (name: string) => `import { useState, useEffect, useCallback } from 'react';
81
+
82
+ export function use${toPascalCase(name)}() {
83
+ const [state, setState] = useState(null);
84
+ const [loading, setLoading] = useState(false);
85
+ const [error, setError] = useState<Error | null>(null);
86
+
87
+ const execute = useCallback(async () => {
88
+ setLoading(true);
89
+ setError(null);
90
+ try {
91
+ // Add your logic here
92
+ setState(null);
93
+ } catch (err) {
94
+ setError(err instanceof Error ? err : new Error('Unknown error'));
95
+ } finally {
96
+ setLoading(false);
97
+ }
98
+ }, []);
99
+
100
+ return { state, loading, error, execute };
101
+ }
102
+
103
+ export default use${toPascalCase(name)};
104
+ `,
105
+
106
+ // API route template
107
+ api: (name: string) => `import type { IncomingMessage, ServerResponse } from 'http';
108
+
109
+ export default async function handler(req: IncomingMessage, res: ServerResponse) {
110
+ const method = req.method;
111
+
112
+ switch (method) {
113
+ case 'GET':
114
+ return handleGet(req, res);
115
+ case 'POST':
116
+ return handlePost(req, res);
117
+ default:
118
+ res.statusCode = 405;
119
+ res.setHeader('Content-Type', 'application/json');
120
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
121
+ }
122
+ }
123
+
124
+ async function handleGet(req: IncomingMessage, res: ServerResponse) {
125
+ res.statusCode = 200;
126
+ res.setHeader('Content-Type', 'application/json');
127
+ res.end(JSON.stringify({ message: '${name} API - GET' }));
128
+ }
129
+
130
+ async function handlePost(req: IncomingMessage, res: ServerResponse) {
131
+ // Parse body
132
+ let body = '';
133
+ for await (const chunk of req) {
134
+ body += chunk;
135
+ }
136
+ const data = body ? JSON.parse(body) : {};
137
+
138
+ res.statusCode = 200;
139
+ res.setHeader('Content-Type', 'application/json');
140
+ res.end(JSON.stringify({ message: '${name} API - POST', data }));
141
+ }
142
+ `,
143
+
144
+ // Server Action template
145
+ action: (name: string) => `'use server';
146
+
147
+ import { revalidatePath } from '@flexireact/core';
148
+
149
+ export async function ${toCamelCase(name)}Action(formData: FormData) {
150
+ // Validate input
151
+ const data = Object.fromEntries(formData);
152
+
153
+ try {
154
+ // Add your server logic here
155
+ console.log('${toPascalCase(name)} action executed:', data);
156
+
157
+ // Revalidate cache if needed
158
+ // revalidatePath('/');
159
+
160
+ return { success: true, data };
161
+ } catch (error) {
162
+ return {
163
+ success: false,
164
+ error: error instanceof Error ? error.message : 'Unknown error'
165
+ };
166
+ }
167
+ }
168
+ `,
169
+
170
+ // Middleware template
171
+ middleware: (name: string) => `import type { IncomingMessage, ServerResponse } from 'http';
172
+
173
+ export interface ${toPascalCase(name)}MiddlewareOptions {
174
+ // Add your options here
175
+ }
176
+
177
+ export function ${toCamelCase(name)}Middleware(options: ${toPascalCase(name)}MiddlewareOptions = {}) {
178
+ return async (
179
+ req: IncomingMessage,
180
+ res: ServerResponse,
181
+ next: () => Promise<void>
182
+ ) => {
183
+ // Before request handling
184
+ console.log('[${toPascalCase(name)}] Request:', req.url);
185
+
186
+ // Continue to next middleware/handler
187
+ await next();
188
+
189
+ // After request handling (optional)
190
+ };
191
+ }
192
+
193
+ export default ${toCamelCase(name)}Middleware;
194
+ `,
195
+
196
+ // Context template
197
+ context: (name: string) => `'use client';
198
+
199
+ import React, { createContext, useContext, useState, useCallback } from 'react';
200
+
201
+ interface ${toPascalCase(name)}State {
202
+ // Add your state properties here
203
+ value: string | null;
204
+ }
205
+
206
+ interface ${toPascalCase(name)}ContextValue extends ${toPascalCase(name)}State {
207
+ setValue: (value: string) => void;
208
+ reset: () => void;
209
+ }
210
+
211
+ const ${toPascalCase(name)}Context = createContext<${toPascalCase(name)}ContextValue | null>(null);
212
+
213
+ export function ${toPascalCase(name)}Provider({ children }: { children: React.ReactNode }) {
214
+ const [state, setState] = useState<${toPascalCase(name)}State>({
215
+ value: null,
216
+ });
217
+
218
+ const setValue = useCallback((value: string) => {
219
+ setState(prev => ({ ...prev, value }));
220
+ }, []);
221
+
222
+ const reset = useCallback(() => {
223
+ setState({ value: null });
224
+ }, []);
225
+
226
+ return (
227
+ <${toPascalCase(name)}Context.Provider value={{ ...state, setValue, reset }}>
228
+ {children}
229
+ </${toPascalCase(name)}Context.Provider>
230
+ );
231
+ }
232
+
233
+ export function use${toPascalCase(name)}() {
234
+ const context = useContext(${toPascalCase(name)}Context);
235
+ if (!context) {
236
+ throw new Error('use${toPascalCase(name)} must be used within a ${toPascalCase(name)}Provider');
237
+ }
238
+ return context;
239
+ }
240
+ `,
241
+
242
+ // Loading template
243
+ loading: () => `import React from 'react';
244
+ import { Spinner } from '@flexireact/flexi-ui';
245
+
246
+ export default function Loading() {
247
+ return (
248
+ <div className="min-h-screen flex items-center justify-center">
249
+ <Spinner size="lg" />
250
+ </div>
251
+ );
252
+ }
253
+ `,
254
+
255
+ // Error template
256
+ error: () => `'use client';
257
+
258
+ import React from 'react';
259
+ import { Button, Alert } from '@flexireact/flexi-ui';
260
+
261
+ interface ErrorProps {
262
+ error: Error;
263
+ reset: () => void;
264
+ }
265
+
266
+ export default function Error({ error, reset }: ErrorProps) {
267
+ return (
268
+ <div className="min-h-screen flex flex-col items-center justify-center p-8">
269
+ <Alert variant="error" className="max-w-md mb-8">
270
+ <h2 className="font-bold text-lg mb-2">Something went wrong</h2>
271
+ <p className="text-sm">{error.message}</p>
272
+ </Alert>
273
+ <Button onClick={reset}>Try again</Button>
274
+ </div>
275
+ );
276
+ }
277
+ `,
278
+
279
+ // Not found template
280
+ notFound: () => `import React from 'react';
281
+ import { Button } from '@flexireact/flexi-ui';
282
+
283
+ export default function NotFound() {
284
+ return (
285
+ <div className="min-h-screen flex flex-col items-center justify-center p-8">
286
+ <h1 className="text-8xl font-bold text-primary mb-4">404</h1>
287
+ <p className="text-gray-400 text-xl mb-8">Page not found</p>
288
+ <Button asChild>
289
+ <a href="/">← Back Home</a>
290
+ </Button>
291
+ </div>
292
+ );
293
+ }
294
+ `,
295
+ };
296
+
297
+ // ============================================================================
298
+ // Utility Functions
299
+ // ============================================================================
300
+
301
+ function toPascalCase(str: string): string {
302
+ return str
303
+ .replace(/[-_](.)/g, (_, c) => c.toUpperCase())
304
+ .replace(/^(.)/, (_, c) => c.toUpperCase());
305
+ }
306
+
307
+ function toCamelCase(str: string): string {
308
+ return str
309
+ .replace(/[-_](.)/g, (_, c) => c.toUpperCase())
310
+ .replace(/^(.)/, (_, c) => c.toLowerCase());
311
+ }
312
+
313
+ function toKebabCase(str: string): string {
314
+ return str
315
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
316
+ .replace(/[_\s]+/g, '-')
317
+ .toLowerCase();
318
+ }
319
+
320
+ function ensureDir(filePath: string): void {
321
+ const dir = path.dirname(filePath);
322
+ if (!fs.existsSync(dir)) {
323
+ fs.mkdirSync(dir, { recursive: true });
324
+ }
325
+ }
326
+
327
+ function writeFile(filePath: string, content: string): void {
328
+ ensureDir(filePath);
329
+ fs.writeFileSync(filePath, content);
330
+ }
331
+
332
+ // ============================================================================
333
+ // Generator Commands
334
+ // ============================================================================
335
+
336
+ export type GeneratorType =
337
+ | 'page'
338
+ | 'layout'
339
+ | 'component'
340
+ | 'hook'
341
+ | 'api'
342
+ | 'action'
343
+ | 'middleware'
344
+ | 'context'
345
+ | 'loading'
346
+ | 'error'
347
+ | 'not-found';
348
+
349
+ export async function runGenerate(type?: string, name?: string): Promise<void> {
350
+ const cwd = process.cwd();
351
+
352
+ // Check if we're in a FlexiReact project
353
+ if (!fs.existsSync(path.join(cwd, 'package.json'))) {
354
+ log.error('Not in a FlexiReact project. Run this command in your project root.');
355
+ process.exit(1);
356
+ }
357
+
358
+ // Interactive mode if no type provided
359
+ if (!type) {
360
+ const response = await prompts([
361
+ {
362
+ type: 'select',
363
+ name: 'type',
364
+ message: 'What do you want to generate?',
365
+ choices: [
366
+ { title: '📄 Page', value: 'page', description: 'A new page in app/ or pages/' },
367
+ { title: '📐 Layout', value: 'layout', description: 'A layout wrapper component' },
368
+ { title: '🧩 Component', value: 'component', description: 'A reusable React component' },
369
+ { title: '🪝 Hook', value: 'hook', description: 'A custom React hook' },
370
+ { title: '🔌 API Route', value: 'api', description: 'An API endpoint' },
371
+ { title: '⚡ Server Action', value: 'action', description: 'A server action function' },
372
+ { title: '🛡️ Middleware', value: 'middleware', description: 'Request middleware' },
373
+ { title: '🌐 Context', value: 'context', description: 'React context provider' },
374
+ { title: '⏳ Loading', value: 'loading', description: 'Loading state component' },
375
+ { title: '❌ Error', value: 'error', description: 'Error boundary component' },
376
+ { title: '🔍 Not Found', value: 'not-found', description: '404 page component' },
377
+ ],
378
+ },
379
+ {
380
+ type: (prev) => ['loading', 'error', 'not-found'].includes(prev) ? null : 'text',
381
+ name: 'name',
382
+ message: 'Name:',
383
+ validate: (v: string) => v.length > 0 || 'Name is required',
384
+ },
385
+ ]);
386
+
387
+ if (!response.type) process.exit(0);
388
+ type = response.type;
389
+ name = response.name;
390
+ }
391
+
392
+ // Validate type
393
+ const validTypes: GeneratorType[] = [
394
+ 'page', 'layout', 'component', 'hook', 'api',
395
+ 'action', 'middleware', 'context', 'loading', 'error', 'not-found'
396
+ ];
397
+
398
+ if (!validTypes.includes(type as GeneratorType)) {
399
+ log.error(`Invalid type: ${type}`);
400
+ log.info(`Valid types: ${validTypes.join(', ')}`);
401
+ process.exit(1);
402
+ }
403
+
404
+ // Special types that don't need a name
405
+ if (type && ['loading', 'error', 'not-found'].includes(type)) {
406
+ await generateSpecialFile(type as 'loading' | 'error' | 'not-found', cwd);
407
+ return;
408
+ }
409
+
410
+ // Get name if not provided
411
+ let finalName = name;
412
+ if (!finalName) {
413
+ const response = await prompts({
414
+ type: 'text',
415
+ name: 'name',
416
+ message: `${toPascalCase(type || 'item')} name:`,
417
+ validate: (v: string) => v.length > 0 || 'Name is required',
418
+ });
419
+ finalName = response.name;
420
+ if (!finalName) process.exit(0);
421
+ }
422
+
423
+ // Generate based on type
424
+ switch (type) {
425
+ case 'page':
426
+ await generatePage(finalName, cwd);
427
+ break;
428
+ case 'layout':
429
+ await generateLayout(finalName, cwd);
430
+ break;
431
+ case 'component':
432
+ await generateComponent(finalName, cwd);
433
+ break;
434
+ case 'hook':
435
+ await generateHook(finalName, cwd);
436
+ break;
437
+ case 'api':
438
+ await generateApi(finalName, cwd);
439
+ break;
440
+ case 'action':
441
+ await generateAction(finalName, cwd);
442
+ break;
443
+ case 'middleware':
444
+ await generateMiddleware(finalName, cwd);
445
+ break;
446
+ case 'context':
447
+ await generateContext(finalName, cwd);
448
+ break;
449
+ }
450
+ }
451
+
452
+ async function generatePage(name: string, cwd: string): Promise<void> {
453
+ const response = await prompts([
454
+ {
455
+ type: 'select',
456
+ name: 'directory',
457
+ message: 'Where to create the page?',
458
+ choices: [
459
+ { title: 'app/ (App Router)', value: 'app' },
460
+ { title: 'pages/ (Pages Router)', value: 'pages' },
461
+ ],
462
+ },
463
+ {
464
+ type: 'toggle',
465
+ name: 'client',
466
+ message: 'Client component? (use client)',
467
+ initial: false,
468
+ active: 'Yes',
469
+ inactive: 'No',
470
+ },
471
+ ]);
472
+
473
+ const fileName = response.directory === 'app' ? 'page.tsx' : `${toKebabCase(name)}.tsx`;
474
+ const filePath = response.directory === 'app'
475
+ ? path.join(cwd, 'app', toKebabCase(name), fileName)
476
+ : path.join(cwd, 'pages', fileName);
477
+
478
+ writeFile(filePath, templates.page(name, { client: response.client }));
479
+ log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
480
+ }
481
+
482
+ async function generateLayout(name: string, cwd: string): Promise<void> {
483
+ const filePath = path.join(cwd, 'app', toKebabCase(name), 'layout.tsx');
484
+ writeFile(filePath, templates.layout(name));
485
+ log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
486
+ }
487
+
488
+ async function generateComponent(name: string, cwd: string): Promise<void> {
489
+ const response = await prompts([
490
+ {
491
+ type: 'select',
492
+ name: 'directory',
493
+ message: 'Where to create the component?',
494
+ choices: [
495
+ { title: 'components/', value: 'components' },
496
+ { title: 'app/components/', value: 'app/components' },
497
+ ],
498
+ },
499
+ {
500
+ type: 'toggle',
501
+ name: 'client',
502
+ message: 'Client component?',
503
+ initial: true,
504
+ active: 'Yes',
505
+ inactive: 'No',
506
+ },
507
+ {
508
+ type: 'toggle',
509
+ name: 'props',
510
+ message: 'Include props interface?',
511
+ initial: true,
512
+ active: 'Yes',
513
+ inactive: 'No',
514
+ },
515
+ ]);
516
+
517
+ const filePath = path.join(cwd, response.directory, `${toPascalCase(name)}.tsx`);
518
+ writeFile(filePath, templates.component(name, { client: response.client, props: response.props }));
519
+ log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
520
+ }
521
+
522
+ async function generateHook(name: string, cwd: string): Promise<void> {
523
+ const hookName = name.startsWith('use') ? name : `use-${name}`;
524
+ const filePath = path.join(cwd, 'hooks', `${toKebabCase(hookName)}.ts`);
525
+ writeFile(filePath, templates.hook(hookName.replace(/^use-?/, '')));
526
+ log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
527
+ }
528
+
529
+ async function generateApi(name: string, cwd: string): Promise<void> {
530
+ const filePath = path.join(cwd, 'pages', 'api', `${toKebabCase(name)}.ts`);
531
+ writeFile(filePath, templates.api(name));
532
+ log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
533
+ }
534
+
535
+ async function generateAction(name: string, cwd: string): Promise<void> {
536
+ const filePath = path.join(cwd, 'actions', `${toKebabCase(name)}.ts`);
537
+ writeFile(filePath, templates.action(name));
538
+ log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
539
+ }
540
+
541
+ async function generateMiddleware(name: string, cwd: string): Promise<void> {
542
+ const filePath = path.join(cwd, 'middleware', `${toKebabCase(name)}.ts`);
543
+ writeFile(filePath, templates.middleware(name));
544
+ log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
545
+ }
546
+
547
+ async function generateContext(name: string, cwd: string): Promise<void> {
548
+ const filePath = path.join(cwd, 'contexts', `${toPascalCase(name)}Context.tsx`);
549
+ writeFile(filePath, templates.context(name));
550
+ log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
551
+ }
552
+
553
+ async function generateSpecialFile(type: 'loading' | 'error' | 'not-found', cwd: string): Promise<void> {
554
+ const response = await prompts({
555
+ type: 'text',
556
+ name: 'path',
557
+ message: 'Path (relative to app/):',
558
+ initial: '',
559
+ });
560
+
561
+ const basePath = response.path ? path.join(cwd, 'app', response.path) : path.join(cwd, 'app');
562
+
563
+ let fileName: string;
564
+ let content: string;
565
+
566
+ switch (type) {
567
+ case 'loading':
568
+ fileName = 'loading.tsx';
569
+ content = templates.loading();
570
+ break;
571
+ case 'error':
572
+ fileName = 'error.tsx';
573
+ content = templates.error();
574
+ break;
575
+ case 'not-found':
576
+ fileName = 'not-found.tsx';
577
+ content = templates.notFound();
578
+ break;
579
+ }
580
+
581
+ const filePath = path.join(basePath, fileName);
582
+ writeFile(filePath, content);
583
+ log.success(`Created ${pc.cyan(path.relative(cwd, filePath))}`);
584
+ }
585
+
586
+ // ============================================================================
587
+ // List Generators
588
+ // ============================================================================
589
+
590
+ export function listGenerators(): void {
591
+ console.log(`
592
+ ${pc.bold('Available Generators:')}
593
+
594
+ ${pc.cyan('page')} Create a new page (app/ or pages/)
595
+ ${pc.cyan('layout')} Create a layout wrapper
596
+ ${pc.cyan('component')} Create a React component
597
+ ${pc.cyan('hook')} Create a custom hook
598
+ ${pc.cyan('api')} Create an API route
599
+ ${pc.cyan('action')} Create a server action
600
+ ${pc.cyan('middleware')} Create request middleware
601
+ ${pc.cyan('context')} Create a React context
602
+ ${pc.cyan('loading')} Create a loading component
603
+ ${pc.cyan('error')} Create an error boundary
604
+ ${pc.cyan('not-found')} Create a 404 page
605
+
606
+ ${pc.bold('Usage:')}
607
+ ${pc.dim('$')} flexi generate ${pc.cyan('<type>')} ${pc.dim('[name]')}
608
+ ${pc.dim('$')} flexi g ${pc.cyan('<type>')} ${pc.dim('[name]')}
609
+
610
+ ${pc.bold('Examples:')}
611
+ ${pc.dim('$')} flexi g page dashboard
612
+ ${pc.dim('$')} flexi g component Button
613
+ ${pc.dim('$')} flexi g hook auth
614
+ ${pc.dim('$')} flexi g api users
615
+ `);
616
+ }
package/cli/index.ts CHANGED
@@ -12,10 +12,11 @@ import { execSync, spawn } from 'child_process';
12
12
  import pc from 'picocolors';
13
13
  import prompts from 'prompts';
14
14
  import ora from 'ora';
15
+ import { runGenerate, listGenerators } from './generators.js';
15
16
 
16
17
  const __filename = fileURLToPath(import.meta.url);
17
18
  const __dirname = path.dirname(__filename);
18
- const VERSION = '2.1.0';
19
+ const VERSION = '2.5.0';
19
20
 
20
21
  // ============================================================================
21
22
  // ASCII Logo & Branding
@@ -1090,18 +1091,26 @@ function showHelp(): void {
1090
1091
  log.blank();
1091
1092
 
1092
1093
  console.log(` ${pc.bold('Commands:')}`);
1093
- console.log(` ${pc.cyan('create')} ${pc.dim('<name>')} Create a new FlexiReact project`);
1094
- console.log(` ${pc.cyan('dev')} Start development server`);
1095
- console.log(` ${pc.cyan('build')} Build for production`);
1096
- console.log(` ${pc.cyan('start')} Start production server`);
1097
- console.log(` ${pc.cyan('doctor')} Check project health`);
1098
- console.log(` ${pc.cyan('help')} Show this help message`);
1094
+ console.log(` ${pc.cyan('create')} ${pc.dim('<name>')} Create a new FlexiReact project`);
1095
+ console.log(` ${pc.cyan('dev')} Start development server`);
1096
+ console.log(` ${pc.cyan('build')} Build for production`);
1097
+ console.log(` ${pc.cyan('start')} Start production server`);
1098
+ console.log(` ${pc.cyan('generate')} ${pc.dim('<type>')} Generate component/page/hook/etc (alias: g)`);
1099
+ console.log(` ${pc.cyan('doctor')} Check project health`);
1100
+ console.log(` ${pc.cyan('help')} Show this help message`);
1101
+ log.blank();
1102
+
1103
+ console.log(` ${pc.bold('Generate Types:')}`);
1104
+ console.log(` ${pc.dim('page, layout, component, hook, api, action, middleware, context')}`);
1105
+ console.log(` ${pc.dim('loading, error, not-found')}`);
1099
1106
  log.blank();
1100
1107
 
1101
1108
  console.log(` ${pc.bold('Examples:')}`);
1102
1109
  console.log(` ${pc.dim('$')} flexi create my-app`);
1103
1110
  console.log(` ${pc.dim('$')} flexi dev`);
1104
- console.log(` ${pc.dim('$')} flexi build && flexi start`);
1111
+ console.log(` ${pc.dim('$')} flexi g component Button`);
1112
+ console.log(` ${pc.dim('$')} flexi g page dashboard`);
1113
+ console.log(` ${pc.dim('$')} flexi build --analyze`);
1105
1114
  log.blank();
1106
1115
  }
1107
1116
 
@@ -1135,6 +1144,16 @@ async function main(): Promise<void> {
1135
1144
  await runDoctor();
1136
1145
  break;
1137
1146
 
1147
+ case 'generate':
1148
+ case 'g':
1149
+ await runGenerate(args[1], args[2]);
1150
+ break;
1151
+
1152
+ case 'generate:list':
1153
+ case 'g:list':
1154
+ listGenerators();
1155
+ break;
1156
+
1138
1157
  case 'version':
1139
1158
  case '-v':
1140
1159
  case '--version':
@@ -0,0 +1,644 @@
1
+ /**
2
+ * FlexiReact DevTools
3
+ * Advanced development tools for debugging and performance monitoring
4
+ */
5
+
6
+ import React from 'react';
7
+
8
+ // ============================================================================
9
+ // DevTools State
10
+ // ============================================================================
11
+
12
+ interface DevToolsState {
13
+ enabled: boolean;
14
+ position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
15
+ expanded: boolean;
16
+ activeTab: 'routes' | 'components' | 'network' | 'performance' | 'state' | 'console';
17
+ theme: 'dark' | 'light';
18
+ }
19
+
20
+ interface RouteInfo {
21
+ path: string;
22
+ component: string;
23
+ params: Record<string, string>;
24
+ query: Record<string, string>;
25
+ loadTime: number;
26
+ }
27
+
28
+ interface ComponentInfo {
29
+ name: string;
30
+ renderCount: number;
31
+ lastRenderTime: number;
32
+ props: Record<string, any>;
33
+ isIsland: boolean;
34
+ }
35
+
36
+ interface NetworkRequest {
37
+ id: string;
38
+ url: string;
39
+ method: string;
40
+ status: number;
41
+ duration: number;
42
+ size: number;
43
+ timestamp: number;
44
+ type: 'fetch' | 'xhr' | 'ssr' | 'action';
45
+ }
46
+
47
+ interface PerformanceMetric {
48
+ name: string;
49
+ value: number;
50
+ rating: 'good' | 'needs-improvement' | 'poor';
51
+ }
52
+
53
+ // Global DevTools state
54
+ const devToolsState: {
55
+ routes: RouteInfo[];
56
+ components: Map<string, ComponentInfo>;
57
+ network: NetworkRequest[];
58
+ performance: PerformanceMetric[];
59
+ logs: Array<{ level: string; message: string; timestamp: number }>;
60
+ listeners: Set<() => void>;
61
+ } = {
62
+ routes: [],
63
+ components: new Map(),
64
+ network: [],
65
+ performance: [],
66
+ logs: [],
67
+ listeners: new Set(),
68
+ };
69
+
70
+ // ============================================================================
71
+ // DevTools API
72
+ // ============================================================================
73
+
74
+ export const devtools = {
75
+ // Track route navigation
76
+ trackRoute(info: RouteInfo): void {
77
+ devToolsState.routes.unshift(info);
78
+ if (devToolsState.routes.length > 50) {
79
+ devToolsState.routes.pop();
80
+ }
81
+ this.notify();
82
+ },
83
+
84
+ // Track component render
85
+ trackComponent(name: string, info: Partial<ComponentInfo>): void {
86
+ const existing = devToolsState.components.get(name) || {
87
+ name,
88
+ renderCount: 0,
89
+ lastRenderTime: 0,
90
+ props: {},
91
+ isIsland: false,
92
+ };
93
+
94
+ devToolsState.components.set(name, {
95
+ ...existing,
96
+ ...info,
97
+ renderCount: existing.renderCount + 1,
98
+ lastRenderTime: Date.now(),
99
+ });
100
+ this.notify();
101
+ },
102
+
103
+ // Track network request
104
+ trackRequest(request: NetworkRequest): void {
105
+ devToolsState.network.unshift(request);
106
+ if (devToolsState.network.length > 100) {
107
+ devToolsState.network.pop();
108
+ }
109
+ this.notify();
110
+ },
111
+
112
+ // Track performance metric
113
+ trackMetric(metric: PerformanceMetric): void {
114
+ const existing = devToolsState.performance.findIndex(m => m.name === metric.name);
115
+ if (existing >= 0) {
116
+ devToolsState.performance[existing] = metric;
117
+ } else {
118
+ devToolsState.performance.push(metric);
119
+ }
120
+ this.notify();
121
+ },
122
+
123
+ // Log message
124
+ log(level: 'info' | 'warn' | 'error' | 'debug', message: string): void {
125
+ devToolsState.logs.unshift({
126
+ level,
127
+ message,
128
+ timestamp: Date.now(),
129
+ });
130
+ if (devToolsState.logs.length > 200) {
131
+ devToolsState.logs.pop();
132
+ }
133
+ this.notify();
134
+ },
135
+
136
+ // Get current state
137
+ getState() {
138
+ return {
139
+ routes: devToolsState.routes,
140
+ components: Array.from(devToolsState.components.values()),
141
+ network: devToolsState.network,
142
+ performance: devToolsState.performance,
143
+ logs: devToolsState.logs,
144
+ };
145
+ },
146
+
147
+ // Subscribe to changes
148
+ subscribe(listener: () => void): () => void {
149
+ devToolsState.listeners.add(listener);
150
+ return () => devToolsState.listeners.delete(listener);
151
+ },
152
+
153
+ // Notify listeners
154
+ notify(): void {
155
+ devToolsState.listeners.forEach(listener => listener());
156
+ },
157
+
158
+ // Clear all data
159
+ clear(): void {
160
+ devToolsState.routes = [];
161
+ devToolsState.components.clear();
162
+ devToolsState.network = [];
163
+ devToolsState.performance = [];
164
+ devToolsState.logs = [];
165
+ this.notify();
166
+ },
167
+ };
168
+
169
+ // ============================================================================
170
+ // Performance Monitoring
171
+ // ============================================================================
172
+
173
+ export function initPerformanceMonitoring(): void {
174
+ if (typeof window === 'undefined') return;
175
+
176
+ // Core Web Vitals
177
+ try {
178
+ // LCP (Largest Contentful Paint)
179
+ new PerformanceObserver((list) => {
180
+ const entries = list.getEntries();
181
+ const lastEntry = entries[entries.length - 1] as any;
182
+ devtools.trackMetric({
183
+ name: 'LCP',
184
+ value: lastEntry.startTime,
185
+ rating: lastEntry.startTime < 2500 ? 'good' : lastEntry.startTime < 4000 ? 'needs-improvement' : 'poor',
186
+ });
187
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
188
+
189
+ // FID (First Input Delay)
190
+ new PerformanceObserver((list) => {
191
+ const entries = list.getEntries();
192
+ entries.forEach((entry: any) => {
193
+ devtools.trackMetric({
194
+ name: 'FID',
195
+ value: entry.processingStart - entry.startTime,
196
+ rating: entry.processingStart - entry.startTime < 100 ? 'good' :
197
+ entry.processingStart - entry.startTime < 300 ? 'needs-improvement' : 'poor',
198
+ });
199
+ });
200
+ }).observe({ type: 'first-input', buffered: true });
201
+
202
+ // CLS (Cumulative Layout Shift)
203
+ let clsValue = 0;
204
+ new PerformanceObserver((list) => {
205
+ for (const entry of list.getEntries() as any[]) {
206
+ if (!entry.hadRecentInput) {
207
+ clsValue += entry.value;
208
+ }
209
+ }
210
+ devtools.trackMetric({
211
+ name: 'CLS',
212
+ value: clsValue,
213
+ rating: clsValue < 0.1 ? 'good' : clsValue < 0.25 ? 'needs-improvement' : 'poor',
214
+ });
215
+ }).observe({ type: 'layout-shift', buffered: true });
216
+
217
+ // TTFB (Time to First Byte)
218
+ const navEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
219
+ if (navEntry) {
220
+ devtools.trackMetric({
221
+ name: 'TTFB',
222
+ value: navEntry.responseStart - navEntry.requestStart,
223
+ rating: navEntry.responseStart - navEntry.requestStart < 200 ? 'good' :
224
+ navEntry.responseStart - navEntry.requestStart < 500 ? 'needs-improvement' : 'poor',
225
+ });
226
+ }
227
+ } catch (e) {
228
+ // Performance API not fully supported
229
+ }
230
+ }
231
+
232
+ // ============================================================================
233
+ // Network Interceptor
234
+ // ============================================================================
235
+
236
+ export function initNetworkInterceptor(): void {
237
+ if (typeof window === 'undefined') return;
238
+
239
+ // Intercept fetch
240
+ const originalFetch = window.fetch;
241
+ window.fetch = async function(...args) {
242
+ const startTime = Date.now();
243
+ const url = typeof args[0] === 'string' ? args[0] : (args[0] as Request).url;
244
+ const method = typeof args[0] === 'string' ? (args[1]?.method || 'GET') : (args[0] as Request).method;
245
+
246
+ try {
247
+ const response = await originalFetch.apply(this, args);
248
+ const clone = response.clone();
249
+ const size = (await clone.blob()).size;
250
+
251
+ devtools.trackRequest({
252
+ id: Math.random().toString(36).slice(2),
253
+ url,
254
+ method,
255
+ status: response.status,
256
+ duration: Date.now() - startTime,
257
+ size,
258
+ timestamp: startTime,
259
+ type: url.includes('/_flexi/action') ? 'action' : 'fetch',
260
+ });
261
+
262
+ return response;
263
+ } catch (error) {
264
+ devtools.trackRequest({
265
+ id: Math.random().toString(36).slice(2),
266
+ url,
267
+ method,
268
+ status: 0,
269
+ duration: Date.now() - startTime,
270
+ size: 0,
271
+ timestamp: startTime,
272
+ type: 'fetch',
273
+ });
274
+ throw error;
275
+ }
276
+ };
277
+ }
278
+
279
+ // ============================================================================
280
+ // DevTools Overlay Component
281
+ // ============================================================================
282
+
283
+ export function DevToolsOverlay(): React.ReactElement | null {
284
+ const [state, setState] = React.useState<DevToolsState>({
285
+ enabled: true,
286
+ position: 'bottom-right',
287
+ expanded: false,
288
+ activeTab: 'routes',
289
+ theme: 'dark',
290
+ });
291
+
292
+ const [data, setData] = React.useState(devtools.getState());
293
+
294
+ React.useEffect(() => {
295
+ return devtools.subscribe(() => {
296
+ setData(devtools.getState());
297
+ });
298
+ }, []);
299
+
300
+ React.useEffect(() => {
301
+ initPerformanceMonitoring();
302
+ initNetworkInterceptor();
303
+ }, []);
304
+
305
+ // Keyboard shortcut (Ctrl+Shift+D)
306
+ React.useEffect(() => {
307
+ const handler = (e: KeyboardEvent) => {
308
+ if (e.ctrlKey && e.shiftKey && e.key === 'D') {
309
+ setState(s => ({ ...s, expanded: !s.expanded }));
310
+ }
311
+ };
312
+ window.addEventListener('keydown', handler);
313
+ return () => window.removeEventListener('keydown', handler);
314
+ }, []);
315
+
316
+ if (!state.enabled) return null;
317
+
318
+ const positionStyles: Record<string, React.CSSProperties> = {
319
+ 'bottom-right': { bottom: 16, right: 16 },
320
+ 'bottom-left': { bottom: 16, left: 16 },
321
+ 'top-right': { top: 16, right: 16 },
322
+ 'top-left': { top: 16, left: 16 },
323
+ };
324
+
325
+ // Mini button when collapsed
326
+ if (!state.expanded) {
327
+ return React.createElement('button', {
328
+ onClick: () => setState(s => ({ ...s, expanded: true })),
329
+ style: {
330
+ position: 'fixed',
331
+ ...positionStyles[state.position],
332
+ zIndex: 99999,
333
+ width: 48,
334
+ height: 48,
335
+ borderRadius: 12,
336
+ background: 'linear-gradient(135deg, #00FF9C 0%, #00D68F 100%)',
337
+ border: 'none',
338
+ cursor: 'pointer',
339
+ display: 'flex',
340
+ alignItems: 'center',
341
+ justifyContent: 'center',
342
+ boxShadow: '0 4px 20px rgba(0, 255, 156, 0.3)',
343
+ transition: 'transform 0.2s',
344
+ },
345
+ onMouseEnter: (e: any) => e.target.style.transform = 'scale(1.1)',
346
+ onMouseLeave: (e: any) => e.target.style.transform = 'scale(1)',
347
+ title: 'FlexiReact DevTools (Ctrl+Shift+D)',
348
+ }, React.createElement('span', { style: { fontSize: 24 } }, '⚡'));
349
+ }
350
+
351
+ // Full panel
352
+ const tabs = [
353
+ { id: 'routes', label: '🗺️ Routes', count: data.routes.length },
354
+ { id: 'components', label: '🧩 Components', count: data.components.length },
355
+ { id: 'network', label: '🌐 Network', count: data.network.length },
356
+ { id: 'performance', label: '📊 Performance', count: data.performance.length },
357
+ { id: 'console', label: '📝 Console', count: data.logs.length },
358
+ ];
359
+
360
+ return React.createElement('div', {
361
+ style: {
362
+ position: 'fixed',
363
+ ...positionStyles[state.position],
364
+ zIndex: 99999,
365
+ width: 480,
366
+ maxHeight: '70vh',
367
+ background: '#0a0a0a',
368
+ border: '1px solid #222',
369
+ borderRadius: 12,
370
+ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.5)',
371
+ fontFamily: 'system-ui, -apple-system, sans-serif',
372
+ fontSize: 13,
373
+ color: '#fff',
374
+ overflow: 'hidden',
375
+ },
376
+ }, [
377
+ // Header
378
+ React.createElement('div', {
379
+ key: 'header',
380
+ style: {
381
+ display: 'flex',
382
+ alignItems: 'center',
383
+ justifyContent: 'space-between',
384
+ padding: '12px 16px',
385
+ borderBottom: '1px solid #222',
386
+ background: '#111',
387
+ },
388
+ }, [
389
+ React.createElement('div', {
390
+ key: 'title',
391
+ style: { display: 'flex', alignItems: 'center', gap: 8 },
392
+ }, [
393
+ React.createElement('span', { key: 'icon' }, '⚡'),
394
+ React.createElement('span', { key: 'text', style: { fontWeight: 600 } }, 'FlexiReact DevTools'),
395
+ ]),
396
+ React.createElement('button', {
397
+ key: 'close',
398
+ onClick: () => setState(s => ({ ...s, expanded: false })),
399
+ style: {
400
+ background: 'none',
401
+ border: 'none',
402
+ color: '#666',
403
+ cursor: 'pointer',
404
+ fontSize: 18,
405
+ },
406
+ }, '×'),
407
+ ]),
408
+
409
+ // Tabs
410
+ React.createElement('div', {
411
+ key: 'tabs',
412
+ style: {
413
+ display: 'flex',
414
+ borderBottom: '1px solid #222',
415
+ background: '#0d0d0d',
416
+ overflowX: 'auto',
417
+ },
418
+ }, tabs.map(tab =>
419
+ React.createElement('button', {
420
+ key: tab.id,
421
+ onClick: () => setState(s => ({ ...s, activeTab: tab.id as any })),
422
+ style: {
423
+ padding: '10px 14px',
424
+ background: state.activeTab === tab.id ? '#1a1a1a' : 'transparent',
425
+ border: 'none',
426
+ borderBottom: state.activeTab === tab.id ? '2px solid #00FF9C' : '2px solid transparent',
427
+ color: state.activeTab === tab.id ? '#fff' : '#888',
428
+ cursor: 'pointer',
429
+ fontSize: 12,
430
+ whiteSpace: 'nowrap',
431
+ },
432
+ }, `${tab.label} (${tab.count})`)
433
+ )),
434
+
435
+ // Content
436
+ React.createElement('div', {
437
+ key: 'content',
438
+ style: {
439
+ padding: 16,
440
+ maxHeight: 'calc(70vh - 100px)',
441
+ overflowY: 'auto',
442
+ },
443
+ }, renderTabContent(state.activeTab, data)),
444
+ ]);
445
+ }
446
+
447
+ function renderTabContent(tab: string, data: ReturnType<typeof devtools.getState>): React.ReactElement {
448
+ switch (tab) {
449
+ case 'routes':
450
+ return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
451
+ data.routes.length === 0
452
+ ? React.createElement('div', { style: { color: '#666', textAlign: 'center', padding: 20 } }, 'No routes tracked yet')
453
+ : data.routes.map((route, i) =>
454
+ React.createElement('div', {
455
+ key: i,
456
+ style: {
457
+ padding: 12,
458
+ background: '#111',
459
+ borderRadius: 8,
460
+ border: '1px solid #222',
461
+ },
462
+ }, [
463
+ React.createElement('div', { key: 'path', style: { fontWeight: 600, color: '#00FF9C' } }, route.path),
464
+ React.createElement('div', { key: 'component', style: { fontSize: 11, color: '#888', marginTop: 4 } },
465
+ `Component: ${route.component} • ${route.loadTime}ms`
466
+ ),
467
+ ])
468
+ )
469
+ );
470
+
471
+ case 'components':
472
+ return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
473
+ data.components.length === 0
474
+ ? React.createElement('div', { style: { color: '#666', textAlign: 'center', padding: 20 } }, 'No components tracked')
475
+ : data.components.map((comp, i) =>
476
+ React.createElement('div', {
477
+ key: i,
478
+ style: {
479
+ padding: 12,
480
+ background: '#111',
481
+ borderRadius: 8,
482
+ border: '1px solid #222',
483
+ },
484
+ }, [
485
+ React.createElement('div', {
486
+ key: 'name',
487
+ style: { display: 'flex', alignItems: 'center', gap: 8 }
488
+ }, [
489
+ React.createElement('span', { key: 'text', style: { fontWeight: 600 } }, comp.name),
490
+ comp.isIsland && React.createElement('span', {
491
+ key: 'island',
492
+ style: {
493
+ fontSize: 10,
494
+ padding: '2px 6px',
495
+ background: '#00FF9C20',
496
+ color: '#00FF9C',
497
+ borderRadius: 4,
498
+ },
499
+ }, 'Island'),
500
+ ]),
501
+ React.createElement('div', { key: 'info', style: { fontSize: 11, color: '#888', marginTop: 4 } },
502
+ `Renders: ${comp.renderCount} • Last: ${new Date(comp.lastRenderTime).toLocaleTimeString()}`
503
+ ),
504
+ ])
505
+ )
506
+ );
507
+
508
+ case 'network':
509
+ return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
510
+ data.network.length === 0
511
+ ? React.createElement('div', { style: { color: '#666', textAlign: 'center', padding: 20 } }, 'No requests yet')
512
+ : data.network.map((req, i) =>
513
+ React.createElement('div', {
514
+ key: i,
515
+ style: {
516
+ padding: 12,
517
+ background: '#111',
518
+ borderRadius: 8,
519
+ border: '1px solid #222',
520
+ },
521
+ }, [
522
+ React.createElement('div', {
523
+ key: 'url',
524
+ style: { display: 'flex', alignItems: 'center', gap: 8 }
525
+ }, [
526
+ React.createElement('span', {
527
+ key: 'method',
528
+ style: {
529
+ fontSize: 10,
530
+ padding: '2px 6px',
531
+ background: req.method === 'GET' ? '#3B82F620' : '#F59E0B20',
532
+ color: req.method === 'GET' ? '#3B82F6' : '#F59E0B',
533
+ borderRadius: 4,
534
+ fontWeight: 600,
535
+ },
536
+ }, req.method),
537
+ React.createElement('span', {
538
+ key: 'status',
539
+ style: {
540
+ fontSize: 10,
541
+ padding: '2px 6px',
542
+ background: req.status >= 200 && req.status < 300 ? '#10B98120' : '#EF444420',
543
+ color: req.status >= 200 && req.status < 300 ? '#10B981' : '#EF4444',
544
+ borderRadius: 4,
545
+ },
546
+ }, req.status || 'ERR'),
547
+ React.createElement('span', {
548
+ key: 'path',
549
+ style: { fontSize: 12, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis' }
550
+ }, new URL(req.url, 'http://localhost').pathname),
551
+ ]),
552
+ React.createElement('div', { key: 'info', style: { fontSize: 11, color: '#888', marginTop: 4 } },
553
+ `${req.duration}ms • ${formatBytes(req.size)}`
554
+ ),
555
+ ])
556
+ )
557
+ );
558
+
559
+ case 'performance':
560
+ return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
561
+ data.performance.length === 0
562
+ ? React.createElement('div', { style: { color: '#666', textAlign: 'center', padding: 20 } }, 'Collecting metrics...')
563
+ : data.performance.map((metric, i) =>
564
+ React.createElement('div', {
565
+ key: i,
566
+ style: {
567
+ padding: 12,
568
+ background: '#111',
569
+ borderRadius: 8,
570
+ border: '1px solid #222',
571
+ display: 'flex',
572
+ justifyContent: 'space-between',
573
+ alignItems: 'center',
574
+ },
575
+ }, [
576
+ React.createElement('span', { key: 'name', style: { fontWeight: 600 } }, metric.name),
577
+ React.createElement('div', { key: 'value', style: { display: 'flex', alignItems: 'center', gap: 8 } }, [
578
+ React.createElement('span', { key: 'num' },
579
+ metric.name === 'CLS' ? metric.value.toFixed(3) : `${Math.round(metric.value)}ms`
580
+ ),
581
+ React.createElement('span', {
582
+ key: 'rating',
583
+ style: {
584
+ width: 8,
585
+ height: 8,
586
+ borderRadius: '50%',
587
+ background: metric.rating === 'good' ? '#10B981' :
588
+ metric.rating === 'needs-improvement' ? '#F59E0B' : '#EF4444',
589
+ },
590
+ }),
591
+ ]),
592
+ ])
593
+ )
594
+ );
595
+
596
+ case 'console':
597
+ return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
598
+ data.logs.length === 0
599
+ ? React.createElement('div', { style: { color: '#666', textAlign: 'center', padding: 20 } }, 'No logs yet')
600
+ : data.logs.map((log, i) =>
601
+ React.createElement('div', {
602
+ key: i,
603
+ style: {
604
+ padding: '8px 12px',
605
+ background: log.level === 'error' ? '#EF444410' :
606
+ log.level === 'warn' ? '#F59E0B10' : '#111',
607
+ borderRadius: 6,
608
+ fontSize: 12,
609
+ fontFamily: 'monospace',
610
+ color: log.level === 'error' ? '#EF4444' :
611
+ log.level === 'warn' ? '#F59E0B' : '#888',
612
+ },
613
+ }, [
614
+ React.createElement('span', { key: 'time', style: { color: '#444', marginRight: 8 } },
615
+ new Date(log.timestamp).toLocaleTimeString()
616
+ ),
617
+ log.message,
618
+ ])
619
+ )
620
+ );
621
+
622
+ default:
623
+ return React.createElement('div', {}, 'Unknown tab');
624
+ }
625
+ }
626
+
627
+ function formatBytes(bytes: number): string {
628
+ if (bytes === 0) return '0 B';
629
+ const k = 1024;
630
+ const sizes = ['B', 'KB', 'MB'];
631
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
632
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
633
+ }
634
+
635
+ // ============================================================================
636
+ // Exports
637
+ // ============================================================================
638
+
639
+ export default {
640
+ devtools,
641
+ DevToolsOverlay,
642
+ initPerformanceMonitoring,
643
+ initNetworkInterceptor,
644
+ };
package/core/index.ts CHANGED
@@ -200,8 +200,16 @@ export {
200
200
  } from './helpers.js';
201
201
  export type { CookieOptions } from './helpers.js';
202
202
 
203
+ // DevTools
204
+ export {
205
+ devtools,
206
+ DevToolsOverlay,
207
+ initPerformanceMonitoring,
208
+ initNetworkInterceptor
209
+ } from './devtools/index.js';
210
+
203
211
  // Version
204
- export const VERSION = '2.4.0';
212
+ export const VERSION = '2.5.0';
205
213
 
206
214
  // Default export
207
215
  export default {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flexireact/core",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "The Modern React Framework v2 - SSR, SSG, Islands, App Router, TypeScript, Tailwind",
5
5
  "main": "core/index.ts",
6
6
  "types": "core/types.ts",