@flexireact/core 2.3.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,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
+ };