@flexireact/core 3.0.1 → 3.0.2

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.
Files changed (55) hide show
  1. package/dist/cli/index.js +1514 -0
  2. package/dist/cli/index.js.map +1 -0
  3. package/dist/core/client/index.js +373 -0
  4. package/dist/core/client/index.js.map +1 -0
  5. package/dist/core/index.js +6415 -0
  6. package/dist/core/index.js.map +1 -0
  7. package/dist/core/server/index.js +3094 -0
  8. package/dist/core/server/index.js.map +1 -0
  9. package/package.json +80 -80
  10. package/bin/flexireact.js +0 -23
  11. package/cli/generators.ts +0 -616
  12. package/cli/index.ts +0 -1182
  13. package/core/actions/index.ts +0 -364
  14. package/core/api.ts +0 -143
  15. package/core/build/index.ts +0 -425
  16. package/core/cli/logger.ts +0 -353
  17. package/core/client/Link.tsx +0 -345
  18. package/core/client/hydration.ts +0 -147
  19. package/core/client/index.ts +0 -12
  20. package/core/client/islands.ts +0 -143
  21. package/core/client/navigation.ts +0 -212
  22. package/core/client/runtime.ts +0 -52
  23. package/core/config.ts +0 -116
  24. package/core/context.ts +0 -83
  25. package/core/dev.ts +0 -47
  26. package/core/devtools/index.ts +0 -644
  27. package/core/edge/cache.ts +0 -344
  28. package/core/edge/fetch-polyfill.ts +0 -247
  29. package/core/edge/handler.ts +0 -248
  30. package/core/edge/index.ts +0 -81
  31. package/core/edge/ppr.ts +0 -264
  32. package/core/edge/runtime.ts +0 -161
  33. package/core/font/index.ts +0 -306
  34. package/core/helpers.ts +0 -494
  35. package/core/image/index.ts +0 -413
  36. package/core/index.ts +0 -218
  37. package/core/islands/index.ts +0 -293
  38. package/core/loader.ts +0 -111
  39. package/core/logger.ts +0 -242
  40. package/core/metadata/index.ts +0 -622
  41. package/core/middleware/index.ts +0 -416
  42. package/core/plugins/index.ts +0 -373
  43. package/core/render/index.ts +0 -1243
  44. package/core/render.ts +0 -136
  45. package/core/router/index.ts +0 -551
  46. package/core/router.ts +0 -141
  47. package/core/rsc/index.ts +0 -199
  48. package/core/server/index.ts +0 -779
  49. package/core/server.ts +0 -203
  50. package/core/ssg/index.ts +0 -346
  51. package/core/start-dev.ts +0 -6
  52. package/core/start-prod.ts +0 -6
  53. package/core/tsconfig.json +0 -30
  54. package/core/types.ts +0 -239
  55. package/core/utils.ts +0 -176
@@ -1,1243 +0,0 @@
1
- /**
2
- * FlexiReact Render System v2
3
- * SSR with layouts, loading states, error boundaries, and islands
4
- */
5
-
6
- import React from 'react';
7
- import { renderToString, renderToPipeableStream } from 'react-dom/server';
8
- import { escapeHtml } from '../utils.js';
9
-
10
- /**
11
- * Renders a page with all its layouts and wrappers
12
- */
13
- export async function renderPage(options) {
14
- const {
15
- Component,
16
- props = {},
17
- layouts = [],
18
- loading = null,
19
- error = null,
20
- islands = [],
21
- title = 'FlexiReact App',
22
- meta = {},
23
- scripts = [],
24
- styles = [],
25
- favicon = null,
26
- isSSG = false,
27
- route = '/',
28
- needsHydration = false
29
- } = options;
30
-
31
- const renderStart = Date.now();
32
-
33
- try {
34
- // Build the component tree - start with the page component
35
- let element: any = React.createElement(Component, props);
36
-
37
- // Wrap with error boundary if error component exists
38
- if (error) {
39
- element = React.createElement(ErrorBoundaryWrapper as any, {
40
- fallback: error,
41
- children: element
42
- });
43
- }
44
-
45
- // Wrap with Suspense if loading component exists (for streaming/async)
46
- if (loading) {
47
- element = React.createElement(React.Suspense as any, {
48
- fallback: React.createElement(loading),
49
- children: element
50
- });
51
- }
52
-
53
- // Wrap with layouts (innermost to outermost)
54
- // Each layout receives children as a prop
55
- for (const layout of [...layouts].reverse()) {
56
- if (layout.Component) {
57
- const LayoutComponent = layout.Component;
58
- element = React.createElement(LayoutComponent, {
59
- ...layout.props
60
- }, element);
61
- }
62
- }
63
-
64
- // Render to string
65
- const content = renderToString(element);
66
-
67
- // Calculate render time
68
- const renderTime = Date.now() - renderStart;
69
-
70
- // Generate island hydration scripts
71
- const islandScripts = generateIslandScripts(islands);
72
-
73
- // Build full HTML document
74
- return buildHtmlDocument({
75
- content,
76
- title,
77
- meta,
78
- scripts: [...scripts, ...islandScripts],
79
- styles,
80
- favicon,
81
- props,
82
- isSSG,
83
- renderTime,
84
- route,
85
- isClientComponent: needsHydration
86
- });
87
-
88
- } catch (err) {
89
- console.error('Render Error:', err);
90
- throw err;
91
- }
92
- }
93
-
94
- /**
95
- * Streaming SSR with React 18
96
- * Renders the page progressively, sending HTML chunks as they become ready
97
- */
98
- export async function renderPageStream(options: {
99
- Component: React.ComponentType<any>;
100
- props?: Record<string, any>;
101
- layouts?: Array<{ Component: React.ComponentType<any>; props?: Record<string, any> }>;
102
- loading?: React.ComponentType | null;
103
- error?: React.ComponentType<{ error: Error }> | null;
104
- title?: string;
105
- meta?: Record<string, string>;
106
- scripts?: Array<string | { src?: string; content?: string; type?: string }>;
107
- styles?: Array<string | { content: string }>;
108
- favicon?: string | null;
109
- route?: string;
110
- onShellReady?: () => void;
111
- onAllReady?: () => void;
112
- onError?: (error: Error) => void;
113
- }): Promise<{ stream: NodeJS.ReadableStream; shellReady: Promise<void> }> {
114
- const {
115
- Component,
116
- props = {},
117
- layouts = [],
118
- loading = null,
119
- error = null,
120
- title = 'FlexiReact App',
121
- meta = {},
122
- scripts = [],
123
- styles = [],
124
- favicon = null,
125
- route = '/',
126
- onShellReady,
127
- onAllReady,
128
- onError
129
- } = options;
130
-
131
- const renderStart = Date.now();
132
-
133
- // Build the component tree
134
- let element: any = React.createElement(Component, props);
135
-
136
- // Wrap with error boundary if error component exists
137
- if (error) {
138
- element = React.createElement(ErrorBoundaryWrapper as any, {
139
- fallback: error,
140
- children: element
141
- });
142
- }
143
-
144
- // Wrap with Suspense if loading component exists
145
- if (loading) {
146
- element = React.createElement(React.Suspense as any, {
147
- fallback: React.createElement(loading),
148
- children: element
149
- });
150
- }
151
-
152
- // Wrap with layouts
153
- for (const layout of [...layouts].reverse()) {
154
- if (layout.Component) {
155
- element = React.createElement(layout.Component, layout.props, element);
156
- }
157
- }
158
-
159
- // Create the full document wrapper
160
- const DocumentWrapper = ({ children }: { children: React.ReactNode }) => {
161
- return React.createElement('html', { lang: 'en', className: 'dark' },
162
- React.createElement('head', null,
163
- React.createElement('meta', { charSet: 'UTF-8' }),
164
- React.createElement('meta', { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }),
165
- React.createElement('title', null, title),
166
- favicon && React.createElement('link', { rel: 'icon', href: favicon }),
167
- ...Object.entries(meta).map(([name, content]) =>
168
- React.createElement('meta', { key: name, name, content })
169
- ),
170
- ...styles.map((style, i) =>
171
- typeof style === 'string'
172
- ? React.createElement('link', { key: i, rel: 'stylesheet', href: style })
173
- : React.createElement('style', { key: i, dangerouslySetInnerHTML: { __html: style.content } })
174
- )
175
- ),
176
- React.createElement('body', null,
177
- React.createElement('div', { id: 'root' }, children),
178
- ...scripts.map((script, i) =>
179
- typeof script === 'string'
180
- ? React.createElement('script', { key: i, src: script })
181
- : script.src
182
- ? React.createElement('script', { key: i, src: script.src, type: script.type })
183
- : React.createElement('script', { key: i, type: script.type, dangerouslySetInnerHTML: { __html: script.content } })
184
- )
185
- )
186
- );
187
- };
188
-
189
- const fullElement = React.createElement(DocumentWrapper, null, element);
190
-
191
- // Create streaming render
192
- let shellReadyResolve: () => void;
193
- const shellReady = new Promise<void>((resolve) => {
194
- shellReadyResolve = resolve;
195
- });
196
-
197
- const { pipe, abort } = renderToPipeableStream(fullElement, {
198
- onShellReady() {
199
- const renderTime = Date.now() - renderStart;
200
- console.log(`⚡ Shell ready in ${renderTime}ms`);
201
- shellReadyResolve();
202
- onShellReady?.();
203
- },
204
- onAllReady() {
205
- const renderTime = Date.now() - renderStart;
206
- console.log(`✨ All content ready in ${renderTime}ms`);
207
- onAllReady?.();
208
- },
209
- onError(err: Error) {
210
- console.error('Streaming SSR Error:', err);
211
- onError?.(err);
212
- }
213
- });
214
-
215
- // Create a passthrough stream
216
- const { PassThrough } = await import('stream');
217
- const passThrough = new PassThrough();
218
-
219
- // Pipe the render stream to our passthrough
220
- pipe(passThrough);
221
-
222
- return {
223
- stream: passThrough,
224
- shellReady
225
- };
226
- }
227
-
228
- /**
229
- * Render to stream for HTTP response
230
- * Use this in the server to stream HTML to the client
231
- */
232
- export function streamToResponse(
233
- res: { write: (chunk: string) => void; end: () => void },
234
- stream: NodeJS.ReadableStream,
235
- options: { onFinish?: () => void } = {}
236
- ): void {
237
- stream.on('data', (chunk) => {
238
- res.write(chunk.toString());
239
- });
240
-
241
- stream.on('end', () => {
242
- res.end();
243
- options.onFinish?.();
244
- });
245
-
246
- stream.on('error', (err) => {
247
- console.error('Stream error:', err);
248
- res.end();
249
- });
250
- }
251
-
252
- /**
253
- * Error Boundary Wrapper for SSR
254
- */
255
- interface ErrorBoundaryProps {
256
- fallback: React.ComponentType<{ error: Error }>;
257
- children: React.ReactNode;
258
- }
259
-
260
- interface ErrorBoundaryState {
261
- hasError: boolean;
262
- error: Error | null;
263
- }
264
-
265
- class ErrorBoundaryWrapper extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
266
- constructor(props: ErrorBoundaryProps) {
267
- super(props);
268
- this.state = { hasError: false, error: null };
269
- }
270
-
271
- static getDerivedStateFromError(error: Error): ErrorBoundaryState {
272
- return { hasError: true, error };
273
- }
274
-
275
- render() {
276
- if (this.state.hasError) {
277
- const FallbackComponent = this.props.fallback;
278
- return React.createElement(FallbackComponent, { error: this.state.error! });
279
- }
280
- return this.props.children;
281
- }
282
- }
283
-
284
- /**
285
- * Generates hydration scripts for islands
286
- */
287
- function generateIslandScripts(islands) {
288
- if (!islands.length) return [];
289
-
290
- const scripts = [];
291
-
292
- for (const island of islands) {
293
- scripts.push({
294
- type: 'module',
295
- content: `
296
- import { hydrateIsland } from '/_flexi/client.js';
297
- import ${island.name} from '${island.clientPath}';
298
- hydrateIsland('${island.id}', ${island.name}, ${JSON.stringify(island.props)});
299
- `
300
- });
301
- }
302
-
303
- return scripts;
304
- }
305
-
306
- /**
307
- * Generates the Dev Toolbar HTML (FlexiReact v2 - Premium DevTools)
308
- */
309
- interface DevToolbarOptions {
310
- renderTime?: number;
311
- pageType?: string;
312
- route?: string;
313
- hasError?: boolean;
314
- isHydrated?: boolean;
315
- errorMessage?: string | null;
316
- componentName?: string | null;
317
- }
318
-
319
- function generateDevToolbar(options: DevToolbarOptions = {}) {
320
- const {
321
- renderTime = 0,
322
- pageType = 'SSR',
323
- route = '/',
324
- hasError = false,
325
- isHydrated = false,
326
- errorMessage = null,
327
- componentName = null
328
- } = options;
329
-
330
- const timeColor = renderTime < 50 ? '#00FF9C' : renderTime < 200 ? '#fbbf24' : '#ef4444';
331
- const timeLabel = renderTime < 50 ? 'Fast' : renderTime < 200 ? 'OK' : 'Slow';
332
-
333
- return `
334
- <!-- FlexiReact v2 Dev Toolbar -->
335
- <div id="flexi-dev-toolbar" class="flexi-dev-collapsed">
336
- <style>
337
- #flexi-dev-toolbar {
338
- position: fixed;
339
- bottom: 16px;
340
- left: 16px;
341
- z-index: 99999;
342
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
343
- font-size: 13px;
344
- }
345
-
346
- /* Main Button */
347
- .flexi-dev-trigger {
348
- display: flex;
349
- align-items: center;
350
- gap: 8px;
351
- padding: 8px 12px;
352
- background: rgba(10, 10, 10, 0.95);
353
- backdrop-filter: blur(20px);
354
- border: 1px solid rgba(0, 255, 156, 0.2);
355
- border-radius: 10px;
356
- color: #fafafa;
357
- cursor: pointer;
358
- transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
359
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
360
- }
361
-
362
- .flexi-dev-trigger:hover {
363
- border-color: rgba(0, 255, 156, 0.5);
364
- box-shadow: 0 4px 30px rgba(0, 0, 0, 0.6), 0 0 20px rgba(0, 255, 156, 0.15);
365
- transform: translateY(-2px);
366
- }
367
-
368
- .flexi-dev-trigger.has-error {
369
- border-color: rgba(239, 68, 68, 0.5);
370
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), 0 0 15px rgba(239, 68, 68, 0.2);
371
- }
372
-
373
- .flexi-dev-logo {
374
- width: 20px;
375
- height: 20px;
376
- background: linear-gradient(135deg, #00FF9C, #00D68F);
377
- border-radius: 5px;
378
- display: flex;
379
- align-items: center;
380
- justify-content: center;
381
- font-weight: 800;
382
- font-size: 11px;
383
- color: #000;
384
- }
385
-
386
- .flexi-dev-trigger.has-error .flexi-dev-logo {
387
- background: linear-gradient(135deg, #ef4444, #dc2626);
388
- }
389
-
390
- .flexi-dev-indicator {
391
- display: flex;
392
- align-items: center;
393
- gap: 6px;
394
- }
395
-
396
- .flexi-dev-dot {
397
- width: 6px;
398
- height: 6px;
399
- border-radius: 50%;
400
- background: #00FF9C;
401
- box-shadow: 0 0 8px rgba(0, 255, 156, 0.6);
402
- }
403
-
404
- .flexi-dev-dot.error {
405
- background: #ef4444;
406
- box-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
407
- animation: errorPulse 1s infinite;
408
- }
409
-
410
- @keyframes errorPulse {
411
- 0%, 100% { opacity: 1; }
412
- 50% { opacity: 0.4; }
413
- }
414
-
415
- .flexi-dev-time {
416
- font-size: 11px;
417
- font-weight: 600;
418
- color: ${timeColor};
419
- font-variant-numeric: tabular-nums;
420
- }
421
-
422
- /* Panel */
423
- .flexi-dev-panel {
424
- position: absolute;
425
- bottom: calc(100% + 8px);
426
- left: 0;
427
- min-width: 340px;
428
- background: rgba(10, 10, 10, 0.98);
429
- backdrop-filter: blur(20px);
430
- border: 1px solid rgba(255, 255, 255, 0.08);
431
- border-radius: 16px;
432
- opacity: 0;
433
- visibility: hidden;
434
- transform: translateY(8px) scale(0.96);
435
- transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
436
- box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7);
437
- overflow: hidden;
438
- }
439
-
440
- #flexi-dev-toolbar.flexi-dev-open .flexi-dev-panel {
441
- opacity: 1;
442
- visibility: visible;
443
- transform: translateY(0) scale(1);
444
- }
445
-
446
- /* Header */
447
- .flexi-dev-header {
448
- display: flex;
449
- align-items: center;
450
- gap: 10px;
451
- padding: 14px 16px;
452
- background: linear-gradient(135deg, rgba(0, 255, 156, 0.08), rgba(0, 214, 143, 0.04));
453
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
454
- }
455
-
456
- .flexi-dev-header-logo {
457
- width: 26px;
458
- height: 26px;
459
- background: linear-gradient(135deg, #00FF9C, #00D68F);
460
- border-radius: 7px;
461
- display: flex;
462
- align-items: center;
463
- justify-content: center;
464
- font-weight: 800;
465
- font-size: 13px;
466
- color: #000;
467
- }
468
-
469
- .flexi-dev-header-info {
470
- flex: 1;
471
- }
472
-
473
- .flexi-dev-header-title {
474
- font-weight: 700;
475
- font-size: 14px;
476
- color: #fafafa;
477
- }
478
-
479
- .flexi-dev-header-subtitle {
480
- font-size: 11px;
481
- color: #52525b;
482
- margin-top: 1px;
483
- }
484
-
485
- .flexi-dev-close {
486
- width: 24px;
487
- height: 24px;
488
- display: flex;
489
- align-items: center;
490
- justify-content: center;
491
- background: rgba(255, 255, 255, 0.05);
492
- border: none;
493
- border-radius: 6px;
494
- color: #71717a;
495
- cursor: pointer;
496
- transition: all 0.15s;
497
- }
498
-
499
- .flexi-dev-close:hover {
500
- background: rgba(255, 255, 255, 0.1);
501
- color: #fafafa;
502
- }
503
-
504
- /* Content */
505
- .flexi-dev-content {
506
- padding: 12px 16px;
507
- }
508
-
509
- .flexi-dev-section {
510
- margin-bottom: 12px;
511
- }
512
-
513
- .flexi-dev-section:last-child {
514
- margin-bottom: 0;
515
- }
516
-
517
- .flexi-dev-section-title {
518
- font-size: 10px;
519
- font-weight: 600;
520
- color: #52525b;
521
- text-transform: uppercase;
522
- letter-spacing: 0.5px;
523
- margin-bottom: 8px;
524
- }
525
-
526
- .flexi-dev-grid {
527
- display: grid;
528
- grid-template-columns: 1fr 1fr;
529
- gap: 8px;
530
- }
531
-
532
- .flexi-dev-stat {
533
- background: rgba(255, 255, 255, 0.03);
534
- border: 1px solid rgba(255, 255, 255, 0.05);
535
- border-radius: 10px;
536
- padding: 10px 12px;
537
- }
538
-
539
- .flexi-dev-stat-label {
540
- font-size: 10px;
541
- color: #52525b;
542
- margin-bottom: 4px;
543
- }
544
-
545
- .flexi-dev-stat-value {
546
- font-size: 14px;
547
- font-weight: 600;
548
- color: #fafafa;
549
- }
550
-
551
- .flexi-dev-stat-value.success { color: #00FF9C; }
552
- .flexi-dev-stat-value.warning { color: #fbbf24; }
553
- .flexi-dev-stat-value.error { color: #ef4444; }
554
-
555
- /* Badges */
556
- .flexi-dev-badge {
557
- display: inline-flex;
558
- align-items: center;
559
- padding: 3px 8px;
560
- border-radius: 5px;
561
- font-size: 10px;
562
- font-weight: 700;
563
- letter-spacing: 0.3px;
564
- }
565
-
566
- .flexi-dev-badge.ssr { background: rgba(251, 191, 36, 0.15); color: #fbbf24; }
567
- .flexi-dev-badge.ssg { background: rgba(0, 255, 156, 0.15); color: #00FF9C; }
568
- .flexi-dev-badge.csr { background: rgba(6, 182, 212, 0.15); color: #06b6d4; }
569
- .flexi-dev-badge.isr { background: rgba(139, 92, 246, 0.15); color: #a78bfa; }
570
-
571
- /* Error Display */
572
- .flexi-dev-error {
573
- background: rgba(239, 68, 68, 0.1);
574
- border: 1px solid rgba(239, 68, 68, 0.2);
575
- border-radius: 10px;
576
- padding: 12px;
577
- margin-top: 8px;
578
- }
579
-
580
- .flexi-dev-error-title {
581
- display: flex;
582
- align-items: center;
583
- gap: 6px;
584
- font-size: 11px;
585
- font-weight: 600;
586
- color: #ef4444;
587
- margin-bottom: 6px;
588
- }
589
-
590
- .flexi-dev-error-message {
591
- font-size: 12px;
592
- color: #fca5a5;
593
- font-family: 'SF Mono', 'Fira Code', monospace;
594
- word-break: break-word;
595
- line-height: 1.5;
596
- }
597
-
598
- /* Footer */
599
- .flexi-dev-footer {
600
- display: flex;
601
- gap: 6px;
602
- padding: 12px 16px;
603
- background: rgba(0, 0, 0, 0.3);
604
- border-top: 1px solid rgba(255, 255, 255, 0.05);
605
- }
606
-
607
- .flexi-dev-action {
608
- flex: 1;
609
- padding: 8px;
610
- background: rgba(255, 255, 255, 0.03);
611
- border: 1px solid rgba(255, 255, 255, 0.06);
612
- border-radius: 8px;
613
- color: #71717a;
614
- font-size: 11px;
615
- font-weight: 500;
616
- text-decoration: none;
617
- text-align: center;
618
- cursor: pointer;
619
- transition: all 0.15s;
620
- }
621
-
622
- .flexi-dev-action:hover {
623
- background: rgba(0, 255, 156, 0.1);
624
- border-color: rgba(0, 255, 156, 0.2);
625
- color: #00FF9C;
626
- }
627
- </style>
628
-
629
- <button class="flexi-dev-trigger ${hasError ? 'has-error' : ''}" onclick="this.parentElement.classList.toggle('flexi-dev-open')">
630
- <div class="flexi-dev-logo">F</div>
631
- <div class="flexi-dev-indicator">
632
- <div class="flexi-dev-dot ${hasError ? 'error' : ''}"></div>
633
- <span class="flexi-dev-time">${renderTime}ms</span>
634
- </div>
635
- </button>
636
-
637
- <div class="flexi-dev-panel">
638
- <div class="flexi-dev-header">
639
- <div class="flexi-dev-header-logo">F</div>
640
- <div class="flexi-dev-header-info">
641
- <div class="flexi-dev-header-title">FlexiReact</div>
642
- <div class="flexi-dev-header-subtitle">v2.0.0 • Development</div>
643
- </div>
644
- <button class="flexi-dev-close" onclick="this.closest('#flexi-dev-toolbar').classList.remove('flexi-dev-open')">✕</button>
645
- </div>
646
-
647
- <div class="flexi-dev-content">
648
- <div class="flexi-dev-section">
649
- <div class="flexi-dev-section-title">Page Info</div>
650
- <div class="flexi-dev-grid">
651
- <div class="flexi-dev-stat">
652
- <div class="flexi-dev-stat-label">Route</div>
653
- <div class="flexi-dev-stat-value">${route}</div>
654
- </div>
655
- <div class="flexi-dev-stat">
656
- <div class="flexi-dev-stat-label">Type</div>
657
- <div class="flexi-dev-stat-value"><span class="flexi-dev-badge ${pageType.toLowerCase()}">${pageType}</span></div>
658
- </div>
659
- </div>
660
- </div>
661
-
662
- <div class="flexi-dev-section">
663
- <div class="flexi-dev-section-title">Performance</div>
664
- <div class="flexi-dev-grid">
665
- <div class="flexi-dev-stat">
666
- <div class="flexi-dev-stat-label">Render Time</div>
667
- <div class="flexi-dev-stat-value ${renderTime < 50 ? 'success' : renderTime < 200 ? 'warning' : 'error'}">${renderTime}ms <small style="color:#52525b">${timeLabel}</small></div>
668
- </div>
669
- <div class="flexi-dev-stat">
670
- <div class="flexi-dev-stat-label">Hydration</div>
671
- <div class="flexi-dev-stat-value" id="flexi-hydration-status">${isHydrated ? '✓ Client' : '○ Server'}</div>
672
- </div>
673
- </div>
674
- </div>
675
-
676
- ${hasError && errorMessage ? `
677
- <div class="flexi-dev-error">
678
- <div class="flexi-dev-error-title">
679
- <span>⚠</span> Runtime Error
680
- </div>
681
- <div class="flexi-dev-error-message">${errorMessage}</div>
682
- </div>
683
- ` : ''}
684
- </div>
685
-
686
- <div class="flexi-dev-footer">
687
- <a href="/_flexi/routes" class="flexi-dev-action">Routes</a>
688
- <button class="flexi-dev-action" onclick="location.reload()">Refresh</button>
689
- <a href="https://github.com/flexireact/flexireact" target="_blank" class="flexi-dev-action">Docs ↗</a>
690
- </div>
691
- </div>
692
- </div>
693
-
694
- <script>
695
- // FlexiReact v2 DevTools
696
- window.__FLEXI_DEV__ = {
697
- version: '2.0.0',
698
- renderTime: ${renderTime},
699
- pageType: '${pageType}',
700
- route: '${route}',
701
- hydrated: false,
702
- errors: []
703
- };
704
-
705
- // Track hydration
706
- const root = document.getElementById('root');
707
- if (root) {
708
- const observer = new MutationObserver(() => {
709
- if (root.children.length > 0) {
710
- window.__FLEXI_DEV__.hydrated = true;
711
- const el = document.getElementById('flexi-hydration-status');
712
- if (el) el.textContent = '✓ Client';
713
- observer.disconnect();
714
- }
715
- });
716
- observer.observe(root, { childList: true, subtree: true });
717
- }
718
-
719
- // Capture runtime errors
720
- window.addEventListener('error', (e) => {
721
- window.__FLEXI_DEV__.errors.push({
722
- message: e.message,
723
- file: e.filename,
724
- line: e.lineno,
725
- col: e.colno
726
- });
727
- console.error('%c[FlexiReact Error]', 'color: #ef4444; font-weight: bold;', e.message);
728
- });
729
-
730
- // Console branding
731
- console.log(
732
- '%c ⚡ FlexiReact v2 %c ${pageType} %c ${renderTime}ms ',
733
- 'background: #00FF9C; color: #000; font-weight: bold; padding: 2px 6px; border-radius: 4px 0 0 4px;',
734
- 'background: #1e1e1e; color: #fafafa; padding: 2px 6px;',
735
- 'background: ${timeColor}20; color: ${timeColor}; padding: 2px 6px; border-radius: 0 4px 4px 0;'
736
- );
737
- </script>
738
- `;
739
- }
740
-
741
- /**
742
- * Builds complete HTML document
743
- */
744
- function buildHtmlDocument(options) {
745
- const {
746
- content,
747
- title,
748
- meta = {},
749
- scripts = [],
750
- styles = [],
751
- props = {},
752
- isSSG = false,
753
- renderTime = 0,
754
- route = '/',
755
- isClientComponent = false,
756
- favicon = null
757
- } = options;
758
-
759
- const metaTags = Object.entries(meta)
760
- .map(([name, content]) => {
761
- if (name.startsWith('og:')) {
762
- return `<meta property="${escapeHtml(name)}" content="${escapeHtml(content)}">`;
763
- }
764
- return `<meta name="${escapeHtml(name)}" content="${escapeHtml(content)}">`;
765
- })
766
- .join('\n ');
767
-
768
- const styleTags = styles
769
- .map(style => {
770
- if (typeof style === 'string') {
771
- return `<link rel="stylesheet" href="${escapeHtml(style)}">`;
772
- }
773
- return `<style>${style.content}</style>`;
774
- })
775
- .join('\n ');
776
-
777
- const scriptTags = scripts
778
- .map(script => {
779
- if (typeof script === 'string') {
780
- return `<script src="${escapeHtml(script)}"></script>`;
781
- }
782
- const type = script.type ? ` type="${script.type}"` : '';
783
- if (script.src) {
784
- return `<script${type} src="${escapeHtml(script.src)}"></script>`;
785
- }
786
- return `<script${type}>${script.content}</script>`;
787
- })
788
- .join('\n ');
789
-
790
- // FlexiReact SVG favicon (properly encoded)
791
- const faviconSvg = encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#00FF9C"/><text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" fill="#000" font-family="system-ui" font-weight="bold" font-size="16">F</text></svg>`);
792
-
793
- // Generate Dev Toolbar for development mode
794
- const isDev = process.env.NODE_ENV !== 'production';
795
- const pageType = isSSG ? 'SSG' : isClientComponent ? 'CSR' : 'SSR';
796
- const devToolbar = isDev ? generateDevToolbar({
797
- renderTime,
798
- pageType,
799
- route,
800
- isHydrated: isClientComponent
801
- }) : '';
802
-
803
- // Determine favicon link
804
- const faviconLink = favicon
805
- ? `<link rel="icon" href="${escapeHtml(favicon)}">`
806
- : `<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,${faviconSvg}">`;
807
-
808
- return `<!DOCTYPE html>
809
- <html lang="en" class="dark">
810
- <head>
811
- <meta charset="UTF-8">
812
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
813
- <title>${escapeHtml(title)}</title>
814
- ${faviconLink}
815
- ${metaTags}
816
- <style>
817
- :root { --flexi-bg: #0f172a; --flexi-fg: #f8fafc; }
818
- html, body { background-color: #0f172a; color: #f8fafc; min-height: 100vh; margin: 0; }
819
- </style>
820
- ${styleTags}
821
- <script>
822
- (function() {
823
- var theme = localStorage.getItem('theme');
824
- if (theme === 'light') {
825
- document.documentElement.classList.remove('dark');
826
- document.documentElement.style.backgroundColor = '#ffffff';
827
- document.documentElement.style.color = '#0f172a';
828
- document.body.style.backgroundColor = '#ffffff';
829
- document.body.style.color = '#0f172a';
830
- }
831
- })();
832
- </script>
833
- </head>
834
- <body>
835
- <div id="root">${content}</div>
836
- <script>
837
- window.__FLEXI_DATA__ = ${JSON.stringify({ props, isSSG })};
838
- </script>
839
- ${scriptTags}
840
- ${devToolbar}
841
- </body>
842
- </html>`;
843
- }
844
-
845
- /**
846
- * Renders an error page with beautiful styling (FlexiReact v2)
847
- */
848
- export function renderError(statusCode, message, stack = null) {
849
- const showStack = process.env.NODE_ENV !== 'production' && stack;
850
- const isDev = process.env.NODE_ENV !== 'production';
851
-
852
- // Parse error for better display
853
- const errorDetails = parseErrorStack(stack);
854
-
855
- // Different messages for different status codes
856
- const errorMessages = {
857
- 404: { title: 'Page Not Found', icon: 'search', color: '#00FF9C', desc: 'The page you\'re looking for doesn\'t exist or has been moved.' },
858
- 500: { title: 'Server Error', icon: 'alert', color: '#ef4444', desc: 'Something went wrong on our end.' },
859
- 403: { title: 'Forbidden', icon: 'lock', color: '#f59e0b', desc: 'You don\'t have permission to access this resource.' },
860
- 401: { title: 'Unauthorized', icon: 'key', color: '#8b5cf6', desc: 'Please log in to access this page.' },
861
- };
862
-
863
- const errorInfo = errorMessages[statusCode] || { title: 'Error', icon: 'alert', color: '#ef4444', desc: message };
864
-
865
- // Generate error frames HTML for dev mode
866
- const errorFramesHtml = showStack && errorDetails?.frames?.length > 0
867
- ? errorDetails.frames.slice(0, 5).map((frame, i) => `
868
- <div class="error-frame ${i === 0 ? 'error-frame-first' : ''}">
869
- <div class="error-frame-fn">${escapeHtml(frame.fn)}</div>
870
- <div class="error-frame-loc">${escapeHtml(frame.file)}:${frame.line}:${frame.col}</div>
871
- </div>
872
- `).join('')
873
- : '';
874
-
875
- return `<!DOCTYPE html>
876
- <html lang="en">
877
- <head>
878
- <meta charset="UTF-8">
879
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
880
- <title>${statusCode} - ${errorInfo.title} | FlexiReact</title>
881
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%2300FF9C'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='central' text-anchor='middle' fill='%23000' font-family='system-ui' font-weight='bold' font-size='16'%3EF%3C/text%3E%3C/svg%3E">
882
- <style>
883
- * { margin: 0; padding: 0; box-sizing: border-box; }
884
-
885
- body {
886
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
887
- min-height: 100vh;
888
- background: #0a0a0a;
889
- color: #fafafa;
890
- display: flex;
891
- align-items: center;
892
- justify-content: center;
893
- overflow-x: hidden;
894
- }
895
-
896
- /* Animated background */
897
- .bg-pattern {
898
- position: fixed;
899
- inset: 0;
900
- background-image:
901
- radial-gradient(circle at 25% 25%, rgba(0, 255, 156, 0.03) 0%, transparent 50%),
902
- radial-gradient(circle at 75% 75%, rgba(99, 102, 241, 0.03) 0%, transparent 50%);
903
- pointer-events: none;
904
- }
905
-
906
- .bg-grid {
907
- position: fixed;
908
- inset: 0;
909
- background-image:
910
- linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
911
- linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
912
- background-size: 64px 64px;
913
- pointer-events: none;
914
- }
915
-
916
- .container {
917
- position: relative;
918
- width: 100%;
919
- max-width: ${showStack ? '800px' : '500px'};
920
- padding: 2rem;
921
- animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1);
922
- }
923
-
924
- @keyframes fadeIn {
925
- from { opacity: 0; transform: translateY(30px); }
926
- to { opacity: 1; transform: translateY(0); }
927
- }
928
-
929
- /* Error Card */
930
- .error-card {
931
- background: linear-gradient(145deg, rgba(23, 23, 23, 0.9), rgba(10, 10, 10, 0.95));
932
- border: 1px solid rgba(255, 255, 255, 0.08);
933
- border-radius: 24px;
934
- padding: 3rem;
935
- text-align: center;
936
- backdrop-filter: blur(20px);
937
- box-shadow:
938
- 0 0 0 1px rgba(255, 255, 255, 0.05),
939
- 0 20px 50px -20px rgba(0, 0, 0, 0.5),
940
- 0 0 100px -50px ${errorInfo.color}40;
941
- }
942
-
943
- /* Logo */
944
- .logo {
945
- width: 56px;
946
- height: 56px;
947
- background: linear-gradient(135deg, #00FF9C, #00D68F);
948
- border-radius: 14px;
949
- display: flex;
950
- align-items: center;
951
- justify-content: center;
952
- margin: 0 auto 2rem;
953
- font-weight: 800;
954
- font-size: 24px;
955
- color: #000;
956
- box-shadow: 0 0 30px rgba(0, 255, 156, 0.3);
957
- }
958
-
959
- /* Error Code */
960
- .error-code {
961
- font-size: 7rem;
962
- font-weight: 800;
963
- line-height: 1;
964
- background: linear-gradient(135deg, ${errorInfo.color}, ${errorInfo.color}99);
965
- -webkit-background-clip: text;
966
- -webkit-text-fill-color: transparent;
967
- background-clip: text;
968
- margin-bottom: 0.5rem;
969
- letter-spacing: -4px;
970
- }
971
-
972
- .error-title {
973
- font-size: 1.5rem;
974
- font-weight: 600;
975
- color: #fafafa;
976
- margin-bottom: 0.75rem;
977
- }
978
-
979
- .error-desc {
980
- font-size: 1rem;
981
- color: #71717a;
982
- line-height: 1.6;
983
- margin-bottom: 2rem;
984
- }
985
-
986
- /* Buttons */
987
- .buttons {
988
- display: flex;
989
- gap: 12px;
990
- justify-content: center;
991
- flex-wrap: wrap;
992
- }
993
-
994
- .btn {
995
- display: inline-flex;
996
- align-items: center;
997
- gap: 8px;
998
- padding: 12px 24px;
999
- border-radius: 12px;
1000
- font-weight: 600;
1001
- font-size: 14px;
1002
- text-decoration: none;
1003
- transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
1004
- border: none;
1005
- cursor: pointer;
1006
- }
1007
-
1008
- .btn-primary {
1009
- background: #00FF9C;
1010
- color: #000;
1011
- box-shadow: 0 0 20px rgba(0, 255, 156, 0.3);
1012
- }
1013
-
1014
- .btn-primary:hover {
1015
- transform: translateY(-2px);
1016
- box-shadow: 0 0 30px rgba(0, 255, 156, 0.5);
1017
- }
1018
-
1019
- .btn-secondary {
1020
- background: rgba(255, 255, 255, 0.06);
1021
- color: #a1a1aa;
1022
- border: 1px solid rgba(255, 255, 255, 0.1);
1023
- }
1024
-
1025
- .btn-secondary:hover {
1026
- background: rgba(255, 255, 255, 0.1);
1027
- color: #fafafa;
1028
- border-color: rgba(255, 255, 255, 0.2);
1029
- }
1030
-
1031
- /* Error Stack (Dev Mode) */
1032
- .error-stack {
1033
- margin-top: 2rem;
1034
- text-align: left;
1035
- }
1036
-
1037
- .error-stack-header {
1038
- display: flex;
1039
- align-items: center;
1040
- gap: 8px;
1041
- margin-bottom: 12px;
1042
- font-size: 12px;
1043
- font-weight: 600;
1044
- color: #ef4444;
1045
- text-transform: uppercase;
1046
- letter-spacing: 0.5px;
1047
- }
1048
-
1049
- .error-stack-header::before {
1050
- content: '';
1051
- width: 8px;
1052
- height: 8px;
1053
- background: #ef4444;
1054
- border-radius: 50%;
1055
- animation: pulse 2s infinite;
1056
- }
1057
-
1058
- @keyframes pulse {
1059
- 0%, 100% { opacity: 1; }
1060
- 50% { opacity: 0.5; }
1061
- }
1062
-
1063
- .error-message {
1064
- background: rgba(239, 68, 68, 0.1);
1065
- border: 1px solid rgba(239, 68, 68, 0.2);
1066
- border-radius: 12px;
1067
- padding: 16px;
1068
- margin-bottom: 12px;
1069
- font-family: 'SF Mono', 'Fira Code', monospace;
1070
- font-size: 13px;
1071
- color: #fca5a5;
1072
- word-break: break-word;
1073
- }
1074
-
1075
- .error-frames {
1076
- background: rgba(0, 0, 0, 0.4);
1077
- border: 1px solid rgba(255, 255, 255, 0.05);
1078
- border-radius: 12px;
1079
- overflow: hidden;
1080
- }
1081
-
1082
- .error-frame {
1083
- padding: 12px 16px;
1084
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1085
- font-family: 'SF Mono', 'Fira Code', monospace;
1086
- font-size: 12px;
1087
- }
1088
-
1089
- .error-frame:last-child {
1090
- border-bottom: none;
1091
- }
1092
-
1093
- .error-frame-first {
1094
- background: rgba(239, 68, 68, 0.05);
1095
- }
1096
-
1097
- .error-frame-fn {
1098
- color: #fafafa;
1099
- font-weight: 500;
1100
- margin-bottom: 4px;
1101
- }
1102
-
1103
- .error-frame-loc {
1104
- color: #52525b;
1105
- font-size: 11px;
1106
- }
1107
-
1108
- /* Dev Badge */
1109
- .dev-badge {
1110
- position: fixed;
1111
- bottom: 20px;
1112
- right: 20px;
1113
- display: flex;
1114
- align-items: center;
1115
- gap: 8px;
1116
- padding: 8px 14px;
1117
- background: rgba(0, 0, 0, 0.8);
1118
- border: 1px solid rgba(255, 255, 255, 0.1);
1119
- border-radius: 10px;
1120
- font-size: 12px;
1121
- font-weight: 500;
1122
- color: #71717a;
1123
- backdrop-filter: blur(10px);
1124
- }
1125
-
1126
- .dev-badge-dot {
1127
- width: 8px;
1128
- height: 8px;
1129
- background: #00FF9C;
1130
- border-radius: 50%;
1131
- box-shadow: 0 0 10px rgba(0, 255, 156, 0.5);
1132
- }
1133
- </style>
1134
- </head>
1135
- <body>
1136
- <div class="bg-pattern"></div>
1137
- <div class="bg-grid"></div>
1138
-
1139
- <div class="container">
1140
- <div class="error-card">
1141
- <div class="logo">F</div>
1142
-
1143
- <div class="error-code">${statusCode}</div>
1144
- <h1 class="error-title">${errorInfo.title}</h1>
1145
- <p class="error-desc">${errorInfo.desc}</p>
1146
-
1147
- <div class="buttons">
1148
- <a href="/" class="btn btn-primary">
1149
- ← Back to Home
1150
- </a>
1151
- <a href="javascript:history.back()" class="btn btn-secondary">
1152
- Go Back
1153
- </a>
1154
- </div>
1155
-
1156
- ${showStack ? `
1157
- <div class="error-stack">
1158
- <div class="error-stack-header">Error Details</div>
1159
- <div class="error-message">${escapeHtml(message)}</div>
1160
- ${errorFramesHtml ? `<div class="error-frames">${errorFramesHtml}</div>` : ''}
1161
- </div>
1162
- ` : ''}
1163
- </div>
1164
- </div>
1165
-
1166
- ${isDev ? `
1167
- <div class="dev-badge">
1168
- <div class="dev-badge-dot"></div>
1169
- FlexiReact v2
1170
- </div>
1171
- ` : ''}
1172
- </body>
1173
- </html>`;
1174
- }
1175
-
1176
- /**
1177
- * Parses error stack for better display
1178
- */
1179
- function parseErrorStack(stack) {
1180
- if (!stack) return null;
1181
-
1182
- const lines = stack.split('\n');
1183
- const parsed = {
1184
- message: lines[0] || '',
1185
- frames: []
1186
- };
1187
-
1188
- for (let i = 1; i < lines.length; i++) {
1189
- const line = lines[i].trim();
1190
- const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/) ||
1191
- line.match(/at\s+(.+?):(\d+):(\d+)/);
1192
-
1193
- if (match) {
1194
- parsed.frames.push({
1195
- fn: match[1] || 'anonymous',
1196
- file: match[2] || match[1],
1197
- line: match[3] || match[2],
1198
- col: match[4] || match[3]
1199
- });
1200
- }
1201
- }
1202
-
1203
- return parsed;
1204
- }
1205
-
1206
- /**
1207
- * Renders a loading state
1208
- */
1209
- export function renderLoading(LoadingComponent) {
1210
- if (!LoadingComponent) {
1211
- return `<div class="flexi-loading">
1212
- <div class="flexi-spinner"></div>
1213
- <style>
1214
- .flexi-loading {
1215
- display: flex;
1216
- align-items: center;
1217
- justify-content: center;
1218
- min-height: 200px;
1219
- }
1220
- .flexi-spinner {
1221
- width: 40px;
1222
- height: 40px;
1223
- border: 3px solid #f3f3f3;
1224
- border-top: 3px solid #667eea;
1225
- border-radius: 50%;
1226
- animation: spin 1s linear infinite;
1227
- }
1228
- @keyframes spin {
1229
- 0% { transform: rotate(0deg); }
1230
- 100% { transform: rotate(360deg); }
1231
- }
1232
- </style>
1233
- </div>`;
1234
- }
1235
-
1236
- return renderToString(React.createElement(LoadingComponent));
1237
- }
1238
-
1239
- export default {
1240
- renderPage,
1241
- renderError,
1242
- renderLoading
1243
- };