@fynixorg/ui 1.0.12 → 1.0.14
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/hooks/nixAsync.d.ts +7 -1
- package/dist/hooks/nixAsync.d.ts.map +1 -1
- package/dist/hooks/nixAsync.js +84 -10
- package/dist/hooks/nixAsync.js.map +2 -2
- package/dist/hooks/nixAsyncCache.d.ts +6 -1
- package/dist/hooks/nixAsyncCache.d.ts.map +1 -1
- package/dist/hooks/nixAsyncCache.js +104 -26
- package/dist/hooks/nixAsyncCache.js.map +3 -3
- package/dist/hooks/nixComputed.d.ts.map +1 -1
- package/dist/hooks/nixComputed.js +4 -1
- package/dist/hooks/nixComputed.js.map +2 -2
- package/dist/hooks/nixEffect.d.ts.map +1 -1
- package/dist/hooks/nixEffect.js.map +2 -2
- package/dist/hooks/nixLocalStorage.d.ts +4 -1
- package/dist/hooks/nixLocalStorage.d.ts.map +1 -1
- package/dist/hooks/nixLocalStorage.js +118 -8
- package/dist/hooks/nixLocalStorage.js.map +2 -2
- package/dist/hooks/nixStore.d.ts +3 -0
- package/dist/hooks/nixStore.d.ts.map +1 -1
- package/dist/hooks/nixStore.js +57 -4
- package/dist/hooks/nixStore.js.map +2 -2
- package/dist/package.json +1 -1
- package/dist/plugins/vite-plugin-res.d.ts.map +1 -1
- package/dist/plugins/vite-plugin-res.js +239 -47
- package/dist/plugins/vite-plugin-res.js.map +3 -3
- package/dist/router/router.d.ts +13 -0
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +329 -21
- package/dist/router/router.js.map +2 -2
- package/dist/runtime.d.ts +62 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +489 -13
- package/dist/runtime.js.map +2 -2
- package/package.json +1 -1
package/dist/router/router.js
CHANGED
|
@@ -19,10 +19,71 @@ function escapeHTML(str) {
|
|
|
19
19
|
.replace(/"/g, """)
|
|
20
20
|
.replace(/'/g, "'")
|
|
21
21
|
.replace(/`/g, "`")
|
|
22
|
-
.replace(/\//g, "/")
|
|
22
|
+
.replace(/\//g, "/")
|
|
23
|
+
.replace(/=/g, "=")
|
|
24
|
+
.replace(/\(/g, "(")
|
|
25
|
+
.replace(/\)/g, ")")
|
|
26
|
+
.replace(/\{/g, "{")
|
|
27
|
+
.replace(/\}/g, "}")
|
|
28
|
+
.replace(/\[/g, "[")
|
|
29
|
+
.replace(/\]/g, "]");
|
|
30
|
+
}
|
|
31
|
+
function sanitizeContent(content) {
|
|
32
|
+
return content
|
|
33
|
+
.replace(/<script[^>]*>.*?<\/script>/gis, "")
|
|
34
|
+
.replace(/<iframe[^>]*>.*?<\/iframe>/gis, "")
|
|
35
|
+
.replace(/<object[^>]*>.*?<\/object>/gis, "")
|
|
36
|
+
.replace(/<embed[^>]*>/gi, "")
|
|
37
|
+
.replace(/<link[^>]*>/gi, "")
|
|
38
|
+
.replace(/on\w+\s*=/gi, "")
|
|
39
|
+
.replace(/javascript:/gi, "")
|
|
40
|
+
.replace(/vbscript:/gi, "")
|
|
41
|
+
.replace(/data:/gi, "")
|
|
42
|
+
.replace(/expression\s*\(/gi, "");
|
|
43
|
+
}
|
|
44
|
+
function sanitizeProps(props) {
|
|
45
|
+
const sanitized = {};
|
|
46
|
+
for (const [key, value] of Object.entries(props)) {
|
|
47
|
+
if (typeof key !== "string" ||
|
|
48
|
+
key.startsWith("__") ||
|
|
49
|
+
key.includes("javascript") ||
|
|
50
|
+
key.includes("on")) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === "string") {
|
|
54
|
+
const cleanContent = sanitizeContent(value);
|
|
55
|
+
sanitized[key] = escapeHTML(cleanContent);
|
|
56
|
+
}
|
|
57
|
+
else if (typeof value === "object" && value !== null) {
|
|
58
|
+
if (Object.keys(value).length < 50) {
|
|
59
|
+
sanitized[key] = sanitizeProps(value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (typeof value === "number" || typeof value === "boolean") {
|
|
63
|
+
sanitized[key] = value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return sanitized;
|
|
23
67
|
}
|
|
24
68
|
function isValidURL(url) {
|
|
25
69
|
try {
|
|
70
|
+
const suspiciousPatterns = [
|
|
71
|
+
/javascript:/i,
|
|
72
|
+
/vbscript:/i,
|
|
73
|
+
/data:/i,
|
|
74
|
+
/mailto:/i,
|
|
75
|
+
/tel:/i,
|
|
76
|
+
/ftp:/i,
|
|
77
|
+
/file:/i,
|
|
78
|
+
/%2f%2f/i,
|
|
79
|
+
/%5c%5c/i,
|
|
80
|
+
/\\\\/,
|
|
81
|
+
/@/,
|
|
82
|
+
];
|
|
83
|
+
if (suspiciousPatterns.some((pattern) => pattern.test(url))) {
|
|
84
|
+
console.warn("[Router] Security: Suspicious URL pattern blocked");
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
26
87
|
const parsed = new URL(url, window.location.origin);
|
|
27
88
|
if (parsed.origin !== window.location.origin) {
|
|
28
89
|
console.warn("[Router] Security: Cross-origin navigation blocked");
|
|
@@ -32,6 +93,15 @@ function isValidURL(url) {
|
|
|
32
93
|
console.warn("[Router] Security: Dangerous protocol blocked:", parsed.protocol);
|
|
33
94
|
return false;
|
|
34
95
|
}
|
|
96
|
+
const decodedPath = decodeURIComponent(parsed.pathname);
|
|
97
|
+
if (decodedPath !== parsed.pathname && /[<>"'`]/.test(decodedPath)) {
|
|
98
|
+
console.warn("[Router] Security: Encoded XSS attempt blocked");
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (url.length > 2048) {
|
|
102
|
+
console.warn("[Router] Security: Excessively long URL blocked");
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
35
105
|
return true;
|
|
36
106
|
}
|
|
37
107
|
catch (e) {
|
|
@@ -66,19 +136,15 @@ function sanitizePath(path) {
|
|
|
66
136
|
}
|
|
67
137
|
function tryGlobPaths() {
|
|
68
138
|
try {
|
|
69
|
-
let modules = import.meta.glob(
|
|
70
|
-
|
|
139
|
+
let modules = import.meta.glob("/src/**/*.{fnx,tsx,jsx,ts,js}", {
|
|
140
|
+
eager: true,
|
|
141
|
+
});
|
|
71
142
|
if (Object.keys(modules).length === 0) {
|
|
72
|
-
modules = import.meta.glob(["
|
|
73
|
-
console.log("[Router] Glob attempt 2 (../**):", Object.keys(modules));
|
|
143
|
+
modules = import.meta.glob(["./**/*.fnx", "./**/*.tsx", "./**/*.jsx", "./**/*.ts", "./**/*.js"], { eager: true });
|
|
74
144
|
}
|
|
75
145
|
if (Object.keys(modules).length === 0) {
|
|
76
|
-
modules = import.meta.glob("
|
|
77
|
-
eager: true,
|
|
78
|
-
});
|
|
79
|
-
console.log("[Router] Glob attempt 3 (/src/**):", Object.keys(modules));
|
|
146
|
+
modules = import.meta.glob(["../**/*.fnx", "../**/*.tsx", "../**/*.jsx"], { eager: true });
|
|
80
147
|
}
|
|
81
|
-
console.log("[Router] Final modules loaded:", Object.keys(modules).length);
|
|
82
148
|
return modules || {};
|
|
83
149
|
}
|
|
84
150
|
catch (error) {
|
|
@@ -167,13 +233,14 @@ function updateMetaTags(meta = {}) {
|
|
|
167
233
|
if (!meta || typeof meta !== "object")
|
|
168
234
|
return;
|
|
169
235
|
if (meta.title && typeof meta.title === "string") {
|
|
170
|
-
|
|
236
|
+
const sanitizedTitle = escapeHTML(meta.title).substring(0, 60);
|
|
237
|
+
document.title = sanitizedTitle;
|
|
171
238
|
}
|
|
172
239
|
MANAGED_META.forEach((def) => {
|
|
173
240
|
const value = meta[def.key];
|
|
174
241
|
const selector = def.name
|
|
175
|
-
? `meta[name="${def.name}"]`
|
|
176
|
-
: `meta[property="${def.property}"]`;
|
|
242
|
+
? `meta[name="${CSS.escape(def.name)}"]`
|
|
243
|
+
: `meta[property="${CSS.escape(def.property || "")}"]`;
|
|
177
244
|
let el = document.querySelector(selector);
|
|
178
245
|
if (value == null) {
|
|
179
246
|
if (el)
|
|
@@ -182,6 +249,12 @@ function updateMetaTags(meta = {}) {
|
|
|
182
249
|
}
|
|
183
250
|
if (typeof value !== "string")
|
|
184
251
|
return;
|
|
252
|
+
const cleanValue = sanitizeContent(value);
|
|
253
|
+
const sanitizedValue = escapeHTML(cleanValue).substring(0, 300);
|
|
254
|
+
if (/javascript:|vbscript:|data:|<|>/i.test(sanitizedValue)) {
|
|
255
|
+
console.warn(`[Router] Security: Blocked suspicious meta content for ${def.key}`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
185
258
|
if (!el) {
|
|
186
259
|
el = document.createElement("meta");
|
|
187
260
|
if (def.name)
|
|
@@ -190,9 +263,177 @@ function updateMetaTags(meta = {}) {
|
|
|
190
263
|
el.setAttribute("property", def.property);
|
|
191
264
|
document.head.appendChild(el);
|
|
192
265
|
}
|
|
193
|
-
el.setAttribute("content",
|
|
266
|
+
el.setAttribute("content", sanitizedValue);
|
|
194
267
|
});
|
|
195
268
|
}
|
|
269
|
+
class EnterpriseRouter {
|
|
270
|
+
constructor() {
|
|
271
|
+
this.routeCache = new Map();
|
|
272
|
+
this.preloadQueue = new Set();
|
|
273
|
+
this.routeMatchCache = new Map();
|
|
274
|
+
this.routes = {};
|
|
275
|
+
}
|
|
276
|
+
setRoutes(routes) {
|
|
277
|
+
if (!routes || typeof routes !== "object") {
|
|
278
|
+
console.warn("[EnterpriseRouter] Invalid routes configuration");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
this.routes = routes;
|
|
282
|
+
}
|
|
283
|
+
async preloadRoute(path) {
|
|
284
|
+
if (this.routeCache.has(path))
|
|
285
|
+
return;
|
|
286
|
+
const route = this.routes[path];
|
|
287
|
+
if (route?.component) {
|
|
288
|
+
const loadRoute = async () => {
|
|
289
|
+
try {
|
|
290
|
+
const component = await route.component();
|
|
291
|
+
this.routeCache.set(path, component);
|
|
292
|
+
route.prefetch?.forEach((prefetchPath) => {
|
|
293
|
+
this.preloadQueue.add(prefetchPath);
|
|
294
|
+
});
|
|
295
|
+
console.log(`[EnterpriseRouter] Preloaded route: ${path}`);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
console.warn(`[EnterpriseRouter] Failed to preload route ${path}:`, error);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
if ("requestIdleCallback" in window) {
|
|
302
|
+
requestIdleCallback(loadRoute);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
setTimeout(loadRoute, 0);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
matchRoute(path) {
|
|
310
|
+
const cached = this.routeMatchCache.get(path);
|
|
311
|
+
if (cached !== undefined)
|
|
312
|
+
return cached;
|
|
313
|
+
const match = this.computeRouteMatch(path);
|
|
314
|
+
if (this.routeMatchCache.size > 100) {
|
|
315
|
+
const firstKey = this.routeMatchCache.keys().next().value;
|
|
316
|
+
if (firstKey !== undefined) {
|
|
317
|
+
this.routeMatchCache.delete(firstKey);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
this.routeMatchCache.set(path, match);
|
|
321
|
+
return match;
|
|
322
|
+
}
|
|
323
|
+
computeRouteMatch(path) {
|
|
324
|
+
const segments = path.split("/").filter(Boolean);
|
|
325
|
+
for (const [routePath, routeConfig] of Object.entries(this.routes)) {
|
|
326
|
+
const routeSegments = routePath.split("/").filter(Boolean);
|
|
327
|
+
if (segments.length !== routeSegments.length)
|
|
328
|
+
continue;
|
|
329
|
+
const params = {};
|
|
330
|
+
let isMatch = true;
|
|
331
|
+
for (let i = 0; i < segments.length; i++) {
|
|
332
|
+
const segment = segments[i];
|
|
333
|
+
const routeSegment = routeSegments[i];
|
|
334
|
+
if (routeSegment && segment) {
|
|
335
|
+
if (routeSegment.startsWith(":")) {
|
|
336
|
+
params[routeSegment.slice(1)] = segment;
|
|
337
|
+
}
|
|
338
|
+
else if (segment !== routeSegment) {
|
|
339
|
+
isMatch = false;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
isMatch = false;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (isMatch) {
|
|
349
|
+
return {
|
|
350
|
+
component: routeConfig.component,
|
|
351
|
+
params,
|
|
352
|
+
meta: typeof routeConfig.meta === "function"
|
|
353
|
+
? routeConfig.meta(params)
|
|
354
|
+
: routeConfig.meta,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
async checkRouteGuard(route, path, params) {
|
|
361
|
+
if (!route.guard)
|
|
362
|
+
return true;
|
|
363
|
+
if (route.guard.canActivate) {
|
|
364
|
+
const canActivate = await route.guard.canActivate(path, params);
|
|
365
|
+
return canActivate;
|
|
366
|
+
}
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
getPreloadedComponent(path) {
|
|
370
|
+
return this.routeCache.get(path);
|
|
371
|
+
}
|
|
372
|
+
clearCache() {
|
|
373
|
+
this.routeCache.clear();
|
|
374
|
+
this.routeMatchCache.clear();
|
|
375
|
+
this.preloadQueue.clear();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
class LayoutRouter {
|
|
379
|
+
constructor() {
|
|
380
|
+
this.layoutCache = new Map();
|
|
381
|
+
this.keepAliveComponents = new Map();
|
|
382
|
+
}
|
|
383
|
+
renderNestedRoutes(routes, segments) {
|
|
384
|
+
if (segments.length === 0)
|
|
385
|
+
return null;
|
|
386
|
+
const [currentSegment, ...remainingSegments] = segments;
|
|
387
|
+
const currentRoute = routes.find((r) => r.path === currentSegment);
|
|
388
|
+
if (!currentRoute)
|
|
389
|
+
return null;
|
|
390
|
+
let content;
|
|
391
|
+
if (remainingSegments.length > 0 && currentRoute.children) {
|
|
392
|
+
content = this.renderNestedRoutes(currentRoute.children, remainingSegments);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
if (currentRoute.keepAlive && currentSegment) {
|
|
396
|
+
content = this.renderKeepAlive(currentRoute.component, currentSegment);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
content = this.renderComponent(currentRoute.component);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (currentRoute.layout) {
|
|
403
|
+
const layoutKey = `${currentSegment}_layout`;
|
|
404
|
+
let layoutComponent = this.layoutCache.get(layoutKey);
|
|
405
|
+
if (!layoutComponent) {
|
|
406
|
+
layoutComponent = this.renderComponent(currentRoute.layout);
|
|
407
|
+
this.layoutCache.set(layoutKey, layoutComponent);
|
|
408
|
+
}
|
|
409
|
+
return this.renderComponent(currentRoute.layout, { children: content });
|
|
410
|
+
}
|
|
411
|
+
return content;
|
|
412
|
+
}
|
|
413
|
+
renderKeepAlive(component, key) {
|
|
414
|
+
if (this.keepAliveComponents.has(key)) {
|
|
415
|
+
return this.keepAliveComponents.get(key);
|
|
416
|
+
}
|
|
417
|
+
const rendered = this.renderComponent(component);
|
|
418
|
+
this.keepAliveComponents.set(key, rendered);
|
|
419
|
+
return rendered;
|
|
420
|
+
}
|
|
421
|
+
renderComponent(component, props = {}) {
|
|
422
|
+
try {
|
|
423
|
+
return component(props);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
console.error("[LayoutRouter] Component render error:", error);
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
cleanup() {
|
|
431
|
+
this.layoutCache.clear();
|
|
432
|
+
this.keepAliveComponents.clear();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const enterpriseRouter = new EnterpriseRouter();
|
|
436
|
+
const layoutRouter = new LayoutRouter();
|
|
196
437
|
function createFynix() {
|
|
197
438
|
const isDevMode = import.meta.hot !== undefined;
|
|
198
439
|
if (routerInstance && isRouterInitialized && !isDevMode) {
|
|
@@ -210,6 +451,8 @@ function createFynix() {
|
|
|
210
451
|
let isDestroyed = false;
|
|
211
452
|
let listenerCount = 0;
|
|
212
453
|
let renderTimeout = null;
|
|
454
|
+
let lastNavigationTime = 0;
|
|
455
|
+
const NAVIGATION_RATE_LIMIT = 100;
|
|
213
456
|
const listeners = [];
|
|
214
457
|
if (!window[PROPS_NAMESPACE]) {
|
|
215
458
|
window[PROPS_NAMESPACE] = {};
|
|
@@ -262,13 +505,24 @@ function createFynix() {
|
|
|
262
505
|
routes[routePath] = component;
|
|
263
506
|
}
|
|
264
507
|
}
|
|
265
|
-
function renderRouteImmediate() {
|
|
508
|
+
async function renderRouteImmediate() {
|
|
266
509
|
if (isDestroyed)
|
|
267
510
|
return;
|
|
268
511
|
const path = normalizePath(window.location.pathname);
|
|
269
512
|
let Page = routes[path];
|
|
270
513
|
let params = {};
|
|
271
514
|
let routeProps = {};
|
|
515
|
+
const enterpriseMatch = enterpriseRouter.matchRoute(path);
|
|
516
|
+
if (enterpriseMatch) {
|
|
517
|
+
const preloadedComponent = enterpriseRouter.getPreloadedComponent(path);
|
|
518
|
+
if (preloadedComponent) {
|
|
519
|
+
Page = preloadedComponent;
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
Page = enterpriseMatch.component;
|
|
523
|
+
}
|
|
524
|
+
params = enterpriseMatch.params;
|
|
525
|
+
}
|
|
272
526
|
if (!Page) {
|
|
273
527
|
const match = matchDynamicRoute(path, dynamicRoutes);
|
|
274
528
|
if (match) {
|
|
@@ -282,8 +536,30 @@ function createFynix() {
|
|
|
282
536
|
return;
|
|
283
537
|
}
|
|
284
538
|
if (!Page) {
|
|
285
|
-
root.innerHTML =
|
|
539
|
+
root.innerHTML = "";
|
|
540
|
+
const container = document.createElement("div");
|
|
541
|
+
container.style.cssText =
|
|
542
|
+
"padding: 2rem; text-align: center; font-family: system-ui, sans-serif;";
|
|
543
|
+
const heading = document.createElement("h2");
|
|
544
|
+
heading.textContent = "404 Not Found";
|
|
545
|
+
heading.style.cssText = "color: #dc2626; margin-bottom: 1rem;";
|
|
546
|
+
const pathInfo = document.createElement("p");
|
|
547
|
+
const safePath = escapeHTML(sanitizeContent(path));
|
|
548
|
+
pathInfo.textContent = `Path: ${safePath}`;
|
|
549
|
+
pathInfo.style.cssText = "color: #6b7280; margin-bottom: 2rem;";
|
|
550
|
+
const backButton = document.createElement("button");
|
|
551
|
+
backButton.textContent = "Go Back";
|
|
552
|
+
backButton.style.cssText =
|
|
553
|
+
"padding: 0.5rem 1rem; background: #3b82f6; color: white; border: none; border-radius: 0.25rem; cursor: pointer;";
|
|
554
|
+
backButton.onclick = () => window.history.back();
|
|
555
|
+
container.appendChild(heading);
|
|
556
|
+
container.appendChild(pathInfo);
|
|
557
|
+
container.appendChild(backButton);
|
|
558
|
+
root.appendChild(container);
|
|
286
559
|
updateMetaTags({ title: "404 - Page Not Found" });
|
|
560
|
+
["/", "/home", "/about"].forEach((commonPath) => {
|
|
561
|
+
enterpriseRouter.preloadRoute(commonPath).catch(console.warn);
|
|
562
|
+
});
|
|
287
563
|
return;
|
|
288
564
|
}
|
|
289
565
|
const state = (window.history.state || {});
|
|
@@ -301,17 +577,23 @@ function createFynix() {
|
|
|
301
577
|
const meta = typeof Page.meta === "function" ? Page.meta(params) : Page.meta;
|
|
302
578
|
updateMetaTags(meta);
|
|
303
579
|
}
|
|
304
|
-
|
|
580
|
+
const unsafeProps = {
|
|
305
581
|
...routeProps,
|
|
306
582
|
...passedProps,
|
|
307
583
|
params,
|
|
308
584
|
};
|
|
585
|
+
const safeProps = sanitizeProps(unsafeProps);
|
|
586
|
+
window.__lastRouteProps = safeProps;
|
|
309
587
|
try {
|
|
310
|
-
mount(Page, rootSelector,
|
|
588
|
+
mount(Page, rootSelector, safeProps);
|
|
311
589
|
}
|
|
312
590
|
catch (err) {
|
|
313
591
|
console.error("[Router] Mount failed:", err);
|
|
314
|
-
root.innerHTML =
|
|
592
|
+
root.innerHTML = "";
|
|
593
|
+
const errorDiv = document.createElement("pre");
|
|
594
|
+
errorDiv.style.color = "red";
|
|
595
|
+
errorDiv.textContent = "Mount Error occurred";
|
|
596
|
+
root.appendChild(errorDiv);
|
|
315
597
|
}
|
|
316
598
|
currentPath = path;
|
|
317
599
|
}
|
|
@@ -329,6 +611,12 @@ function createFynix() {
|
|
|
329
611
|
function navigate(path, props = {}) {
|
|
330
612
|
if (isDestroyed)
|
|
331
613
|
return;
|
|
614
|
+
const now = Date.now();
|
|
615
|
+
if (now - lastNavigationTime < NAVIGATION_RATE_LIMIT) {
|
|
616
|
+
console.warn("[Router] Security: Navigation rate limited");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
lastNavigationTime = now;
|
|
332
620
|
const normalizedPath = normalizePath(path);
|
|
333
621
|
if (!isValidURL(window.location.origin + normalizedPath)) {
|
|
334
622
|
console.error("[Router] Invalid navigation URL");
|
|
@@ -336,8 +624,10 @@ function createFynix() {
|
|
|
336
624
|
}
|
|
337
625
|
if (normalizedPath === currentPath)
|
|
338
626
|
return;
|
|
627
|
+
enterpriseRouter.preloadRoute(normalizedPath).catch(console.warn);
|
|
628
|
+
const sanitizedProps = sanitizeProps(props);
|
|
339
629
|
const cacheKey = generateCacheKey();
|
|
340
|
-
addToCache(propsCache, cacheKey,
|
|
630
|
+
addToCache(propsCache, cacheKey, sanitizedProps);
|
|
341
631
|
try {
|
|
342
632
|
window.history.pushState({ __fynixCacheKey: cacheKey }, "", normalizedPath);
|
|
343
633
|
renderRoute();
|
|
@@ -349,13 +639,20 @@ function createFynix() {
|
|
|
349
639
|
function replace(path, props = {}) {
|
|
350
640
|
if (isDestroyed)
|
|
351
641
|
return;
|
|
642
|
+
const now = Date.now();
|
|
643
|
+
if (now - lastNavigationTime < NAVIGATION_RATE_LIMIT) {
|
|
644
|
+
console.warn("[Router] Security: Replace rate limited");
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
lastNavigationTime = now;
|
|
352
648
|
const normalizedPath = normalizePath(path);
|
|
353
649
|
if (!isValidURL(window.location.origin + normalizedPath)) {
|
|
354
650
|
console.error("[Router] Invalid replace URL");
|
|
355
651
|
return;
|
|
356
652
|
}
|
|
653
|
+
const sanitizedProps = sanitizeProps(props);
|
|
357
654
|
const cacheKey = generateCacheKey();
|
|
358
|
-
addToCache(propsCache, cacheKey,
|
|
655
|
+
addToCache(propsCache, cacheKey, sanitizedProps);
|
|
359
656
|
try {
|
|
360
657
|
window.history.replaceState({ __fynixCacheKey: cacheKey }, "", normalizedPath);
|
|
361
658
|
renderRoute();
|
|
@@ -459,6 +756,8 @@ function createFynix() {
|
|
|
459
756
|
renderTimeout = null;
|
|
460
757
|
}
|
|
461
758
|
isDestroyed = true;
|
|
759
|
+
enterpriseRouter.clearCache();
|
|
760
|
+
layoutRouter.cleanup();
|
|
462
761
|
listeners.forEach(({ element, event, handler }) => {
|
|
463
762
|
try {
|
|
464
763
|
element.removeEventListener(event, handler);
|
|
@@ -519,6 +818,15 @@ function createFynix() {
|
|
|
519
818
|
cleanup,
|
|
520
819
|
routes,
|
|
521
820
|
dynamicRoutes,
|
|
821
|
+
preloadRoute: enterpriseRouter.preloadRoute.bind(enterpriseRouter),
|
|
822
|
+
clearCache: () => {
|
|
823
|
+
enterpriseRouter.clearCache();
|
|
824
|
+
layoutRouter.cleanup();
|
|
825
|
+
},
|
|
826
|
+
enableNestedRouting: (nestedRoutes) => {
|
|
827
|
+
router.nestedRoutes = nestedRoutes;
|
|
828
|
+
console.log("[Router] Nested routing enabled with", nestedRoutes.length, "routes");
|
|
829
|
+
},
|
|
522
830
|
};
|
|
523
831
|
routerInstance = router;
|
|
524
832
|
return router;
|