@flexireact/core 2.4.0 → 3.0.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/generators.ts +616 -0
- package/cli/index.ts +27 -8
- package/core/devtools/index.ts +644 -0
- package/core/index.ts +9 -1
- package/package.json +1 -1
|
@@ -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 = '
|
|
19
|
+
const VERSION = '3.0.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>')}
|
|
1094
|
-
console.log(` ${pc.cyan('dev')}
|
|
1095
|
-
console.log(` ${pc.cyan('build')}
|
|
1096
|
-
console.log(` ${pc.cyan('start')}
|
|
1097
|
-
console.log(` ${pc.cyan('
|
|
1098
|
-
console.log(` ${pc.cyan('
|
|
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
|
|
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 = '
|
|
212
|
+
export const VERSION = '3.0.0';
|
|
205
213
|
|
|
206
214
|
// Default export
|
|
207
215
|
export default {
|