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