@flexireact/core 1.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/LICENSE +21 -0
- package/README.md +549 -0
- package/cli/index.js +992 -0
- package/cli/index.ts +1129 -0
- package/core/api.js +143 -0
- package/core/build/index.js +357 -0
- package/core/cli/logger.js +347 -0
- package/core/client/hydration.js +137 -0
- package/core/client/index.js +8 -0
- package/core/client/islands.js +138 -0
- package/core/client/navigation.js +204 -0
- package/core/client/runtime.js +36 -0
- package/core/config.js +113 -0
- package/core/context.js +83 -0
- package/core/dev.js +47 -0
- package/core/index.js +76 -0
- package/core/islands/index.js +281 -0
- package/core/loader.js +111 -0
- package/core/logger.js +242 -0
- package/core/middleware/index.js +393 -0
- package/core/plugins/index.js +370 -0
- package/core/render/index.js +765 -0
- package/core/render.js +134 -0
- package/core/router/index.js +296 -0
- package/core/router.js +141 -0
- package/core/rsc/index.js +198 -0
- package/core/server/index.js +653 -0
- package/core/server.js +197 -0
- package/core/ssg/index.js +321 -0
- package/core/utils.js +176 -0
- package/package.json +73 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact CLI Logger - Premium Console Output
|
|
3
|
+
* Beautiful, modern, and professional CLI aesthetics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Color Palette
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
const colors = {
|
|
13
|
+
primary: (text) => pc.green(text), // Neon emerald
|
|
14
|
+
secondary: (text) => pc.cyan(text), // Cyan
|
|
15
|
+
accent: (text) => pc.magenta(text), // Magenta accent
|
|
16
|
+
success: (text) => pc.green(text), // Success green
|
|
17
|
+
error: (text) => pc.red(text), // Error red
|
|
18
|
+
warning: (text) => pc.yellow(text), // Warning yellow
|
|
19
|
+
info: (text) => pc.blue(text), // Info blue
|
|
20
|
+
muted: (text) => pc.dim(text), // Muted/dim
|
|
21
|
+
bold: (text) => pc.bold(text), // Bold
|
|
22
|
+
white: (text) => pc.white(text), // White
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Icons & Symbols
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
const icons = {
|
|
30
|
+
success: pc.green('✓'),
|
|
31
|
+
error: pc.red('✗'),
|
|
32
|
+
warning: pc.yellow('⚠'),
|
|
33
|
+
info: pc.blue('ℹ'),
|
|
34
|
+
arrow: pc.dim('→'),
|
|
35
|
+
dot: pc.dim('•'),
|
|
36
|
+
star: pc.yellow('★'),
|
|
37
|
+
rocket: '🚀',
|
|
38
|
+
lightning: '⚡',
|
|
39
|
+
puzzle: '🧩',
|
|
40
|
+
island: '🏝️',
|
|
41
|
+
plug: '🔌',
|
|
42
|
+
package: '📦',
|
|
43
|
+
folder: '📁',
|
|
44
|
+
file: '📄',
|
|
45
|
+
globe: '🌐',
|
|
46
|
+
check: pc.green('✔'),
|
|
47
|
+
cross: pc.red('✘'),
|
|
48
|
+
spinner: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Box Drawing
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
const box = {
|
|
56
|
+
topLeft: '╭',
|
|
57
|
+
topRight: '╮',
|
|
58
|
+
bottomLeft: '╰',
|
|
59
|
+
bottomRight: '╯',
|
|
60
|
+
horizontal: '─',
|
|
61
|
+
vertical: '│',
|
|
62
|
+
horizontalDown: '┬',
|
|
63
|
+
horizontalUp: '┴',
|
|
64
|
+
verticalRight: '├',
|
|
65
|
+
verticalLeft: '┤',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function createBox(content, options = {}) {
|
|
69
|
+
const {
|
|
70
|
+
padding = 1,
|
|
71
|
+
borderColor = colors.primary,
|
|
72
|
+
width = 50
|
|
73
|
+
} = options;
|
|
74
|
+
|
|
75
|
+
const lines = content.split('\n');
|
|
76
|
+
const maxLen = Math.max(...lines.map(l => stripAnsi(l).length), width - 4);
|
|
77
|
+
const innerWidth = maxLen + (padding * 2);
|
|
78
|
+
|
|
79
|
+
const top = borderColor(box.topLeft + box.horizontal.repeat(innerWidth) + box.topRight);
|
|
80
|
+
const bottom = borderColor(box.bottomLeft + box.horizontal.repeat(innerWidth) + box.bottomRight);
|
|
81
|
+
const empty = borderColor(box.vertical) + ' '.repeat(innerWidth) + borderColor(box.vertical);
|
|
82
|
+
|
|
83
|
+
const contentLines = lines.map(line => {
|
|
84
|
+
const stripped = stripAnsi(line);
|
|
85
|
+
const pad = innerWidth - stripped.length - padding;
|
|
86
|
+
return borderColor(box.vertical) + ' '.repeat(padding) + line + ' '.repeat(Math.max(0, pad)) + borderColor(box.vertical);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const paddingLines = padding > 0 ? [empty] : [];
|
|
90
|
+
|
|
91
|
+
return [top, ...paddingLines, ...contentLines, ...paddingLines, bottom].join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function stripAnsi(str) {
|
|
95
|
+
return str.replace(/\x1B\[[0-9;]*m/g, '');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// ASCII Art Banners
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
const banners = {
|
|
103
|
+
// Main banner - Gradient style
|
|
104
|
+
main: `
|
|
105
|
+
${pc.cyan(' ╭─────────────────────────────────────────╮')}
|
|
106
|
+
${pc.cyan(' │')} ${pc.cyan('│')}
|
|
107
|
+
${pc.cyan(' │')} ${pc.bold(pc.green('⚡'))} ${pc.bold(pc.white('F L E X I R E A C T'))} ${pc.cyan('│')}
|
|
108
|
+
${pc.cyan(' │')} ${pc.cyan('│')}
|
|
109
|
+
${pc.cyan(' │')} ${pc.dim('The Modern React Framework')} ${pc.cyan('│')}
|
|
110
|
+
${pc.cyan(' │')} ${pc.cyan('│')}
|
|
111
|
+
${pc.cyan(' ╰─────────────────────────────────────────╯')}
|
|
112
|
+
`,
|
|
113
|
+
|
|
114
|
+
// Compact banner
|
|
115
|
+
compact: `
|
|
116
|
+
${pc.green('⚡')} ${pc.bold('FlexiReact')} ${pc.dim('v2.1.0')}
|
|
117
|
+
`,
|
|
118
|
+
|
|
119
|
+
// Build banner
|
|
120
|
+
build: `
|
|
121
|
+
${pc.cyan(' ╭─────────────────────────────────────────╮')}
|
|
122
|
+
${pc.cyan(' │')} ${pc.bold(pc.green('⚡'))} ${pc.bold('FlexiReact Build')} ${pc.cyan('│')}
|
|
123
|
+
${pc.cyan(' ╰─────────────────────────────────────────╯')}
|
|
124
|
+
`,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Server Status Panel
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
function serverPanel(config) {
|
|
132
|
+
const {
|
|
133
|
+
mode = 'development',
|
|
134
|
+
port = 3000,
|
|
135
|
+
host = 'localhost',
|
|
136
|
+
pagesDir = './pages',
|
|
137
|
+
islands = true,
|
|
138
|
+
rsc = true,
|
|
139
|
+
} = config;
|
|
140
|
+
|
|
141
|
+
const modeColor = mode === 'development' ? colors.warning : colors.success;
|
|
142
|
+
const url = `http://${host}:${port}`;
|
|
143
|
+
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log(pc.dim(' ─────────────────────────────────────────'));
|
|
146
|
+
console.log(` ${icons.arrow} ${pc.dim('Mode:')} ${modeColor(mode)}`);
|
|
147
|
+
console.log(` ${icons.arrow} ${pc.dim('Local:')} ${pc.cyan(pc.underline(url))}`);
|
|
148
|
+
console.log(` ${icons.arrow} ${pc.dim('Pages:')} ${pc.white(pagesDir)}`);
|
|
149
|
+
console.log(` ${icons.arrow} ${pc.dim('Islands:')} ${islands ? pc.green('enabled') : pc.dim('disabled')}`);
|
|
150
|
+
console.log(` ${icons.arrow} ${pc.dim('RSC:')} ${rsc ? pc.green('enabled') : pc.dim('disabled')}`);
|
|
151
|
+
console.log(pc.dim(' ─────────────────────────────────────────'));
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(` ${icons.success} ${pc.green('Ready for requests...')}`);
|
|
154
|
+
console.log('');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// Plugin Loader
|
|
159
|
+
// ============================================================================
|
|
160
|
+
|
|
161
|
+
function pluginLoader(plugins = []) {
|
|
162
|
+
console.log('');
|
|
163
|
+
console.log(`${icons.package} ${pc.bold('Loading plugins...')}`);
|
|
164
|
+
console.log('');
|
|
165
|
+
|
|
166
|
+
if (plugins.length === 0) {
|
|
167
|
+
console.log(` ${pc.dim('No plugins configured')}`);
|
|
168
|
+
} else {
|
|
169
|
+
plugins.forEach((plugin, i) => {
|
|
170
|
+
console.log(` ${icons.check} ${pc.white(plugin.name)} ${pc.dim(`v${plugin.version || '1.0.0'}`)}`);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log('');
|
|
175
|
+
console.log(` ${pc.dim('Total plugins:')} ${pc.white(plugins.length)}`);
|
|
176
|
+
console.log('');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// HTTP Request Logger
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
function request(method, path, statusCode, duration, options = {}) {
|
|
184
|
+
const { type = 'dynamic' } = options;
|
|
185
|
+
|
|
186
|
+
// Method colors
|
|
187
|
+
const methodColors = {
|
|
188
|
+
GET: pc.green,
|
|
189
|
+
POST: pc.blue,
|
|
190
|
+
PUT: pc.yellow,
|
|
191
|
+
DELETE: pc.red,
|
|
192
|
+
PATCH: pc.magenta,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Type badges
|
|
196
|
+
const typeBadges = {
|
|
197
|
+
ssr: `${pc.yellow('[SSR]')}`,
|
|
198
|
+
ssg: `${pc.green('[SSG]')}`,
|
|
199
|
+
rsc: `${pc.magenta('[RSC]')}`,
|
|
200
|
+
island: `${pc.cyan('[ISL]')}`,
|
|
201
|
+
api: `${pc.blue('[API]')}`,
|
|
202
|
+
asset: `${pc.dim('[AST]')}`,
|
|
203
|
+
dynamic: `${pc.yellow('[SSR]')}`,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Status colors
|
|
207
|
+
const statusColor = statusCode >= 500 ? pc.red :
|
|
208
|
+
statusCode >= 400 ? pc.yellow :
|
|
209
|
+
statusCode >= 300 ? pc.cyan :
|
|
210
|
+
pc.green;
|
|
211
|
+
|
|
212
|
+
const methodColor = methodColors[method] || pc.white;
|
|
213
|
+
const badge = typeBadges[type] || typeBadges.dynamic;
|
|
214
|
+
const durationStr = duration < 100 ? pc.green(`${duration}ms`) :
|
|
215
|
+
duration < 500 ? pc.yellow(`${duration}ms`) :
|
|
216
|
+
pc.red(`${duration}ms`);
|
|
217
|
+
|
|
218
|
+
console.log(` ${methodColor(method.padEnd(6))} ${pc.white(path)}`);
|
|
219
|
+
console.log(` ${pc.dim('└─')} ${badge} ${statusColor(statusCode)} ${pc.dim('(')}${durationStr}${pc.dim(')')}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// Error & Warning Formatters
|
|
224
|
+
// ============================================================================
|
|
225
|
+
|
|
226
|
+
function error(title, message, stack = null, suggestions = []) {
|
|
227
|
+
console.log('');
|
|
228
|
+
console.log(pc.red(' ╭─────────────────────────────────────────────────────────╮'));
|
|
229
|
+
console.log(pc.red(' │') + ` ${pc.red(pc.bold('✗ ERROR'))} ` + pc.red('│'));
|
|
230
|
+
console.log(pc.red(' ├─────────────────────────────────────────────────────────┤'));
|
|
231
|
+
console.log(pc.red(' │') + ` ${pc.white(title.substring(0, 55).padEnd(55))} ` + pc.red('│'));
|
|
232
|
+
console.log(pc.red(' │') + ' '.repeat(57) + pc.red('│'));
|
|
233
|
+
|
|
234
|
+
// Message lines
|
|
235
|
+
const msgLines = message.split('\n').slice(0, 3);
|
|
236
|
+
msgLines.forEach(line => {
|
|
237
|
+
console.log(pc.red(' │') + ` ${pc.dim(line.substring(0, 55).padEnd(55))} ` + pc.red('│'));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (suggestions.length > 0) {
|
|
241
|
+
console.log(pc.red(' │') + ' '.repeat(57) + pc.red('│'));
|
|
242
|
+
console.log(pc.red(' │') + ` ${pc.cyan('Suggestions:')} ` + pc.red('│'));
|
|
243
|
+
suggestions.slice(0, 2).forEach(s => {
|
|
244
|
+
console.log(pc.red(' │') + ` ${pc.dim('→')} ${pc.white(s.substring(0, 52).padEnd(52))} ` + pc.red('│'));
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(pc.red(' ╰─────────────────────────────────────────────────────────╯'));
|
|
249
|
+
console.log('');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function warning(title, message) {
|
|
253
|
+
console.log('');
|
|
254
|
+
console.log(pc.yellow(' ⚠ WARNING: ') + pc.white(title));
|
|
255
|
+
console.log(pc.dim(` ${message}`));
|
|
256
|
+
console.log('');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function info(message) {
|
|
260
|
+
console.log(` ${icons.info} ${message}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function success(message) {
|
|
264
|
+
console.log(` ${icons.success} ${pc.green(message)}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ============================================================================
|
|
268
|
+
// Build Logger
|
|
269
|
+
// ============================================================================
|
|
270
|
+
|
|
271
|
+
function buildStart() {
|
|
272
|
+
console.log(banners.build);
|
|
273
|
+
console.log(` ${icons.rocket} ${pc.bold('Starting production build...')}`);
|
|
274
|
+
console.log('');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function buildStep(step, total, message) {
|
|
278
|
+
console.log(` ${pc.dim(`[${step}/${total}]`)} ${message}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildComplete(stats) {
|
|
282
|
+
const { duration, pages, size } = stats;
|
|
283
|
+
|
|
284
|
+
console.log('');
|
|
285
|
+
console.log(pc.green(' ╭─────────────────────────────────────────╮'));
|
|
286
|
+
console.log(pc.green(' │') + ` ${pc.green(pc.bold('✓ Build completed successfully!'))} ` + pc.green('│'));
|
|
287
|
+
console.log(pc.green(' ├─────────────────────────────────────────┤'));
|
|
288
|
+
console.log(pc.green(' │') + ` ${pc.dim('Duration:')} ${pc.white(duration + 'ms').padEnd(27)} ` + pc.green('│'));
|
|
289
|
+
console.log(pc.green(' │') + ` ${pc.dim('Pages:')} ${pc.white(pages + ' pages').padEnd(27)} ` + pc.green('│'));
|
|
290
|
+
console.log(pc.green(' │') + ` ${pc.dim('Size:')} ${pc.white(size).padEnd(27)} ` + pc.green('│'));
|
|
291
|
+
console.log(pc.green(' ╰─────────────────────────────────────────╯'));
|
|
292
|
+
console.log('');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// Dividers & Spacing
|
|
297
|
+
// ============================================================================
|
|
298
|
+
|
|
299
|
+
function divider() {
|
|
300
|
+
console.log(pc.dim(' ─────────────────────────────────────────'));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function blank() {
|
|
304
|
+
console.log('');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function clear() {
|
|
308
|
+
console.clear();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// Export
|
|
313
|
+
// ============================================================================
|
|
314
|
+
|
|
315
|
+
export const logger = {
|
|
316
|
+
// Banners
|
|
317
|
+
banner: () => console.log(banners.main),
|
|
318
|
+
bannerCompact: () => console.log(banners.compact),
|
|
319
|
+
|
|
320
|
+
// Panels
|
|
321
|
+
serverPanel,
|
|
322
|
+
pluginLoader,
|
|
323
|
+
|
|
324
|
+
// Logging
|
|
325
|
+
request,
|
|
326
|
+
error,
|
|
327
|
+
warning,
|
|
328
|
+
info,
|
|
329
|
+
success,
|
|
330
|
+
|
|
331
|
+
// Build
|
|
332
|
+
buildStart,
|
|
333
|
+
buildStep,
|
|
334
|
+
buildComplete,
|
|
335
|
+
|
|
336
|
+
// Utilities
|
|
337
|
+
divider,
|
|
338
|
+
blank,
|
|
339
|
+
clear,
|
|
340
|
+
createBox,
|
|
341
|
+
|
|
342
|
+
// Colors & Icons
|
|
343
|
+
colors,
|
|
344
|
+
icons,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
export default logger;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact Client Hydration
|
|
3
|
+
* Handles selective hydration of islands and full app hydration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hydrates a specific island component
|
|
11
|
+
*/
|
|
12
|
+
export function hydrateIsland(islandId, Component, props) {
|
|
13
|
+
const element = document.querySelector(`[data-island="${islandId}"]`);
|
|
14
|
+
|
|
15
|
+
if (!element) {
|
|
16
|
+
console.warn(`Island element not found: ${islandId}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (element.hasAttribute('data-hydrated')) {
|
|
21
|
+
return; // Already hydrated
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
hydrateRoot(element, React.createElement(Component, props));
|
|
26
|
+
element.setAttribute('data-hydrated', 'true');
|
|
27
|
+
|
|
28
|
+
// Dispatch custom event
|
|
29
|
+
element.dispatchEvent(new CustomEvent('flexi:hydrated', {
|
|
30
|
+
bubbles: true,
|
|
31
|
+
detail: { islandId, props }
|
|
32
|
+
}));
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(`Failed to hydrate island ${islandId}:`, error);
|
|
35
|
+
|
|
36
|
+
// Fallback: try full render instead of hydration
|
|
37
|
+
try {
|
|
38
|
+
createRoot(element).render(React.createElement(Component, props));
|
|
39
|
+
element.setAttribute('data-hydrated', 'true');
|
|
40
|
+
} catch (fallbackError) {
|
|
41
|
+
console.error(`Fallback render also failed for ${islandId}:`, fallbackError);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hydrates the entire application
|
|
48
|
+
*/
|
|
49
|
+
export function hydrateApp(App, props = {}) {
|
|
50
|
+
const root = document.getElementById('root');
|
|
51
|
+
|
|
52
|
+
if (!root) {
|
|
53
|
+
console.error('Root element not found');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Get server-rendered props
|
|
58
|
+
const serverProps = window.__FLEXI_DATA__?.props || {};
|
|
59
|
+
const mergedProps = { ...serverProps, ...props };
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
hydrateRoot(root, React.createElement(App, mergedProps));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Hydration failed, falling back to full render:', error);
|
|
65
|
+
createRoot(root).render(React.createElement(App, mergedProps));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hydrates all islands on the page
|
|
71
|
+
*/
|
|
72
|
+
export async function hydrateAllIslands(islandModules) {
|
|
73
|
+
const islands = document.querySelectorAll('[data-island]');
|
|
74
|
+
|
|
75
|
+
for (const element of islands) {
|
|
76
|
+
if (element.hasAttribute('data-hydrated')) continue;
|
|
77
|
+
|
|
78
|
+
const islandId = element.getAttribute('data-island');
|
|
79
|
+
const islandName = element.getAttribute('data-island-name');
|
|
80
|
+
const propsJson = element.getAttribute('data-island-props');
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const props = propsJson ? JSON.parse(propsJson) : {};
|
|
84
|
+
const module = islandModules[islandName];
|
|
85
|
+
|
|
86
|
+
if (module) {
|
|
87
|
+
hydrateIsland(islandId, module.default || module, props);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(`Failed to hydrate island ${islandName}:`, error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Progressive hydration based on visibility
|
|
97
|
+
*/
|
|
98
|
+
export function setupProgressiveHydration(islandModules) {
|
|
99
|
+
const observer = new IntersectionObserver(
|
|
100
|
+
(entries) => {
|
|
101
|
+
entries.forEach(async (entry) => {
|
|
102
|
+
if (!entry.isIntersecting) return;
|
|
103
|
+
|
|
104
|
+
const element = entry.target;
|
|
105
|
+
if (element.hasAttribute('data-hydrated')) return;
|
|
106
|
+
|
|
107
|
+
const islandName = element.getAttribute('data-island-name');
|
|
108
|
+
const islandId = element.getAttribute('data-island');
|
|
109
|
+
const propsJson = element.getAttribute('data-island-props');
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const props = propsJson ? JSON.parse(propsJson) : {};
|
|
113
|
+
const module = await islandModules[islandName]();
|
|
114
|
+
|
|
115
|
+
hydrateIsland(islandId, module.default || module, props);
|
|
116
|
+
observer.unobserve(element);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error(`Failed to hydrate island ${islandName}:`, error);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
{ rootMargin: '50px' }
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
document.querySelectorAll('[data-island]:not([data-hydrated])').forEach(el => {
|
|
126
|
+
observer.observe(el);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return observer;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default {
|
|
133
|
+
hydrateIsland,
|
|
134
|
+
hydrateApp,
|
|
135
|
+
hydrateAllIslands,
|
|
136
|
+
setupProgressiveHydration
|
|
137
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact Client Runtime
|
|
3
|
+
* Handles hydration, navigation, and client-side interactivity
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { hydrateIsland, hydrateApp } from './hydration.js';
|
|
7
|
+
export { navigate, prefetch, Link } from './navigation.js';
|
|
8
|
+
export { useIsland, IslandBoundary } from './islands.js';
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact Client Islands
|
|
3
|
+
* Client-side island utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook to check if component is hydrated
|
|
10
|
+
*/
|
|
11
|
+
export function useIsland() {
|
|
12
|
+
const [isHydrated, setIsHydrated] = React.useState(false);
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
setIsHydrated(true);
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
return { isHydrated };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Island boundary component
|
|
23
|
+
* Wraps interactive components for partial hydration
|
|
24
|
+
*/
|
|
25
|
+
export function IslandBoundary({ children, fallback = null, name = 'island' }) {
|
|
26
|
+
const { isHydrated } = useIsland();
|
|
27
|
+
|
|
28
|
+
// On server or before hydration, render children normally
|
|
29
|
+
// The server will wrap this in the island marker
|
|
30
|
+
if (!isHydrated && fallback) {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return React.createElement('div', {
|
|
35
|
+
'data-island-boundary': name,
|
|
36
|
+
children
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a lazy-loaded island
|
|
42
|
+
*/
|
|
43
|
+
export function createClientIsland(loader, options = {}) {
|
|
44
|
+
const { fallback = null, name = 'lazy-island' } = options;
|
|
45
|
+
|
|
46
|
+
return function LazyIsland(props) {
|
|
47
|
+
const [Component, setComponent] = React.useState(null);
|
|
48
|
+
const [error, setError] = React.useState(null);
|
|
49
|
+
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
loader()
|
|
52
|
+
.then(mod => setComponent(() => mod.default || mod))
|
|
53
|
+
.catch(err => setError(err));
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
if (error) {
|
|
57
|
+
return React.createElement('div', {
|
|
58
|
+
className: 'island-error',
|
|
59
|
+
children: `Failed to load ${name}`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!Component) {
|
|
64
|
+
return fallback || React.createElement('div', {
|
|
65
|
+
className: 'island-loading',
|
|
66
|
+
children: 'Loading...'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return React.createElement(Component, props);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Island with interaction trigger
|
|
76
|
+
* Only hydrates when user interacts
|
|
77
|
+
*/
|
|
78
|
+
export function InteractiveIsland({ children, trigger = 'click', fallback }) {
|
|
79
|
+
const [shouldHydrate, setShouldHydrate] = React.useState(false);
|
|
80
|
+
const ref = React.useRef(null);
|
|
81
|
+
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
const element = ref.current;
|
|
84
|
+
if (!element) return;
|
|
85
|
+
|
|
86
|
+
const handleInteraction = () => {
|
|
87
|
+
setShouldHydrate(true);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
element.addEventListener(trigger, handleInteraction, { once: true });
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
element.removeEventListener(trigger, handleInteraction);
|
|
94
|
+
};
|
|
95
|
+
}, [trigger]);
|
|
96
|
+
|
|
97
|
+
if (!shouldHydrate) {
|
|
98
|
+
return React.createElement('div', {
|
|
99
|
+
ref,
|
|
100
|
+
'data-interactive-island': 'true',
|
|
101
|
+
children: fallback || children
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return children;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Media query island
|
|
110
|
+
* Only hydrates when media query matches
|
|
111
|
+
*/
|
|
112
|
+
export function MediaIsland({ children, query, fallback }) {
|
|
113
|
+
const [matches, setMatches] = React.useState(false);
|
|
114
|
+
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
const mediaQuery = window.matchMedia(query);
|
|
117
|
+
setMatches(mediaQuery.matches);
|
|
118
|
+
|
|
119
|
+
const handler = (e) => setMatches(e.matches);
|
|
120
|
+
mediaQuery.addEventListener('change', handler);
|
|
121
|
+
|
|
122
|
+
return () => mediaQuery.removeEventListener('change', handler);
|
|
123
|
+
}, [query]);
|
|
124
|
+
|
|
125
|
+
if (!matches) {
|
|
126
|
+
return fallback || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return children;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default {
|
|
133
|
+
useIsland,
|
|
134
|
+
IslandBoundary,
|
|
135
|
+
createClientIsland,
|
|
136
|
+
InteractiveIsland,
|
|
137
|
+
MediaIsland
|
|
138
|
+
};
|