@flexireact/core 3.0.1 → 3.0.3
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/dist/cli/index.js +1514 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/client/index.js +373 -0
- package/dist/core/client/index.js.map +1 -0
- package/dist/core/index.js +6415 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/server/index.js +3094 -0
- package/dist/core/server/index.js.map +1 -0
- package/package.json +80 -80
- package/bin/flexireact.js +0 -23
- package/cli/generators.ts +0 -616
- package/cli/index.ts +0 -1182
- package/core/actions/index.ts +0 -364
- package/core/api.ts +0 -143
- package/core/build/index.ts +0 -425
- package/core/cli/logger.ts +0 -353
- package/core/client/Link.tsx +0 -345
- package/core/client/hydration.ts +0 -147
- package/core/client/index.ts +0 -12
- package/core/client/islands.ts +0 -143
- package/core/client/navigation.ts +0 -212
- package/core/client/runtime.ts +0 -52
- package/core/config.ts +0 -116
- package/core/context.ts +0 -83
- package/core/dev.ts +0 -47
- package/core/devtools/index.ts +0 -644
- package/core/edge/cache.ts +0 -344
- package/core/edge/fetch-polyfill.ts +0 -247
- package/core/edge/handler.ts +0 -248
- package/core/edge/index.ts +0 -81
- package/core/edge/ppr.ts +0 -264
- package/core/edge/runtime.ts +0 -161
- package/core/font/index.ts +0 -306
- package/core/helpers.ts +0 -494
- package/core/image/index.ts +0 -413
- package/core/index.ts +0 -218
- package/core/islands/index.ts +0 -293
- package/core/loader.ts +0 -111
- package/core/logger.ts +0 -242
- package/core/metadata/index.ts +0 -622
- package/core/middleware/index.ts +0 -416
- package/core/plugins/index.ts +0 -373
- package/core/render/index.ts +0 -1243
- package/core/render.ts +0 -136
- package/core/router/index.ts +0 -551
- package/core/router.ts +0 -141
- package/core/rsc/index.ts +0 -199
- package/core/server/index.ts +0 -779
- package/core/server.ts +0 -203
- package/core/ssg/index.ts +0 -346
- package/core/start-dev.ts +0 -6
- package/core/start-prod.ts +0 -6
- package/core/tsconfig.json +0 -30
- package/core/types.ts +0 -239
- package/core/utils.ts +0 -176
package/core/cli/logger.ts
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
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
|
-
interface BoxOptions {
|
|
69
|
-
padding?: number;
|
|
70
|
-
borderColor?: (s: string) => string;
|
|
71
|
-
width?: number;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function createBox(content: string, options: BoxOptions = {}) {
|
|
75
|
-
const {
|
|
76
|
-
padding = 1,
|
|
77
|
-
borderColor = colors.primary,
|
|
78
|
-
width = 50
|
|
79
|
-
} = options;
|
|
80
|
-
|
|
81
|
-
const lines = content.split('\n');
|
|
82
|
-
const maxLen = Math.max(...lines.map(l => stripAnsi(l).length), width - 4);
|
|
83
|
-
const innerWidth = maxLen + (padding * 2);
|
|
84
|
-
|
|
85
|
-
const top = borderColor(box.topLeft + box.horizontal.repeat(innerWidth) + box.topRight);
|
|
86
|
-
const bottom = borderColor(box.bottomLeft + box.horizontal.repeat(innerWidth) + box.bottomRight);
|
|
87
|
-
const empty = borderColor(box.vertical) + ' '.repeat(innerWidth) + borderColor(box.vertical);
|
|
88
|
-
|
|
89
|
-
const contentLines = lines.map(line => {
|
|
90
|
-
const stripped = stripAnsi(line);
|
|
91
|
-
const pad = innerWidth - stripped.length - padding;
|
|
92
|
-
return borderColor(box.vertical) + ' '.repeat(padding) + line + ' '.repeat(Math.max(0, pad)) + borderColor(box.vertical);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const paddingLines = padding > 0 ? [empty] : [];
|
|
96
|
-
|
|
97
|
-
return [top, ...paddingLines, ...contentLines, ...paddingLines, bottom].join('\n');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function stripAnsi(str) {
|
|
101
|
-
return str.replace(/\x1B\[[0-9;]*m/g, '');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ============================================================================
|
|
105
|
-
// ASCII Art Banners
|
|
106
|
-
// ============================================================================
|
|
107
|
-
|
|
108
|
-
const banners = {
|
|
109
|
-
// Main banner - Gradient style
|
|
110
|
-
main: `
|
|
111
|
-
${pc.cyan(' ╭─────────────────────────────────────────╮')}
|
|
112
|
-
${pc.cyan(' │')} ${pc.cyan('│')}
|
|
113
|
-
${pc.cyan(' │')} ${pc.bold(pc.green('⚡'))} ${pc.bold(pc.white('F L E X I R E A C T'))} ${pc.cyan('│')}
|
|
114
|
-
${pc.cyan(' │')} ${pc.cyan('│')}
|
|
115
|
-
${pc.cyan(' │')} ${pc.dim('The Modern React Framework')} ${pc.cyan('│')}
|
|
116
|
-
${pc.cyan(' │')} ${pc.cyan('│')}
|
|
117
|
-
${pc.cyan(' ╰─────────────────────────────────────────╯')}
|
|
118
|
-
`,
|
|
119
|
-
|
|
120
|
-
// Compact banner
|
|
121
|
-
compact: `
|
|
122
|
-
${pc.green('⚡')} ${pc.bold('FlexiReact')} ${pc.dim('v2.1.0')}
|
|
123
|
-
`,
|
|
124
|
-
|
|
125
|
-
// Build banner
|
|
126
|
-
build: `
|
|
127
|
-
${pc.cyan(' ╭─────────────────────────────────────────╮')}
|
|
128
|
-
${pc.cyan(' │')} ${pc.bold(pc.green('⚡'))} ${pc.bold('FlexiReact Build')} ${pc.cyan('│')}
|
|
129
|
-
${pc.cyan(' ╰─────────────────────────────────────────╯')}
|
|
130
|
-
`,
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
// ============================================================================
|
|
134
|
-
// Server Status Panel
|
|
135
|
-
// ============================================================================
|
|
136
|
-
|
|
137
|
-
function serverPanel(config) {
|
|
138
|
-
const {
|
|
139
|
-
mode = 'development',
|
|
140
|
-
port = 3000,
|
|
141
|
-
host = 'localhost',
|
|
142
|
-
pagesDir = './pages',
|
|
143
|
-
islands = true,
|
|
144
|
-
rsc = true,
|
|
145
|
-
} = config;
|
|
146
|
-
|
|
147
|
-
const modeColor = mode === 'development' ? colors.warning : colors.success;
|
|
148
|
-
const url = `http://${host}:${port}`;
|
|
149
|
-
|
|
150
|
-
console.log('');
|
|
151
|
-
console.log(pc.dim(' ─────────────────────────────────────────'));
|
|
152
|
-
console.log(` ${icons.arrow} ${pc.dim('Mode:')} ${modeColor(mode)}`);
|
|
153
|
-
console.log(` ${icons.arrow} ${pc.dim('Local:')} ${pc.cyan(pc.underline(url))}`);
|
|
154
|
-
console.log(` ${icons.arrow} ${pc.dim('Pages:')} ${pc.white(pagesDir)}`);
|
|
155
|
-
console.log(` ${icons.arrow} ${pc.dim('Islands:')} ${islands ? pc.green('enabled') : pc.dim('disabled')}`);
|
|
156
|
-
console.log(` ${icons.arrow} ${pc.dim('RSC:')} ${rsc ? pc.green('enabled') : pc.dim('disabled')}`);
|
|
157
|
-
console.log(pc.dim(' ─────────────────────────────────────────'));
|
|
158
|
-
console.log('');
|
|
159
|
-
console.log(` ${icons.success} ${pc.green('Ready for requests...')}`);
|
|
160
|
-
console.log('');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ============================================================================
|
|
164
|
-
// Plugin Loader
|
|
165
|
-
// ============================================================================
|
|
166
|
-
|
|
167
|
-
function pluginLoader(plugins = []) {
|
|
168
|
-
console.log('');
|
|
169
|
-
console.log(`${icons.package} ${pc.bold('Loading plugins...')}`);
|
|
170
|
-
console.log('');
|
|
171
|
-
|
|
172
|
-
if (plugins.length === 0) {
|
|
173
|
-
console.log(` ${pc.dim('No plugins configured')}`);
|
|
174
|
-
} else {
|
|
175
|
-
plugins.forEach((plugin, i) => {
|
|
176
|
-
console.log(` ${icons.check} ${pc.white(plugin.name)} ${pc.dim(`v${plugin.version || '1.0.0'}`)}`);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
console.log('');
|
|
181
|
-
console.log(` ${pc.dim('Total plugins:')} ${pc.white(plugins.length)}`);
|
|
182
|
-
console.log('');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ============================================================================
|
|
186
|
-
// HTTP Request Logger
|
|
187
|
-
// ============================================================================
|
|
188
|
-
|
|
189
|
-
function request(method: string, path: string, statusCode: number, duration: number, options: { type?: string } = {}) {
|
|
190
|
-
const { type = 'dynamic' } = options;
|
|
191
|
-
|
|
192
|
-
// Method colors
|
|
193
|
-
const methodColors = {
|
|
194
|
-
GET: pc.green,
|
|
195
|
-
POST: pc.blue,
|
|
196
|
-
PUT: pc.yellow,
|
|
197
|
-
DELETE: pc.red,
|
|
198
|
-
PATCH: pc.magenta,
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
// Type badges
|
|
202
|
-
const typeBadges = {
|
|
203
|
-
ssr: `${pc.yellow('[SSR]')}`,
|
|
204
|
-
ssg: `${pc.green('[SSG]')}`,
|
|
205
|
-
rsc: `${pc.magenta('[RSC]')}`,
|
|
206
|
-
island: `${pc.cyan('[ISL]')}`,
|
|
207
|
-
api: `${pc.blue('[API]')}`,
|
|
208
|
-
asset: `${pc.dim('[AST]')}`,
|
|
209
|
-
dynamic: `${pc.yellow('[SSR]')}`,
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// Status colors
|
|
213
|
-
const statusColor = statusCode >= 500 ? pc.red :
|
|
214
|
-
statusCode >= 400 ? pc.yellow :
|
|
215
|
-
statusCode >= 300 ? pc.cyan :
|
|
216
|
-
pc.green;
|
|
217
|
-
|
|
218
|
-
const methodColor = methodColors[method] || pc.white;
|
|
219
|
-
const badge = typeBadges[type] || typeBadges.dynamic;
|
|
220
|
-
const durationStr = duration < 100 ? pc.green(`${duration}ms`) :
|
|
221
|
-
duration < 500 ? pc.yellow(`${duration}ms`) :
|
|
222
|
-
pc.red(`${duration}ms`);
|
|
223
|
-
|
|
224
|
-
console.log(` ${methodColor(method.padEnd(6))} ${pc.white(path)}`);
|
|
225
|
-
console.log(` ${pc.dim('└─')} ${badge} ${statusColor(statusCode)} ${pc.dim('(')}${durationStr}${pc.dim(')')}`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ============================================================================
|
|
229
|
-
// Error & Warning Formatters
|
|
230
|
-
// ============================================================================
|
|
231
|
-
|
|
232
|
-
function error(title, message, stack = null, suggestions = []) {
|
|
233
|
-
console.log('');
|
|
234
|
-
console.log(pc.red(' ╭─────────────────────────────────────────────────────────╮'));
|
|
235
|
-
console.log(pc.red(' │') + ` ${pc.red(pc.bold('✗ ERROR'))} ` + pc.red('│'));
|
|
236
|
-
console.log(pc.red(' ├─────────────────────────────────────────────────────────┤'));
|
|
237
|
-
console.log(pc.red(' │') + ` ${pc.white(title.substring(0, 55).padEnd(55))} ` + pc.red('│'));
|
|
238
|
-
console.log(pc.red(' │') + ' '.repeat(57) + pc.red('│'));
|
|
239
|
-
|
|
240
|
-
// Message lines
|
|
241
|
-
const msgLines = message.split('\n').slice(0, 3);
|
|
242
|
-
msgLines.forEach(line => {
|
|
243
|
-
console.log(pc.red(' │') + ` ${pc.dim(line.substring(0, 55).padEnd(55))} ` + pc.red('│'));
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
if (suggestions.length > 0) {
|
|
247
|
-
console.log(pc.red(' │') + ' '.repeat(57) + pc.red('│'));
|
|
248
|
-
console.log(pc.red(' │') + ` ${pc.cyan('Suggestions:')} ` + pc.red('│'));
|
|
249
|
-
suggestions.slice(0, 2).forEach(s => {
|
|
250
|
-
console.log(pc.red(' │') + ` ${pc.dim('→')} ${pc.white(s.substring(0, 52).padEnd(52))} ` + pc.red('│'));
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
console.log(pc.red(' ╰─────────────────────────────────────────────────────────╯'));
|
|
255
|
-
console.log('');
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function warning(title, message) {
|
|
259
|
-
console.log('');
|
|
260
|
-
console.log(pc.yellow(' ⚠ WARNING: ') + pc.white(title));
|
|
261
|
-
console.log(pc.dim(` ${message}`));
|
|
262
|
-
console.log('');
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function info(message) {
|
|
266
|
-
console.log(` ${icons.info} ${message}`);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function success(message) {
|
|
270
|
-
console.log(` ${icons.success} ${pc.green(message)}`);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// ============================================================================
|
|
274
|
-
// Build Logger
|
|
275
|
-
// ============================================================================
|
|
276
|
-
|
|
277
|
-
function buildStart() {
|
|
278
|
-
console.log(banners.build);
|
|
279
|
-
console.log(` ${icons.rocket} ${pc.bold('Starting production build...')}`);
|
|
280
|
-
console.log('');
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function buildStep(step, total, message) {
|
|
284
|
-
console.log(` ${pc.dim(`[${step}/${total}]`)} ${message}`);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function buildComplete(stats) {
|
|
288
|
-
const { duration, pages, size } = stats;
|
|
289
|
-
|
|
290
|
-
console.log('');
|
|
291
|
-
console.log(pc.green(' ╭─────────────────────────────────────────╮'));
|
|
292
|
-
console.log(pc.green(' │') + ` ${pc.green(pc.bold('✓ Build completed successfully!'))} ` + pc.green('│'));
|
|
293
|
-
console.log(pc.green(' ├─────────────────────────────────────────┤'));
|
|
294
|
-
console.log(pc.green(' │') + ` ${pc.dim('Duration:')} ${pc.white(duration + 'ms').padEnd(27)} ` + pc.green('│'));
|
|
295
|
-
console.log(pc.green(' │') + ` ${pc.dim('Pages:')} ${pc.white(pages + ' pages').padEnd(27)} ` + pc.green('│'));
|
|
296
|
-
console.log(pc.green(' │') + ` ${pc.dim('Size:')} ${pc.white(size).padEnd(27)} ` + pc.green('│'));
|
|
297
|
-
console.log(pc.green(' ╰─────────────────────────────────────────╯'));
|
|
298
|
-
console.log('');
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// ============================================================================
|
|
302
|
-
// Dividers & Spacing
|
|
303
|
-
// ============================================================================
|
|
304
|
-
|
|
305
|
-
function divider() {
|
|
306
|
-
console.log(pc.dim(' ─────────────────────────────────────────'));
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function blank() {
|
|
310
|
-
console.log('');
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function clear() {
|
|
314
|
-
console.clear();
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// ============================================================================
|
|
318
|
-
// Export
|
|
319
|
-
// ============================================================================
|
|
320
|
-
|
|
321
|
-
export const logger = {
|
|
322
|
-
// Banners
|
|
323
|
-
banner: () => console.log(banners.main),
|
|
324
|
-
bannerCompact: () => console.log(banners.compact),
|
|
325
|
-
|
|
326
|
-
// Panels
|
|
327
|
-
serverPanel,
|
|
328
|
-
pluginLoader,
|
|
329
|
-
|
|
330
|
-
// Logging
|
|
331
|
-
request,
|
|
332
|
-
error,
|
|
333
|
-
warning,
|
|
334
|
-
info,
|
|
335
|
-
success,
|
|
336
|
-
|
|
337
|
-
// Build
|
|
338
|
-
buildStart,
|
|
339
|
-
buildStep,
|
|
340
|
-
buildComplete,
|
|
341
|
-
|
|
342
|
-
// Utilities
|
|
343
|
-
divider,
|
|
344
|
-
blank,
|
|
345
|
-
clear,
|
|
346
|
-
createBox,
|
|
347
|
-
|
|
348
|
-
// Colors & Icons
|
|
349
|
-
colors,
|
|
350
|
-
icons,
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
export default logger;
|
package/core/client/Link.tsx
DELETED
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* FlexiReact Link Component
|
|
5
|
-
* Enhanced link with prefetching, client-side navigation, and loading states
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
9
|
-
|
|
10
|
-
export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
|
11
|
-
/** The URL to navigate to */
|
|
12
|
-
href: string;
|
|
13
|
-
/** Prefetch the page on hover/visibility */
|
|
14
|
-
prefetch?: boolean | 'hover' | 'viewport';
|
|
15
|
-
/** Replace the current history entry instead of pushing */
|
|
16
|
-
replace?: boolean;
|
|
17
|
-
/** Scroll to top after navigation */
|
|
18
|
-
scroll?: boolean;
|
|
19
|
-
/** Show loading indicator while navigating */
|
|
20
|
-
showLoading?: boolean;
|
|
21
|
-
/** Custom loading component */
|
|
22
|
-
loadingComponent?: React.ReactNode;
|
|
23
|
-
/** Callback when navigation starts */
|
|
24
|
-
onNavigationStart?: () => void;
|
|
25
|
-
/** Callback when navigation ends */
|
|
26
|
-
onNavigationEnd?: () => void;
|
|
27
|
-
/** Children */
|
|
28
|
-
children: React.ReactNode;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Prefetch cache to avoid duplicate requests
|
|
32
|
-
const prefetchCache = new Set<string>();
|
|
33
|
-
|
|
34
|
-
// Prefetch a URL
|
|
35
|
-
async function prefetchUrl(url: string): Promise<void> {
|
|
36
|
-
if (prefetchCache.has(url)) return;
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
// Mark as prefetched immediately to prevent duplicate requests
|
|
40
|
-
prefetchCache.add(url);
|
|
41
|
-
|
|
42
|
-
// Use link preload for better browser optimization
|
|
43
|
-
const link = document.createElement('link');
|
|
44
|
-
link.rel = 'prefetch';
|
|
45
|
-
link.href = url;
|
|
46
|
-
link.as = 'document';
|
|
47
|
-
document.head.appendChild(link);
|
|
48
|
-
|
|
49
|
-
// Also fetch the page to warm the cache
|
|
50
|
-
const controller = new AbortController();
|
|
51
|
-
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
52
|
-
|
|
53
|
-
await fetch(url, {
|
|
54
|
-
method: 'GET',
|
|
55
|
-
credentials: 'same-origin',
|
|
56
|
-
signal: controller.signal,
|
|
57
|
-
headers: {
|
|
58
|
-
'X-Flexi-Prefetch': '1',
|
|
59
|
-
'Accept': 'text/html'
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
clearTimeout(timeoutId);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
// Remove from cache on error so it can be retried
|
|
66
|
-
prefetchCache.delete(url);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Check if URL is internal
|
|
71
|
-
function isInternalUrl(url: string): boolean {
|
|
72
|
-
if (url.startsWith('/')) return true;
|
|
73
|
-
if (url.startsWith('#')) return true;
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const parsed = new URL(url, window.location.origin);
|
|
77
|
-
return parsed.origin === window.location.origin;
|
|
78
|
-
} catch {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Navigate to a URL
|
|
84
|
-
function navigate(url: string, options: { replace?: boolean; scroll?: boolean } = {}): void {
|
|
85
|
-
const { replace = false, scroll = true } = options;
|
|
86
|
-
|
|
87
|
-
if (replace) {
|
|
88
|
-
window.history.replaceState({}, '', url);
|
|
89
|
-
} else {
|
|
90
|
-
window.history.pushState({}, '', url);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Dispatch popstate event to trigger any listeners
|
|
94
|
-
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
|
|
95
|
-
|
|
96
|
-
// Scroll to top if requested
|
|
97
|
-
if (scroll) {
|
|
98
|
-
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Link component with prefetching and client-side navigation
|
|
104
|
-
*
|
|
105
|
-
* @example
|
|
106
|
-
* ```tsx
|
|
107
|
-
* import { Link } from '@flexireact/core/client';
|
|
108
|
-
*
|
|
109
|
-
* // Basic usage
|
|
110
|
-
* <Link href="/about">About</Link>
|
|
111
|
-
*
|
|
112
|
-
* // With prefetch on hover
|
|
113
|
-
* <Link href="/products" prefetch="hover">Products</Link>
|
|
114
|
-
*
|
|
115
|
-
* // With prefetch on viewport visibility
|
|
116
|
-
* <Link href="/contact" prefetch="viewport">Contact</Link>
|
|
117
|
-
*
|
|
118
|
-
* // Replace history instead of push
|
|
119
|
-
* <Link href="/login" replace>Login</Link>
|
|
120
|
-
*
|
|
121
|
-
* // Disable scroll to top
|
|
122
|
-
* <Link href="/section#anchor" scroll={false}>Go to section</Link>
|
|
123
|
-
* ```
|
|
124
|
-
*/
|
|
125
|
-
export function Link({
|
|
126
|
-
href,
|
|
127
|
-
prefetch = true,
|
|
128
|
-
replace = false,
|
|
129
|
-
scroll = true,
|
|
130
|
-
showLoading = false,
|
|
131
|
-
loadingComponent,
|
|
132
|
-
onNavigationStart,
|
|
133
|
-
onNavigationEnd,
|
|
134
|
-
children,
|
|
135
|
-
className,
|
|
136
|
-
onClick,
|
|
137
|
-
onMouseEnter,
|
|
138
|
-
onFocus,
|
|
139
|
-
...props
|
|
140
|
-
}: LinkProps) {
|
|
141
|
-
const [isNavigating, setIsNavigating] = useState(false);
|
|
142
|
-
const linkRef = useRef<HTMLAnchorElement>(null);
|
|
143
|
-
const hasPrefetched = useRef(false);
|
|
144
|
-
|
|
145
|
-
// Prefetch on viewport visibility
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
if (prefetch !== 'viewport' && prefetch !== true) return;
|
|
148
|
-
if (!isInternalUrl(href)) return;
|
|
149
|
-
if (hasPrefetched.current) return;
|
|
150
|
-
|
|
151
|
-
const observer = new IntersectionObserver(
|
|
152
|
-
(entries) => {
|
|
153
|
-
entries.forEach((entry) => {
|
|
154
|
-
if (entry.isIntersecting) {
|
|
155
|
-
prefetchUrl(href);
|
|
156
|
-
hasPrefetched.current = true;
|
|
157
|
-
observer.disconnect();
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
},
|
|
161
|
-
{ rootMargin: '200px' }
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
if (linkRef.current) {
|
|
165
|
-
observer.observe(linkRef.current);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return () => observer.disconnect();
|
|
169
|
-
}, [href, prefetch]);
|
|
170
|
-
|
|
171
|
-
// Handle hover prefetch
|
|
172
|
-
const handleMouseEnter = useCallback(
|
|
173
|
-
(e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
174
|
-
onMouseEnter?.(e);
|
|
175
|
-
|
|
176
|
-
if ((prefetch === 'hover' || prefetch === true) && isInternalUrl(href)) {
|
|
177
|
-
prefetchUrl(href);
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
[href, prefetch, onMouseEnter]
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
// Handle focus prefetch (for keyboard navigation)
|
|
184
|
-
const handleFocus = useCallback(
|
|
185
|
-
(e: React.FocusEvent<HTMLAnchorElement>) => {
|
|
186
|
-
onFocus?.(e);
|
|
187
|
-
|
|
188
|
-
if ((prefetch === 'hover' || prefetch === true) && isInternalUrl(href)) {
|
|
189
|
-
prefetchUrl(href);
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
[href, prefetch, onFocus]
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
// Handle click for client-side navigation
|
|
196
|
-
const handleClick = useCallback(
|
|
197
|
-
async (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
198
|
-
onClick?.(e);
|
|
199
|
-
|
|
200
|
-
// Don't handle if default was prevented
|
|
201
|
-
if (e.defaultPrevented) return;
|
|
202
|
-
|
|
203
|
-
// Don't handle if modifier keys are pressed (open in new tab, etc.)
|
|
204
|
-
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
205
|
-
|
|
206
|
-
// Don't handle external URLs
|
|
207
|
-
if (!isInternalUrl(href)) return;
|
|
208
|
-
|
|
209
|
-
// Don't handle if target is set
|
|
210
|
-
if (props.target && props.target !== '_self') return;
|
|
211
|
-
|
|
212
|
-
// Prevent default navigation
|
|
213
|
-
e.preventDefault();
|
|
214
|
-
|
|
215
|
-
// Start navigation
|
|
216
|
-
setIsNavigating(true);
|
|
217
|
-
onNavigationStart?.();
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
// Fetch the new page
|
|
221
|
-
const response = await fetch(href, {
|
|
222
|
-
method: 'GET',
|
|
223
|
-
credentials: 'same-origin',
|
|
224
|
-
headers: {
|
|
225
|
-
'X-Flexi-Navigation': '1',
|
|
226
|
-
'Accept': 'text/html'
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
if (response.ok) {
|
|
231
|
-
const html = await response.text();
|
|
232
|
-
|
|
233
|
-
// Parse and update the page
|
|
234
|
-
const parser = new DOMParser();
|
|
235
|
-
const doc = parser.parseFromString(html, 'text/html');
|
|
236
|
-
|
|
237
|
-
// Update title
|
|
238
|
-
const newTitle = doc.querySelector('title')?.textContent;
|
|
239
|
-
if (newTitle) {
|
|
240
|
-
document.title = newTitle;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Update body content (or specific container)
|
|
244
|
-
const newContent = doc.querySelector('#root') || doc.body;
|
|
245
|
-
const currentContent = document.querySelector('#root') || document.body;
|
|
246
|
-
|
|
247
|
-
if (newContent && currentContent) {
|
|
248
|
-
currentContent.innerHTML = newContent.innerHTML;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Update URL
|
|
252
|
-
navigate(href, { replace, scroll });
|
|
253
|
-
} else {
|
|
254
|
-
// Fallback to regular navigation on error
|
|
255
|
-
window.location.href = href;
|
|
256
|
-
}
|
|
257
|
-
} catch (error) {
|
|
258
|
-
// Fallback to regular navigation on error
|
|
259
|
-
window.location.href = href;
|
|
260
|
-
} finally {
|
|
261
|
-
setIsNavigating(false);
|
|
262
|
-
onNavigationEnd?.();
|
|
263
|
-
}
|
|
264
|
-
},
|
|
265
|
-
[href, replace, scroll, onClick, onNavigationStart, onNavigationEnd, props.target]
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
return (
|
|
269
|
-
<a
|
|
270
|
-
ref={linkRef}
|
|
271
|
-
href={href}
|
|
272
|
-
className={className}
|
|
273
|
-
onClick={handleClick}
|
|
274
|
-
onMouseEnter={handleMouseEnter}
|
|
275
|
-
onFocus={handleFocus}
|
|
276
|
-
data-prefetch={prefetch}
|
|
277
|
-
data-navigating={isNavigating || undefined}
|
|
278
|
-
{...props}
|
|
279
|
-
>
|
|
280
|
-
{showLoading && isNavigating ? (
|
|
281
|
-
loadingComponent || (
|
|
282
|
-
<span className="flexi-link-loading">
|
|
283
|
-
<span className="flexi-link-spinner" />
|
|
284
|
-
{children}
|
|
285
|
-
</span>
|
|
286
|
-
)
|
|
287
|
-
) : (
|
|
288
|
-
children
|
|
289
|
-
)}
|
|
290
|
-
</a>
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Programmatic navigation function
|
|
296
|
-
*
|
|
297
|
-
* @example
|
|
298
|
-
* ```tsx
|
|
299
|
-
* import { useRouter } from '@flexireact/core/client';
|
|
300
|
-
*
|
|
301
|
-
* function MyComponent() {
|
|
302
|
-
* const router = useRouter();
|
|
303
|
-
*
|
|
304
|
-
* const handleClick = () => {
|
|
305
|
-
* router.push('/dashboard');
|
|
306
|
-
* };
|
|
307
|
-
*
|
|
308
|
-
* return <button onClick={handleClick}>Go to Dashboard</button>;
|
|
309
|
-
* }
|
|
310
|
-
* ```
|
|
311
|
-
*/
|
|
312
|
-
export function useRouter() {
|
|
313
|
-
return {
|
|
314
|
-
push(url: string, options?: { scroll?: boolean }) {
|
|
315
|
-
navigate(url, { replace: false, scroll: options?.scroll ?? true });
|
|
316
|
-
// Trigger page reload for now (full SPA navigation requires more work)
|
|
317
|
-
window.location.href = url;
|
|
318
|
-
},
|
|
319
|
-
|
|
320
|
-
replace(url: string, options?: { scroll?: boolean }) {
|
|
321
|
-
navigate(url, { replace: true, scroll: options?.scroll ?? true });
|
|
322
|
-
window.location.href = url;
|
|
323
|
-
},
|
|
324
|
-
|
|
325
|
-
back() {
|
|
326
|
-
window.history.back();
|
|
327
|
-
},
|
|
328
|
-
|
|
329
|
-
forward() {
|
|
330
|
-
window.history.forward();
|
|
331
|
-
},
|
|
332
|
-
|
|
333
|
-
prefetch(url: string) {
|
|
334
|
-
if (isInternalUrl(url)) {
|
|
335
|
-
prefetchUrl(url);
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
refresh() {
|
|
340
|
-
window.location.reload();
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
export default Link;
|