@flexireact/core 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -116
- 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/{index.js → index.ts} +2 -1
- 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 +19 -14
- package/cli/index.js +0 -992
- package/core/render/index.js +0 -765
- 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/{config.js → config.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
package/core/render/index.js
DELETED
|
@@ -1,765 +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 = 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
|
-
class ErrorBoundaryWrapper extends React.Component {
|
|
82
|
-
constructor(props) {
|
|
83
|
-
super(props);
|
|
84
|
-
this.state = { hasError: false, error: null };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
static getDerivedStateFromError(error) {
|
|
88
|
-
return { hasError: true, error };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
render() {
|
|
92
|
-
if (this.state.hasError) {
|
|
93
|
-
const FallbackComponent = this.props.fallback;
|
|
94
|
-
return React.createElement(FallbackComponent, { error: this.state.error });
|
|
95
|
-
}
|
|
96
|
-
return this.props.children;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Generates hydration scripts for islands
|
|
102
|
-
*/
|
|
103
|
-
function generateIslandScripts(islands) {
|
|
104
|
-
if (!islands.length) return [];
|
|
105
|
-
|
|
106
|
-
const scripts = [];
|
|
107
|
-
|
|
108
|
-
for (const island of islands) {
|
|
109
|
-
scripts.push({
|
|
110
|
-
type: 'module',
|
|
111
|
-
content: `
|
|
112
|
-
import { hydrateIsland } from '/_flexi/client.js';
|
|
113
|
-
import ${island.name} from '${island.clientPath}';
|
|
114
|
-
hydrateIsland('${island.id}', ${island.name}, ${JSON.stringify(island.props)});
|
|
115
|
-
`
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return scripts;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Generates the Dev Toolbar HTML (Premium Next.js/Vercel style)
|
|
124
|
-
*/
|
|
125
|
-
function generateDevToolbar(options = {}) {
|
|
126
|
-
const {
|
|
127
|
-
renderTime = 0,
|
|
128
|
-
pageType = 'SSR',
|
|
129
|
-
route = '/',
|
|
130
|
-
hasError = false,
|
|
131
|
-
isHydrated = false
|
|
132
|
-
} = options;
|
|
133
|
-
|
|
134
|
-
return `
|
|
135
|
-
<!-- FlexiReact Dev Toolbar -->
|
|
136
|
-
<div id="flexi-dev-toolbar">
|
|
137
|
-
<style>
|
|
138
|
-
#flexi-dev-toolbar {
|
|
139
|
-
position: fixed;
|
|
140
|
-
bottom: 20px;
|
|
141
|
-
left: 20px;
|
|
142
|
-
z-index: 99999;
|
|
143
|
-
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
144
|
-
font-size: 13px;
|
|
145
|
-
}
|
|
146
|
-
.flexi-dev-btn {
|
|
147
|
-
display: flex;
|
|
148
|
-
align-items: center;
|
|
149
|
-
gap: 10px;
|
|
150
|
-
padding: 10px 16px;
|
|
151
|
-
background: linear-gradient(135deg, rgba(15, 23, 42, 0.95), rgba(30, 41, 59, 0.95));
|
|
152
|
-
backdrop-filter: blur(12px);
|
|
153
|
-
border: 1px solid rgba(16, 185, 129, 0.3);
|
|
154
|
-
border-radius: 12px;
|
|
155
|
-
color: #fff;
|
|
156
|
-
cursor: pointer;
|
|
157
|
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
158
|
-
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(16, 185, 129, 0.1);
|
|
159
|
-
}
|
|
160
|
-
.flexi-dev-btn:hover {
|
|
161
|
-
border-color: rgba(16, 185, 129, 0.6);
|
|
162
|
-
transform: translateY(-3px);
|
|
163
|
-
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5), 0 0 20px rgba(16, 185, 129, 0.2);
|
|
164
|
-
}
|
|
165
|
-
.flexi-dev-logo {
|
|
166
|
-
width: 22px;
|
|
167
|
-
height: 22px;
|
|
168
|
-
background: linear-gradient(135deg, #10b981, #06b6d4);
|
|
169
|
-
border-radius: 6px;
|
|
170
|
-
display: flex;
|
|
171
|
-
align-items: center;
|
|
172
|
-
justify-content: center;
|
|
173
|
-
font-weight: 800;
|
|
174
|
-
font-size: 12px;
|
|
175
|
-
color: #000;
|
|
176
|
-
}
|
|
177
|
-
.flexi-dev-panel {
|
|
178
|
-
position: absolute;
|
|
179
|
-
bottom: 100%;
|
|
180
|
-
left: 0;
|
|
181
|
-
margin-bottom: 12px;
|
|
182
|
-
min-width: 320px;
|
|
183
|
-
background: linear-gradient(135deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.98));
|
|
184
|
-
backdrop-filter: blur(16px);
|
|
185
|
-
border: 1px solid rgba(16, 185, 129, 0.2);
|
|
186
|
-
border-radius: 16px;
|
|
187
|
-
padding: 0;
|
|
188
|
-
opacity: 0;
|
|
189
|
-
visibility: hidden;
|
|
190
|
-
transform: translateY(10px) scale(0.95);
|
|
191
|
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
192
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(16, 185, 129, 0.1);
|
|
193
|
-
overflow: hidden;
|
|
194
|
-
}
|
|
195
|
-
.flexi-dev-btn:hover + .flexi-dev-panel,
|
|
196
|
-
.flexi-dev-panel:hover {
|
|
197
|
-
opacity: 1;
|
|
198
|
-
visibility: visible;
|
|
199
|
-
transform: translateY(0) scale(1);
|
|
200
|
-
}
|
|
201
|
-
.flexi-dev-header {
|
|
202
|
-
display: flex;
|
|
203
|
-
align-items: center;
|
|
204
|
-
gap: 10px;
|
|
205
|
-
padding: 16px 20px;
|
|
206
|
-
background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(6, 182, 212, 0.1));
|
|
207
|
-
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
208
|
-
}
|
|
209
|
-
.flexi-dev-header-logo {
|
|
210
|
-
width: 28px;
|
|
211
|
-
height: 28px;
|
|
212
|
-
background: linear-gradient(135deg, #10b981, #06b6d4);
|
|
213
|
-
border-radius: 8px;
|
|
214
|
-
display: flex;
|
|
215
|
-
align-items: center;
|
|
216
|
-
justify-content: center;
|
|
217
|
-
font-weight: 800;
|
|
218
|
-
font-size: 14px;
|
|
219
|
-
color: #000;
|
|
220
|
-
}
|
|
221
|
-
.flexi-dev-header-text {
|
|
222
|
-
font-weight: 700;
|
|
223
|
-
font-size: 15px;
|
|
224
|
-
background: linear-gradient(90deg, #10b981, #06b6d4);
|
|
225
|
-
-webkit-background-clip: text;
|
|
226
|
-
-webkit-text-fill-color: transparent;
|
|
227
|
-
}
|
|
228
|
-
.flexi-dev-header-version {
|
|
229
|
-
margin-left: auto;
|
|
230
|
-
font-size: 11px;
|
|
231
|
-
color: #64748b;
|
|
232
|
-
background: rgba(255, 255, 255, 0.05);
|
|
233
|
-
padding: 3px 8px;
|
|
234
|
-
border-radius: 6px;
|
|
235
|
-
}
|
|
236
|
-
.flexi-dev-row {
|
|
237
|
-
display: flex;
|
|
238
|
-
justify-content: space-between;
|
|
239
|
-
align-items: center;
|
|
240
|
-
padding: 12px 20px;
|
|
241
|
-
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
|
242
|
-
}
|
|
243
|
-
.flexi-dev-row:last-child {
|
|
244
|
-
border-bottom: none;
|
|
245
|
-
}
|
|
246
|
-
.flexi-dev-label {
|
|
247
|
-
color: #64748b;
|
|
248
|
-
font-size: 12px;
|
|
249
|
-
}
|
|
250
|
-
.flexi-dev-value {
|
|
251
|
-
font-weight: 600;
|
|
252
|
-
color: #f1f5f9;
|
|
253
|
-
}
|
|
254
|
-
.flexi-dev-badge {
|
|
255
|
-
display: inline-flex;
|
|
256
|
-
align-items: center;
|
|
257
|
-
gap: 6px;
|
|
258
|
-
padding: 4px 10px;
|
|
259
|
-
border-radius: 6px;
|
|
260
|
-
font-size: 11px;
|
|
261
|
-
font-weight: 700;
|
|
262
|
-
letter-spacing: 0.5px;
|
|
263
|
-
}
|
|
264
|
-
.flexi-dev-badge.ssr {
|
|
265
|
-
background: linear-gradient(135deg, rgba(251, 191, 36, 0.2), rgba(245, 158, 11, 0.1));
|
|
266
|
-
color: #fbbf24;
|
|
267
|
-
border: 1px solid rgba(251, 191, 36, 0.3);
|
|
268
|
-
}
|
|
269
|
-
.flexi-dev-badge.ssg {
|
|
270
|
-
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(16, 185, 129, 0.1));
|
|
271
|
-
color: #22c55e;
|
|
272
|
-
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
273
|
-
}
|
|
274
|
-
.flexi-dev-badge.isr {
|
|
275
|
-
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.1));
|
|
276
|
-
color: #818cf8;
|
|
277
|
-
border: 1px solid rgba(99, 102, 241, 0.3);
|
|
278
|
-
}
|
|
279
|
-
.flexi-dev-badge.csr {
|
|
280
|
-
background: linear-gradient(135deg, rgba(6, 182, 212, 0.2), rgba(14, 165, 233, 0.1));
|
|
281
|
-
color: #06b6d4;
|
|
282
|
-
border: 1px solid rgba(6, 182, 212, 0.3);
|
|
283
|
-
}
|
|
284
|
-
.flexi-dev-badge.api {
|
|
285
|
-
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(99, 102, 241, 0.1));
|
|
286
|
-
color: #60a5fa;
|
|
287
|
-
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
288
|
-
}
|
|
289
|
-
.flexi-dev-time {
|
|
290
|
-
font-weight: 700;
|
|
291
|
-
color: ${renderTime < 100 ? '#10b981' : renderTime < 500 ? '#fbbf24' : '#ef4444'};
|
|
292
|
-
}
|
|
293
|
-
.flexi-dev-status {
|
|
294
|
-
display: flex;
|
|
295
|
-
align-items: center;
|
|
296
|
-
gap: 8px;
|
|
297
|
-
}
|
|
298
|
-
.flexi-dev-dot {
|
|
299
|
-
width: 10px;
|
|
300
|
-
height: 10px;
|
|
301
|
-
border-radius: 50%;
|
|
302
|
-
background: linear-gradient(135deg, #10b981, #06b6d4);
|
|
303
|
-
box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
|
|
304
|
-
animation: pulse 2s infinite;
|
|
305
|
-
}
|
|
306
|
-
.flexi-dev-dot.error {
|
|
307
|
-
background: linear-gradient(135deg, #ef4444, #f87171);
|
|
308
|
-
box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
|
|
309
|
-
}
|
|
310
|
-
@keyframes pulse {
|
|
311
|
-
0%, 100% { opacity: 1; transform: scale(1); }
|
|
312
|
-
50% { opacity: 0.7; transform: scale(0.9); }
|
|
313
|
-
}
|
|
314
|
-
.flexi-dev-links {
|
|
315
|
-
display: flex;
|
|
316
|
-
gap: 8px;
|
|
317
|
-
padding: 16px 20px;
|
|
318
|
-
background: rgba(0, 0, 0, 0.2);
|
|
319
|
-
}
|
|
320
|
-
.flexi-dev-link {
|
|
321
|
-
flex: 1;
|
|
322
|
-
padding: 8px 12px;
|
|
323
|
-
background: rgba(255, 255, 255, 0.03);
|
|
324
|
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
325
|
-
border-radius: 8px;
|
|
326
|
-
color: #94a3b8;
|
|
327
|
-
text-decoration: none;
|
|
328
|
-
font-size: 11px;
|
|
329
|
-
font-weight: 500;
|
|
330
|
-
text-align: center;
|
|
331
|
-
transition: all 0.2s;
|
|
332
|
-
}
|
|
333
|
-
.flexi-dev-link:hover {
|
|
334
|
-
background: rgba(16, 185, 129, 0.15);
|
|
335
|
-
border-color: rgba(16, 185, 129, 0.3);
|
|
336
|
-
color: #10b981;
|
|
337
|
-
}
|
|
338
|
-
</style>
|
|
339
|
-
|
|
340
|
-
<button class="flexi-dev-btn" title="FlexiReact Dev Tools">
|
|
341
|
-
<div class="flexi-dev-logo">⚡</div>
|
|
342
|
-
<span class="flexi-dev-badge ${pageType.toLowerCase()}">${pageType}</span>
|
|
343
|
-
</button>
|
|
344
|
-
|
|
345
|
-
<div class="flexi-dev-panel">
|
|
346
|
-
<div class="flexi-dev-header">
|
|
347
|
-
<div class="flexi-dev-header-logo">⚡</div>
|
|
348
|
-
<span class="flexi-dev-header-text">FlexiReact</span>
|
|
349
|
-
<span class="flexi-dev-header-version">v1.0.0</span>
|
|
350
|
-
</div>
|
|
351
|
-
|
|
352
|
-
<div class="flexi-dev-row">
|
|
353
|
-
<span class="flexi-dev-label">Status</span>
|
|
354
|
-
<div class="flexi-dev-status">
|
|
355
|
-
<div class="flexi-dev-dot ${hasError ? 'error' : ''}"></div>
|
|
356
|
-
<span class="flexi-dev-value">${hasError ? 'Error' : 'Ready'}</span>
|
|
357
|
-
</div>
|
|
358
|
-
</div>
|
|
359
|
-
|
|
360
|
-
<div class="flexi-dev-row">
|
|
361
|
-
<span class="flexi-dev-label">Route</span>
|
|
362
|
-
<span class="flexi-dev-value">${route}</span>
|
|
363
|
-
</div>
|
|
364
|
-
|
|
365
|
-
<div class="flexi-dev-row">
|
|
366
|
-
<span class="flexi-dev-label">Page Type</span>
|
|
367
|
-
<span class="flexi-dev-badge ${pageType.toLowerCase()}">${pageType}</span>
|
|
368
|
-
</div>
|
|
369
|
-
|
|
370
|
-
<div class="flexi-dev-row">
|
|
371
|
-
<span class="flexi-dev-label">Render Time</span>
|
|
372
|
-
<span class="flexi-dev-value flexi-dev-time">${renderTime}ms</span>
|
|
373
|
-
</div>
|
|
374
|
-
|
|
375
|
-
<div class="flexi-dev-row">
|
|
376
|
-
<span class="flexi-dev-label">Hydration</span>
|
|
377
|
-
<span class="flexi-dev-value">${isHydrated ? '✓ Client' : '○ Server'}</span>
|
|
378
|
-
</div>
|
|
379
|
-
|
|
380
|
-
<div class="flexi-dev-links">
|
|
381
|
-
<a href="/_flexi/routes" class="flexi-dev-link">Routes</a>
|
|
382
|
-
<a href="/api" class="flexi-dev-link">API</a>
|
|
383
|
-
<a href="https://github.com/nicholasmusic/flexireact" target="_blank" class="flexi-dev-link">Docs ↗</a>
|
|
384
|
-
</div>
|
|
385
|
-
</div>
|
|
386
|
-
</div>
|
|
387
|
-
|
|
388
|
-
<script>
|
|
389
|
-
// Track hydration
|
|
390
|
-
window.__FLEXI_DEV__ = {
|
|
391
|
-
renderTime: ${renderTime},
|
|
392
|
-
pageType: '${pageType}',
|
|
393
|
-
route: '${route}',
|
|
394
|
-
hydrated: false
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
// Update hydration status when React hydrates
|
|
398
|
-
const observer = new MutationObserver(() => {
|
|
399
|
-
if (document.getElementById('root')?.children.length > 0) {
|
|
400
|
-
window.__FLEXI_DEV__.hydrated = true;
|
|
401
|
-
const hydrationEl = document.querySelector('.flexi-dev-row:nth-child(5) .flexi-dev-value');
|
|
402
|
-
if (hydrationEl) hydrationEl.textContent = '✓ Client';
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
observer.observe(document.getElementById('root'), { childList: true, subtree: true });
|
|
406
|
-
|
|
407
|
-
// Log to console
|
|
408
|
-
console.log('%c⚛ FlexiReact Dev', 'color: #61DAFB; font-weight: bold; font-size: 14px;');
|
|
409
|
-
console.log('%cPage: ' + '${route}' + ' [${pageType}] - ${renderTime}ms', 'color: #94a3b8;');
|
|
410
|
-
</script>
|
|
411
|
-
`;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Builds complete HTML document
|
|
416
|
-
*/
|
|
417
|
-
function buildHtmlDocument(options) {
|
|
418
|
-
const {
|
|
419
|
-
content,
|
|
420
|
-
title,
|
|
421
|
-
meta = {},
|
|
422
|
-
scripts = [],
|
|
423
|
-
styles = [],
|
|
424
|
-
props = {},
|
|
425
|
-
isSSG = false,
|
|
426
|
-
renderTime = 0,
|
|
427
|
-
route = '/',
|
|
428
|
-
isClientComponent = false,
|
|
429
|
-
favicon = null
|
|
430
|
-
} = options;
|
|
431
|
-
|
|
432
|
-
const metaTags = Object.entries(meta)
|
|
433
|
-
.map(([name, content]) => {
|
|
434
|
-
if (name.startsWith('og:')) {
|
|
435
|
-
return `<meta property="${escapeHtml(name)}" content="${escapeHtml(content)}">`;
|
|
436
|
-
}
|
|
437
|
-
return `<meta name="${escapeHtml(name)}" content="${escapeHtml(content)}">`;
|
|
438
|
-
})
|
|
439
|
-
.join('\n ');
|
|
440
|
-
|
|
441
|
-
const styleTags = styles
|
|
442
|
-
.map(style => {
|
|
443
|
-
if (typeof style === 'string') {
|
|
444
|
-
return `<link rel="stylesheet" href="${escapeHtml(style)}">`;
|
|
445
|
-
}
|
|
446
|
-
return `<style>${style.content}</style>`;
|
|
447
|
-
})
|
|
448
|
-
.join('\n ');
|
|
449
|
-
|
|
450
|
-
const scriptTags = scripts
|
|
451
|
-
.map(script => {
|
|
452
|
-
if (typeof script === 'string') {
|
|
453
|
-
return `<script src="${escapeHtml(script)}"></script>`;
|
|
454
|
-
}
|
|
455
|
-
const type = script.type ? ` type="${script.type}"` : '';
|
|
456
|
-
if (script.src) {
|
|
457
|
-
return `<script${type} src="${escapeHtml(script.src)}"></script>`;
|
|
458
|
-
}
|
|
459
|
-
return `<script${type}>${script.content}</script>`;
|
|
460
|
-
})
|
|
461
|
-
.join('\n ');
|
|
462
|
-
|
|
463
|
-
// FlexiReact SVG favicon (properly encoded)
|
|
464
|
-
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>`);
|
|
465
|
-
|
|
466
|
-
// Generate Dev Toolbar for development mode
|
|
467
|
-
const isDev = process.env.NODE_ENV !== 'production';
|
|
468
|
-
const pageType = isSSG ? 'SSG' : isClientComponent ? 'CSR' : 'SSR';
|
|
469
|
-
const devToolbar = isDev ? generateDevToolbar({
|
|
470
|
-
renderTime,
|
|
471
|
-
pageType,
|
|
472
|
-
route,
|
|
473
|
-
isHydrated: isClientComponent
|
|
474
|
-
}) : '';
|
|
475
|
-
|
|
476
|
-
// Determine favicon link
|
|
477
|
-
const faviconLink = favicon
|
|
478
|
-
? `<link rel="icon" href="${escapeHtml(favicon)}">`
|
|
479
|
-
: `<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,${faviconSvg}">`;
|
|
480
|
-
|
|
481
|
-
return `<!DOCTYPE html>
|
|
482
|
-
<html lang="en" class="dark">
|
|
483
|
-
<head>
|
|
484
|
-
<meta charset="UTF-8">
|
|
485
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
486
|
-
<title>${escapeHtml(title)}</title>
|
|
487
|
-
${faviconLink}
|
|
488
|
-
${metaTags}
|
|
489
|
-
${styleTags}
|
|
490
|
-
<script>
|
|
491
|
-
(function() {
|
|
492
|
-
var theme = localStorage.getItem('theme');
|
|
493
|
-
if (theme === 'light') {
|
|
494
|
-
document.documentElement.classList.remove('dark');
|
|
495
|
-
}
|
|
496
|
-
})();
|
|
497
|
-
</script>
|
|
498
|
-
</head>
|
|
499
|
-
<body>
|
|
500
|
-
<div id="root">${content}</div>
|
|
501
|
-
<script>
|
|
502
|
-
window.__FLEXI_DATA__ = ${JSON.stringify({ props, isSSG })};
|
|
503
|
-
</script>
|
|
504
|
-
${scriptTags}
|
|
505
|
-
${devToolbar}
|
|
506
|
-
</body>
|
|
507
|
-
</html>`;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Renders an error page with beautiful styling
|
|
512
|
-
*/
|
|
513
|
-
export function renderError(statusCode, message, stack = null) {
|
|
514
|
-
const showStack = process.env.NODE_ENV !== 'production' && stack;
|
|
515
|
-
const isDev = process.env.NODE_ENV !== 'production';
|
|
516
|
-
|
|
517
|
-
// Different messages for different status codes
|
|
518
|
-
const errorMessages = {
|
|
519
|
-
404: { title: 'Page Not Found', emoji: '🔍', desc: 'The page you\'re looking for doesn\'t exist or has been moved.' },
|
|
520
|
-
500: { title: 'Server Error', emoji: '💥', desc: 'Something went wrong on our end. Please try again later.' },
|
|
521
|
-
403: { title: 'Forbidden', emoji: '🚫', desc: 'You don\'t have permission to access this resource.' },
|
|
522
|
-
401: { title: 'Unauthorized', emoji: '🔐', desc: 'Please log in to access this page.' },
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
const errorInfo = errorMessages[statusCode] || { title: 'Error', emoji: '⚠️', desc: message };
|
|
526
|
-
|
|
527
|
-
return `<!DOCTYPE html>
|
|
528
|
-
<html lang="en">
|
|
529
|
-
<head>
|
|
530
|
-
<meta charset="UTF-8">
|
|
531
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
532
|
-
<title>${statusCode} - ${errorInfo.title} | FlexiReact</title>
|
|
533
|
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'><defs><linearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'><stop offset='0%25' stop-color='%2361DAFB'/><stop offset='100%25' stop-color='%2321A1F1'/></linearGradient></defs><circle cx='100' cy='100' r='12' fill='url(%23g)'/><ellipse cx='100' cy='100' rx='80' ry='30' fill='none' stroke='url(%23g)' stroke-width='6' transform='rotate(-30 100 100)'/><ellipse cx='100' cy='100' rx='80' ry='30' fill='none' stroke='url(%23g)' stroke-width='6' transform='rotate(30 100 100)'/><ellipse cx='100' cy='100' rx='80' ry='30' fill='none' stroke='url(%23g)' stroke-width='6' transform='rotate(90 100 100)'/></svg>">
|
|
534
|
-
<style>
|
|
535
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
536
|
-
body {
|
|
537
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
538
|
-
min-height: 100vh;
|
|
539
|
-
display: flex;
|
|
540
|
-
align-items: center;
|
|
541
|
-
justify-content: center;
|
|
542
|
-
background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 50%, #0f172a 100%);
|
|
543
|
-
color: #f8fafc;
|
|
544
|
-
overflow: hidden;
|
|
545
|
-
}
|
|
546
|
-
.bg-grid {
|
|
547
|
-
position: fixed;
|
|
548
|
-
inset: 0;
|
|
549
|
-
background-image:
|
|
550
|
-
linear-gradient(rgba(99, 102, 241, 0.03) 1px, transparent 1px),
|
|
551
|
-
linear-gradient(90deg, rgba(99, 102, 241, 0.03) 1px, transparent 1px);
|
|
552
|
-
background-size: 50px 50px;
|
|
553
|
-
pointer-events: none;
|
|
554
|
-
}
|
|
555
|
-
.bg-glow {
|
|
556
|
-
position: fixed;
|
|
557
|
-
top: 50%;
|
|
558
|
-
left: 50%;
|
|
559
|
-
transform: translate(-50%, -50%);
|
|
560
|
-
width: 600px;
|
|
561
|
-
height: 600px;
|
|
562
|
-
background: radial-gradient(circle, rgba(99, 102, 241, 0.15) 0%, transparent 70%);
|
|
563
|
-
pointer-events: none;
|
|
564
|
-
}
|
|
565
|
-
.container {
|
|
566
|
-
position: relative;
|
|
567
|
-
text-align: center;
|
|
568
|
-
padding: 2rem;
|
|
569
|
-
max-width: 600px;
|
|
570
|
-
animation: fadeIn 0.5s ease-out;
|
|
571
|
-
}
|
|
572
|
-
@keyframes fadeIn {
|
|
573
|
-
from { opacity: 0; transform: translateY(20px); }
|
|
574
|
-
to { opacity: 1; transform: translateY(0); }
|
|
575
|
-
}
|
|
576
|
-
.logo {
|
|
577
|
-
width: 80px;
|
|
578
|
-
height: 80px;
|
|
579
|
-
margin: 0 auto 2rem;
|
|
580
|
-
opacity: 0.8;
|
|
581
|
-
}
|
|
582
|
-
.emoji {
|
|
583
|
-
font-size: 4rem;
|
|
584
|
-
margin-bottom: 1rem;
|
|
585
|
-
}
|
|
586
|
-
.error-code {
|
|
587
|
-
font-size: 8rem;
|
|
588
|
-
font-weight: 800;
|
|
589
|
-
line-height: 1;
|
|
590
|
-
background: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #6366f1 100%);
|
|
591
|
-
-webkit-background-clip: text;
|
|
592
|
-
-webkit-text-fill-color: transparent;
|
|
593
|
-
background-clip: text;
|
|
594
|
-
text-shadow: 0 0 80px rgba(99, 102, 241, 0.5);
|
|
595
|
-
}
|
|
596
|
-
.error-title {
|
|
597
|
-
font-size: 1.5rem;
|
|
598
|
-
font-weight: 600;
|
|
599
|
-
margin: 1rem 0 0.5rem;
|
|
600
|
-
color: #e2e8f0;
|
|
601
|
-
}
|
|
602
|
-
.error-desc {
|
|
603
|
-
font-size: 1rem;
|
|
604
|
-
color: #94a3b8;
|
|
605
|
-
margin-bottom: 2rem;
|
|
606
|
-
line-height: 1.6;
|
|
607
|
-
}
|
|
608
|
-
.buttons {
|
|
609
|
-
display: flex;
|
|
610
|
-
gap: 1rem;
|
|
611
|
-
justify-content: center;
|
|
612
|
-
flex-wrap: wrap;
|
|
613
|
-
}
|
|
614
|
-
.btn {
|
|
615
|
-
display: inline-flex;
|
|
616
|
-
align-items: center;
|
|
617
|
-
gap: 0.5rem;
|
|
618
|
-
padding: 0.875rem 1.75rem;
|
|
619
|
-
border-radius: 12px;
|
|
620
|
-
font-weight: 600;
|
|
621
|
-
text-decoration: none;
|
|
622
|
-
transition: all 0.2s;
|
|
623
|
-
font-size: 0.95rem;
|
|
624
|
-
}
|
|
625
|
-
.btn-primary {
|
|
626
|
-
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
|
627
|
-
color: white;
|
|
628
|
-
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4);
|
|
629
|
-
}
|
|
630
|
-
.btn-primary:hover {
|
|
631
|
-
transform: translateY(-2px);
|
|
632
|
-
box-shadow: 0 8px 30px rgba(99, 102, 241, 0.5);
|
|
633
|
-
}
|
|
634
|
-
.btn-secondary {
|
|
635
|
-
background: rgba(255, 255, 255, 0.05);
|
|
636
|
-
color: #e2e8f0;
|
|
637
|
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
638
|
-
}
|
|
639
|
-
.btn-secondary:hover {
|
|
640
|
-
background: rgba(255, 255, 255, 0.1);
|
|
641
|
-
border-color: rgba(255, 255, 255, 0.2);
|
|
642
|
-
}
|
|
643
|
-
.stack {
|
|
644
|
-
text-align: left;
|
|
645
|
-
background: rgba(0, 0, 0, 0.3);
|
|
646
|
-
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
647
|
-
padding: 1rem;
|
|
648
|
-
border-radius: 12px;
|
|
649
|
-
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
650
|
-
font-size: 0.8rem;
|
|
651
|
-
overflow-x: auto;
|
|
652
|
-
white-space: pre-wrap;
|
|
653
|
-
margin-top: 2rem;
|
|
654
|
-
color: #fca5a5;
|
|
655
|
-
max-height: 200px;
|
|
656
|
-
overflow-y: auto;
|
|
657
|
-
}
|
|
658
|
-
.dev-badge {
|
|
659
|
-
position: fixed;
|
|
660
|
-
bottom: 1rem;
|
|
661
|
-
right: 1rem;
|
|
662
|
-
display: flex;
|
|
663
|
-
align-items: center;
|
|
664
|
-
gap: 0.5rem;
|
|
665
|
-
padding: 0.5rem 1rem;
|
|
666
|
-
background: rgba(0, 0, 0, 0.5);
|
|
667
|
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
668
|
-
border-radius: 8px;
|
|
669
|
-
font-size: 0.75rem;
|
|
670
|
-
color: #94a3b8;
|
|
671
|
-
}
|
|
672
|
-
.dev-badge svg {
|
|
673
|
-
width: 16px;
|
|
674
|
-
height: 16px;
|
|
675
|
-
}
|
|
676
|
-
</style>
|
|
677
|
-
</head>
|
|
678
|
-
<body>
|
|
679
|
-
<div class="bg-grid"></div>
|
|
680
|
-
<div class="bg-glow"></div>
|
|
681
|
-
|
|
682
|
-
<div class="container">
|
|
683
|
-
<svg class="logo" viewBox="0 0 200 200" fill="none">
|
|
684
|
-
<defs>
|
|
685
|
-
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
686
|
-
<stop offset="0%" stop-color="#6366f1"/>
|
|
687
|
-
<stop offset="100%" stop-color="#a855f7"/>
|
|
688
|
-
</linearGradient>
|
|
689
|
-
</defs>
|
|
690
|
-
<circle cx="100" cy="100" r="12" fill="url(#g)"/>
|
|
691
|
-
<ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#g)" stroke-width="6" transform="rotate(-30 100 100)"/>
|
|
692
|
-
<ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#g)" stroke-width="6" transform="rotate(30 100 100)"/>
|
|
693
|
-
<ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#g)" stroke-width="6" transform="rotate(90 100 100)"/>
|
|
694
|
-
</svg>
|
|
695
|
-
|
|
696
|
-
<div class="error-code">${statusCode}</div>
|
|
697
|
-
<h1 class="error-title">${errorInfo.title}</h1>
|
|
698
|
-
<p class="error-desc">${errorInfo.desc}</p>
|
|
699
|
-
|
|
700
|
-
<div class="buttons">
|
|
701
|
-
<a href="/" class="btn btn-primary">
|
|
702
|
-
<span>←</span> Back to Home
|
|
703
|
-
</a>
|
|
704
|
-
<a href="javascript:history.back()" class="btn btn-secondary">
|
|
705
|
-
Go Back
|
|
706
|
-
</a>
|
|
707
|
-
</div>
|
|
708
|
-
|
|
709
|
-
${showStack ? `<pre class="stack">${escapeHtml(stack)}</pre>` : ''}
|
|
710
|
-
</div>
|
|
711
|
-
|
|
712
|
-
${isDev ? `
|
|
713
|
-
<div class="dev-badge">
|
|
714
|
-
<svg viewBox="0 0 200 200" fill="none">
|
|
715
|
-
<defs><linearGradient id="db" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#61DAFB"/><stop offset="100%" stop-color="#21A1F1"/></linearGradient></defs>
|
|
716
|
-
<circle cx="100" cy="100" r="12" fill="url(#db)"/>
|
|
717
|
-
<ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#db)" stroke-width="6" transform="rotate(-30 100 100)"/>
|
|
718
|
-
<ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#db)" stroke-width="6" transform="rotate(30 100 100)"/>
|
|
719
|
-
<ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#db)" stroke-width="6" transform="rotate(90 100 100)"/>
|
|
720
|
-
</svg>
|
|
721
|
-
FlexiReact Dev
|
|
722
|
-
</div>
|
|
723
|
-
` : ''}
|
|
724
|
-
</body>
|
|
725
|
-
</html>`;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
/**
|
|
729
|
-
* Renders a loading state
|
|
730
|
-
*/
|
|
731
|
-
export function renderLoading(LoadingComponent) {
|
|
732
|
-
if (!LoadingComponent) {
|
|
733
|
-
return `<div class="flexi-loading">
|
|
734
|
-
<div class="flexi-spinner"></div>
|
|
735
|
-
<style>
|
|
736
|
-
.flexi-loading {
|
|
737
|
-
display: flex;
|
|
738
|
-
align-items: center;
|
|
739
|
-
justify-content: center;
|
|
740
|
-
min-height: 200px;
|
|
741
|
-
}
|
|
742
|
-
.flexi-spinner {
|
|
743
|
-
width: 40px;
|
|
744
|
-
height: 40px;
|
|
745
|
-
border: 3px solid #f3f3f3;
|
|
746
|
-
border-top: 3px solid #667eea;
|
|
747
|
-
border-radius: 50%;
|
|
748
|
-
animation: spin 1s linear infinite;
|
|
749
|
-
}
|
|
750
|
-
@keyframes spin {
|
|
751
|
-
0% { transform: rotate(0deg); }
|
|
752
|
-
100% { transform: rotate(360deg); }
|
|
753
|
-
}
|
|
754
|
-
</style>
|
|
755
|
-
</div>`;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
return renderToString(React.createElement(LoadingComponent));
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
export default {
|
|
762
|
-
renderPage,
|
|
763
|
-
renderError,
|
|
764
|
-
renderLoading
|
|
765
|
-
};
|