slicejs-web-framework 2.2.13 → 2.3.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.
@@ -1,589 +1,598 @@
1
- export default class Router {
2
- constructor(routes) {
3
- this.routes = routes;
4
- this.activeRoute = null;
5
- this.pathToRouteMap = this.createPathToRouteMap(routes);
6
-
7
- // Navigation Guards
8
- this._beforeEachGuard = null;
9
- this._afterEachGuard = null;
10
-
11
- // Router state
12
- this._started = false;
13
- this._autoStartTimeout = null;
14
-
15
- // Sistema de caché optimizado
16
- this.routeContainersCache = new Map();
17
- this.lastCacheUpdate = 0;
18
- this.CACHE_DURATION = 100; // ms - caché muy corto pero efectivo
19
-
20
- // Observer para invalidar caché automáticamente
21
- this.setupMutationObserver();
22
- }
23
-
24
- /**
25
- * Inicializa el router
26
- * Si el usuario no llama start() manualmente, se auto-inicia después de un delay
27
- */
28
- init() {
29
- window.addEventListener('popstate', this.onRouteChange.bind(this));
30
-
31
- // Auto-start después de 50ms si el usuario no llama start() manualmente
32
- // Esto da tiempo para que el usuario configure guards si lo necesita
33
- this._autoStartTimeout = setTimeout(async () => {
34
- if (!this._started) {
35
- slice.logger.logInfo('Router', 'Auto-starting router (no manual start() called)');
36
- await this.start();
37
- }
38
- }, 50);
39
- }
40
-
41
- /**
42
- * Inicia el router y carga la ruta inicial
43
- * OPCIONAL: Solo necesario si usas guards (beforeEach/afterEach)
44
- * Si no lo llamas, el router se auto-inicia después de 50ms
45
- */
46
- async start() {
47
- // Prevenir múltiples llamadas
48
- if (this._started) {
49
- slice.logger.logWarning('Router', 'start() already called');
50
- return;
51
- }
52
-
53
- // Cancelar auto-start si existe
54
- if (this._autoStartTimeout) {
55
- clearTimeout(this._autoStartTimeout);
56
- this._autoStartTimeout = null;
57
- }
58
-
59
- this._started = true;
60
- await this.loadInitialRoute();
61
- }
62
-
63
- // ============================================
64
- // NAVIGATION GUARDS API
65
- // ============================================
66
-
67
- /**
68
- * Registra un guard que se ejecuta ANTES de cada navegación
69
- * Puede bloquear o redirigir la navegación
70
- * @param {Function} guard - Función (to, from, next) => {}
71
- */
72
- beforeEach(guard) {
73
- if (typeof guard !== 'function') {
74
- slice.logger.logError('Router', 'beforeEach expects a function');
75
- return;
76
- }
77
- this._beforeEachGuard = guard;
78
- }
79
-
80
- /**
81
- * Registra un guard que se ejecuta DESPUÉS de cada navegación
82
- * No puede bloquear la navegación
83
- * @param {Function} guard - Función (to, from) => {}
84
- */
85
- afterEach(guard) {
86
- if (typeof guard !== 'function') {
87
- slice.logger.logError('Router', 'afterEach expects a function');
88
- return;
89
- }
90
- this._afterEachGuard = guard;
91
- }
92
-
93
- /**
94
- * Crea un objeto con información de ruta para los guards
95
- * @param {Object} route - Objeto de ruta
96
- * @param {Object} params - Parámetros de la ruta
97
- * @param {String} requestedPath - Path original solicitado
98
- * @returns {Object} Objeto con path, component, params, query, metadata
99
- */
100
- _createRouteInfo(route, params = {}, requestedPath = null) {
101
- if (!route) {
102
- return {
103
- path: requestedPath || '/404',
104
- component: 'NotFound',
105
- params: {},
106
- query: this._parseQueryParams(),
107
- metadata: {}
108
- };
109
- }
110
-
111
- return {
112
- path: requestedPath || route.fullPath || route.path,
113
- component: route.parentRoute ? route.parentRoute.component : route.component,
114
- params: params,
115
- query: this._parseQueryParams(),
116
- metadata: route.metadata || {}
117
- };
118
- }
119
-
120
- /**
121
- * Parsea los query parameters de la URL actual
122
- * @returns {Object} Objeto con los query params
123
- */
124
- _parseQueryParams() {
125
- const queryString = window.location.search;
126
- if (!queryString) return {};
127
-
128
- const params = {};
129
- const urlParams = new URLSearchParams(queryString);
130
-
131
- for (const [key, value] of urlParams) {
132
- params[key] = value;
133
- }
134
-
135
- return params;
136
- }
137
-
138
- /**
139
- * Ejecuta el beforeEach guard si existe
140
- * @param {Object} to - Información de ruta destino
141
- * @param {Object} from - Información de ruta origen
142
- * @returns {Object|null} Objeto con redirectPath y options, o null si continúa
143
- */
144
- async _executeBeforeEachGuard(to, from) {
145
- if (!this._beforeEachGuard) {
146
- return null;
147
- }
148
-
149
- let redirectPath = null;
150
- let redirectOptions = {};
151
- let nextCalled = false;
152
-
153
- const next = (arg) => {
154
- if (nextCalled) {
155
- slice.logger.logWarning('Router', 'next() called multiple times in guard');
156
- return;
157
- }
158
- nextCalled = true;
159
-
160
- // Caso 1: Sin argumentos - continuar navegación
161
- if (arg === undefined) {
162
- return;
163
- }
164
-
165
- // Caso 2: false - cancelar navegación
166
- if (arg === false) {
167
- redirectPath = false;
168
- return;
169
- }
170
-
171
- // Caso 3: String - redirección simple (backward compatibility)
172
- if (typeof arg === 'string') {
173
- redirectPath = arg;
174
- redirectOptions = { replace: false };
175
- return;
176
- }
177
-
178
- // Caso 4: Objeto - redirección con opciones
179
- if (typeof arg === 'object' && arg.path) {
180
- redirectPath = arg.path;
181
- redirectOptions = {
182
- replace: arg.replace || false
183
- };
184
- return;
185
- }
186
-
187
- // Argumento inválido
188
- slice.logger.logError('Router', 'Invalid argument passed to next(). Expected string, object with path, false, or undefined.');
189
- };
190
-
191
- try {
192
- await this._beforeEachGuard(to, from, next);
193
-
194
- // Si no se llamó next(), loguear advertencia pero continuar
195
- if (!nextCalled) {
196
- slice.logger.logWarning(
197
- 'Router',
198
- 'beforeEach guard did not call next(). Navigation will continue.'
199
- );
200
- }
201
-
202
- // Retornar tanto el path como las opciones
203
- return redirectPath ? { path: redirectPath, options: redirectOptions } : null;
204
- } catch (error) {
205
- slice.logger.logError('Router', 'Error in beforeEach guard', error);
206
- return null; // En caso de error, continuar con la navegación
207
- }
208
- }
209
-
210
- /**
211
- * Ejecuta el afterEach guard si existe
212
- * @param {Object} to - Información de ruta destino
213
- * @param {Object} from - Información de ruta origen
214
- */
215
- _executeAfterEachGuard(to, from) {
216
- if (!this._afterEachGuard) {
217
- return;
218
- }
219
-
220
- try {
221
- this._afterEachGuard(to, from);
222
- } catch (error) {
223
- slice.logger.logError('Router', 'Error in afterEach guard', error);
224
- }
225
- }
226
-
227
- // ============================================
228
- // ROUTING CORE (MODIFICADO CON GUARDS)
229
- // ============================================
230
-
231
- async navigate(path, _redirectChain = [], _options = {}) {
232
- const currentPath = window.location.pathname;
233
-
234
-
235
- // Detectar loops infinitos: si ya visitamos esta ruta en la cadena de redirecciones
236
- if (_redirectChain.includes(path)) {
237
- slice.logger.logError(
238
- 'Router',
239
- `Guard redirection loop detected: ${_redirectChain.join(' → ')} → ${path}`
240
- );
241
- return;
242
- }
243
-
244
- // Límite de seguridad: máximo 10 redirecciones
245
- if (_redirectChain.length >= 10) {
246
- slice.logger.logError(
247
- 'Router',
248
- `Too many redirections: ${_redirectChain.join(' ')} → ${path}`
249
- );
250
- return;
251
- }
252
-
253
- // Obtener información de ruta actual
254
- const { route: fromRoute, params: fromParams } = this.matchRoute(currentPath);
255
- const from = this._createRouteInfo(fromRoute, fromParams, currentPath);
256
-
257
- // Obtener información de ruta destino
258
- const { route: toRoute, params: toParams } = this.matchRoute(path);
259
- const to = this._createRouteInfo(toRoute, toParams, path);
260
-
261
-
262
- // EJECUTAR BEFORE EACH GUARD
263
- const guardResult = await this._executeBeforeEachGuard(to, from);
264
-
265
- // Si el guard redirige
266
- if (guardResult && guardResult.path) {
267
- const newChain = [..._redirectChain, path];
268
- return this.navigate(guardResult.path, newChain, guardResult.options);
269
- }
270
-
271
- // Si el guard cancela la navegación (next(false))
272
- if (guardResult && guardResult.path === false) {
273
- slice.logger.logInfo('Router', 'Navigation cancelled by guard');
274
- return;
275
- }
276
-
277
- // No hay redirección - continuar con la navegación normal
278
- // Usar replace o push según las opciones
279
- if (_options.replace) {
280
- window.history.replaceState({}, path, window.location.origin + path);
281
- } else {
282
- window.history.pushState({}, path, window.location.origin + path);
283
- }
284
-
285
- await this._performNavigation(to, from);
286
- }
287
-
288
- /**
289
- * Método interno para ejecutar la navegación después de pasar los guards
290
- * @param {Object} to - Información de ruta destino
291
- * @param {Object} from - Información de ruta origen
292
- */
293
- async _performNavigation(to, from) {
294
- // Renderizar la nueva ruta
295
- await this.onRouteChange();
296
-
297
- // EJECUTAR AFTER EACH GUARD
298
- this._executeAfterEachGuard(to, from);
299
- }
300
-
301
- async onRouteChange() {
302
- // Cancelar el timeout anterior si existe
303
- if (this.routeChangeTimeout) {
304
- clearTimeout(this.routeChangeTimeout);
305
- }
306
-
307
- // Debounce de 10ms para evitar múltiples llamadas seguidas
308
- this.routeChangeTimeout = setTimeout(async () => {
309
- const path = window.location.pathname;
310
- const routeContainersFlag = await this.renderRoutesComponentsInPage();
311
-
312
- if (routeContainersFlag) {
313
- return;
314
- }
315
-
316
- const { route, params } = this.matchRoute(path);
317
- if (route) {
318
- await this.handleRoute(route, params);
319
- }
320
- }, 10);
321
- }
322
-
323
- async handleRoute(route, params) {
324
- const targetElement = document.querySelector('#app');
325
-
326
- const componentName = route.parentRoute ? route.parentRoute.component : route.component;
327
- const sliceId = `route-${componentName}`;
328
-
329
- const existingComponent = slice.controller.getComponent(sliceId);
330
-
331
- if (slice.loading) {
332
- slice.loading.start();
333
- }
334
-
335
- if (existingComponent) {
336
- targetElement.innerHTML = '';
337
- if (existingComponent.update) {
338
- existingComponent.props = { ...existingComponent.props, ...params };
339
- await existingComponent.update();
340
- }
341
- targetElement.appendChild(existingComponent);
342
- await this.renderRoutesInComponent(existingComponent);
343
- } else {
344
- const component = await slice.build(componentName, {
345
- params,
346
- sliceId: sliceId,
347
- });
348
-
349
- targetElement.innerHTML = '';
350
- targetElement.appendChild(component);
351
-
352
- await this.renderRoutesInComponent(component);
353
- }
354
-
355
- // Invalidar caché después de cambios importantes en el DOM
356
- this.invalidateCache();
357
-
358
- if (slice.loading) {
359
- slice.loading.stop();
360
- }
361
-
362
- slice.router.activeRoute = route;
363
- }
364
-
365
- async loadInitialRoute() {
366
- const path = window.location.pathname;
367
- const { route, params } = this.matchRoute(path);
368
-
369
- // Para la carga inicial, también ejecutar guards
370
- const from = this._createRouteInfo(null, {}, null);
371
- const to = this._createRouteInfo(route, params, path);
372
-
373
- // EJECUTAR BEFORE EACH GUARD en carga inicial
374
- const guardResult = await this._executeBeforeEachGuard(to, from);
375
-
376
- if (guardResult && guardResult.path) {
377
- return this.navigate(guardResult.path, [], guardResult.options);
378
- }
379
-
380
- // Si el guard cancela la navegación inicial (caso raro pero posible)
381
- if (guardResult && guardResult.path === false) {
382
- slice.logger.logWarning('Router', 'Initial route navigation cancelled by guard');
383
- return;
384
- }
385
-
386
- await this.handleRoute(route, params);
387
-
388
- // EJECUTAR AFTER EACH GUARD en carga inicial
389
- this._executeAfterEachGuard(to, from);
390
- }
391
-
392
- // ============================================
393
- // MÉTODOS EXISTENTES (SIN CAMBIOS)
394
- // ============================================
395
-
396
- setupMutationObserver() {
397
- if (typeof MutationObserver !== 'undefined') {
398
- this.observer = new MutationObserver((mutations) => {
399
- let shouldInvalidateCache = false;
400
-
401
- mutations.forEach((mutation) => {
402
- if (mutation.type === 'childList') {
403
- const addedNodes = Array.from(mutation.addedNodes);
404
- const removedNodes = Array.from(mutation.removedNodes);
405
-
406
- const hasRouteNodes = [...addedNodes, ...removedNodes].some(node =>
407
- node.nodeType === Node.ELEMENT_NODE &&
408
- (node.tagName === 'SLICE-ROUTE' ||
409
- node.tagName === 'SLICE-MULTI-ROUTE' ||
410
- node.querySelector?.('slice-route, slice-multi-route'))
411
- );
412
-
413
- if (hasRouteNodes) {
414
- shouldInvalidateCache = true;
415
- }
416
- }
417
- });
418
-
419
- if (shouldInvalidateCache) {
420
- this.invalidateCache();
421
- }
422
- });
423
-
424
- this.observer.observe(document.body, {
425
- childList: true,
426
- subtree: true
427
- });
428
- }
429
- }
430
-
431
- invalidateCache() {
432
- this.routeContainersCache.clear();
433
- this.lastCacheUpdate = 0;
434
- }
435
-
436
- createPathToRouteMap(routes, basePath = '', parentRoute = null) {
437
- const pathToRouteMap = new Map();
438
-
439
- for (const route of routes) {
440
- const fullPath = `${basePath}${route.path}`.replace(/\/+/g, '/');
441
-
442
- const routeWithParent = {
443
- ...route,
444
- fullPath,
445
- parentPath: parentRoute ? parentRoute.fullPath : null,
446
- parentRoute: parentRoute
447
- };
448
-
449
- pathToRouteMap.set(fullPath, routeWithParent);
450
-
451
- if (route.children) {
452
- const childPathToRouteMap = this.createPathToRouteMap(
453
- route.children,
454
- fullPath,
455
- routeWithParent
456
- );
457
-
458
- for (const [childPath, childRoute] of childPathToRouteMap.entries()) {
459
- pathToRouteMap.set(childPath, childRoute);
460
- }
461
- }
462
- }
463
-
464
- return pathToRouteMap;
465
- }
466
-
467
- async renderRoutesComponentsInPage(searchContainer = document) {
468
- let routerContainersFlag = false;
469
- const routeContainers = this.getCachedRouteContainers(searchContainer);
470
-
471
- for (const routeContainer of routeContainers) {
472
- try {
473
- if (!routeContainer.isConnected) {
474
- this.invalidateCache();
475
- continue;
476
- }
477
-
478
- let response = await routeContainer.renderIfCurrentRoute();
479
- if (response) {
480
- this.activeRoute = routeContainer.props;
481
- routerContainersFlag = true;
482
- }
483
- } catch (error) {
484
- slice.logger.logError('Router', `Error rendering route container`, error);
485
- }
486
- }
487
-
488
- return routerContainersFlag;
489
- }
490
-
491
- getCachedRouteContainers(container) {
492
- const containerKey = container === document ? 'document' : container.sliceId || 'anonymous';
493
- const now = Date.now();
494
-
495
- if (this.routeContainersCache.has(containerKey) &&
496
- (now - this.lastCacheUpdate) < this.CACHE_DURATION) {
497
- return this.routeContainersCache.get(containerKey);
498
- }
499
-
500
- const routeContainers = this.findAllRouteContainersOptimized(container);
501
- this.routeContainersCache.set(containerKey, routeContainers);
502
- this.lastCacheUpdate = now;
503
-
504
- return routeContainers;
505
- }
506
-
507
- findAllRouteContainersOptimized(container) {
508
- const routeContainers = [];
509
-
510
- const walker = document.createTreeWalker(
511
- container,
512
- NodeFilter.SHOW_ELEMENT,
513
- {
514
- acceptNode: (node) => {
515
- if (node.tagName === 'SLICE-ROUTE' || node.tagName === 'SLICE-MULTI-ROUTE') {
516
- return NodeFilter.FILTER_ACCEPT;
517
- }
518
- return NodeFilter.FILTER_SKIP;
519
- }
520
- }
521
- );
522
-
523
- let node;
524
- while (node = walker.nextNode()) {
525
- routeContainers.push(node);
526
- }
527
-
528
- return routeContainers;
529
- }
530
-
531
- async renderRoutesInComponent(component) {
532
- if (!component) {
533
- slice.logger.logWarning('Router', 'No component provided for route rendering');
534
- return false;
535
- }
536
-
537
- return await this.renderRoutesComponentsInPage(component);
538
- }
539
-
540
- matchRoute(path) {
541
- const exactMatch = this.pathToRouteMap.get(path);
542
- if (exactMatch) {
543
- if (exactMatch.parentRoute) {
544
- return {
545
- route: exactMatch.parentRoute,
546
- params: {},
547
- childRoute: exactMatch
548
- };
549
- }
550
- return { route: exactMatch, params: {} };
551
- }
552
-
553
- for (const [routePattern, route] of this.pathToRouteMap.entries()) {
554
- if (routePattern.includes('${')) {
555
- const { regex, paramNames } = this.compilePathPattern(routePattern);
556
- const match = path.match(regex);
557
- if (match) {
558
- const params = {};
559
- paramNames.forEach((name, i) => {
560
- params[name] = match[i + 1];
561
- });
562
-
563
- if (route.parentRoute) {
564
- return {
565
- route: route.parentRoute,
566
- params: params,
567
- childRoute: route
568
- };
569
- }
570
-
571
- return { route, params };
572
- }
573
- }
574
- }
575
-
576
- const notFoundRoute = this.pathToRouteMap.get('/404');
577
- return { route: notFoundRoute, params: {} };
578
- }
579
-
580
- compilePathPattern(pattern) {
581
- const paramNames = [];
582
- const regexPattern = '^' + pattern.replace(/\$\{([^}]+)\}/g, (_, paramName) => {
583
- paramNames.push(paramName);
584
- return '([^/]+)';
585
- }) + '$';
586
-
587
- return { regex: new RegExp(regexPattern), paramNames };
588
- }
589
- }
1
+ export default class Router {
2
+ constructor(routes) {
3
+ this.routes = routes;
4
+ this.activeRoute = null;
5
+ this.pathToRouteMap = this.createPathToRouteMap(routes);
6
+
7
+ // Navigation Guards
8
+ this._beforeEachGuard = null;
9
+ this._afterEachGuard = null;
10
+
11
+ // Router state
12
+ this._started = false;
13
+ this._autoStartTimeout = null;
14
+
15
+ // Sistema de caché optimizado
16
+ this.routeContainersCache = new Map();
17
+ this.lastCacheUpdate = 0;
18
+ this.CACHE_DURATION = 100; // ms - caché muy corto pero efectivo
19
+
20
+ // Observer para invalidar caché automáticamente
21
+ this.setupMutationObserver();
22
+ }
23
+
24
+ /**
25
+ * Inicializa el router
26
+ * Si el usuario no llama start() manualmente, se auto-inicia después de un delay
27
+ */
28
+ init() {
29
+ window.addEventListener('popstate', this.onRouteChange.bind(this));
30
+
31
+ // Auto-start después de 50ms si el usuario no llama start() manualmente
32
+ // Esto da tiempo para que el usuario configure guards si lo necesita
33
+ this._autoStartTimeout = setTimeout(async () => {
34
+ if (!this._started) {
35
+ slice.logger.logInfo('Router', 'Auto-starting router (no manual start() called)');
36
+ await this.start();
37
+ }
38
+ }, 50);
39
+ }
40
+
41
+ /**
42
+ * Inicia el router y carga la ruta inicial
43
+ * OPCIONAL: Solo necesario si usas guards (beforeEach/afterEach)
44
+ * Si no lo llamas, el router se auto-inicia después de 50ms
45
+ */
46
+ async start() {
47
+ // Prevenir múltiples llamadas
48
+ if (this._started) {
49
+ slice.logger.logWarning('Router', 'start() already called');
50
+ return;
51
+ }
52
+
53
+ // Cancelar auto-start si existe
54
+ if (this._autoStartTimeout) {
55
+ clearTimeout(this._autoStartTimeout);
56
+ this._autoStartTimeout = null;
57
+ }
58
+
59
+ this._started = true;
60
+ await this.loadInitialRoute();
61
+ }
62
+
63
+ // ============================================
64
+ // NAVIGATION GUARDS API
65
+ // ============================================
66
+
67
+ /**
68
+ * Registra un guard que se ejecuta ANTES de cada navegación
69
+ * Puede bloquear o redirigir la navegación
70
+ * @param {Function} guard - Función (to, from, next) => {}
71
+ */
72
+ beforeEach(guard) {
73
+ if (typeof guard !== 'function') {
74
+ slice.logger.logError('Router', 'beforeEach expects a function');
75
+ return;
76
+ }
77
+ this._beforeEachGuard = guard;
78
+ }
79
+
80
+ /**
81
+ * Registra un guard que se ejecuta DESPUÉS de cada navegación
82
+ * No puede bloquear la navegación
83
+ * @param {Function} guard - Función (to, from) => {}
84
+ */
85
+ afterEach(guard) {
86
+ if (typeof guard !== 'function') {
87
+ slice.logger.logError('Router', 'afterEach expects a function');
88
+ return;
89
+ }
90
+ this._afterEachGuard = guard;
91
+ }
92
+
93
+ /**
94
+ * Crea un objeto con información de ruta para los guards
95
+ * @param {Object} route - Objeto de ruta
96
+ * @param {Object} params - Parámetros de la ruta
97
+ * @param {String} requestedPath - Path original solicitado
98
+ * @returns {Object} Objeto con path, component, params, query, metadata
99
+ */
100
+ _createRouteInfo(route, params = {}, requestedPath = null) {
101
+ if (!route) {
102
+ return {
103
+ path: requestedPath || '/404',
104
+ component: 'NotFound',
105
+ params: {},
106
+ query: this._parseQueryParams(),
107
+ metadata: {},
108
+ };
109
+ }
110
+
111
+ return {
112
+ path: requestedPath || route.fullPath || route.path,
113
+ component: route.parentRoute ? route.parentRoute.component : route.component,
114
+ params: params,
115
+ query: this._parseQueryParams(),
116
+ metadata: route.metadata || {},
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Parsea los query parameters de la URL actual
122
+ * @returns {Object} Objeto con los query params
123
+ */
124
+ _parseQueryParams() {
125
+ const queryString = window.location.search;
126
+ if (!queryString) return {};
127
+
128
+ const params = {};
129
+ const urlParams = new URLSearchParams(queryString);
130
+
131
+ for (const [key, value] of urlParams) {
132
+ params[key] = value;
133
+ }
134
+
135
+ return params;
136
+ }
137
+
138
+ /**
139
+ * Ejecuta el beforeEach guard si existe
140
+ * @param {Object} to - Información de ruta destino
141
+ * @param {Object} from - Información de ruta origen
142
+ * @returns {Object|null} Objeto con redirectPath y options, o null si continúa
143
+ */
144
+ async _executeBeforeEachGuard(to, from) {
145
+ if (!this._beforeEachGuard) {
146
+ return null;
147
+ }
148
+
149
+ let redirectPath = null;
150
+ let redirectOptions = {};
151
+ let nextCalled = false;
152
+
153
+ const next = (arg) => {
154
+ if (nextCalled) {
155
+ slice.logger.logWarning('Router', 'next() called multiple times in guard');
156
+ return;
157
+ }
158
+ nextCalled = true;
159
+
160
+ // Caso 1: Sin argumentos - continuar navegación
161
+ if (arg === undefined) {
162
+ return;
163
+ }
164
+
165
+ // Caso 2: false - cancelar navegación
166
+ if (arg === false) {
167
+ redirectPath = false;
168
+ return;
169
+ }
170
+
171
+ // Caso 3: String - redirección simple (backward compatibility)
172
+ if (typeof arg === 'string') {
173
+ redirectPath = arg;
174
+ redirectOptions = { replace: false };
175
+ return;
176
+ }
177
+
178
+ // Caso 4: Objeto - redirección con opciones
179
+ if (typeof arg === 'object' && arg.path) {
180
+ redirectPath = arg.path;
181
+ redirectOptions = {
182
+ replace: arg.replace || false,
183
+ };
184
+ return;
185
+ }
186
+
187
+ // Argumento inválido
188
+ slice.logger.logError(
189
+ 'Router',
190
+ 'Invalid argument passed to next(). Expected string, object with path, false, or undefined.'
191
+ );
192
+ };
193
+
194
+ try {
195
+ await this._beforeEachGuard(to, from, next);
196
+
197
+ // Si no se llamó next(), loguear advertencia pero continuar
198
+ if (!nextCalled) {
199
+ slice.logger.logWarning('Router', 'beforeEach guard did not call next(). Navigation will continue.');
200
+ }
201
+
202
+ // Retornar tanto el path como las opciones
203
+ return redirectPath ? { path: redirectPath, options: redirectOptions } : null;
204
+ } catch (error) {
205
+ slice.logger.logError('Router', 'Error in beforeEach guard', error);
206
+ return null; // En caso de error, continuar con la navegación
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Ejecuta el afterEach guard si existe
212
+ * @param {Object} to - Información de ruta destino
213
+ * @param {Object} from - Información de ruta origen
214
+ */
215
+ _executeAfterEachGuard(to, from) {
216
+ if (!this._afterEachGuard) {
217
+ return;
218
+ }
219
+
220
+ try {
221
+ this._afterEachGuard(to, from);
222
+ } catch (error) {
223
+ slice.logger.logError('Router', 'Error in afterEach guard', error);
224
+ }
225
+ }
226
+
227
+ // ============================================
228
+ // ROUTING CORE (MODIFICADO CON GUARDS)
229
+ // ============================================
230
+
231
+ async navigate(path, _redirectChain = [], _options = {}) {
232
+ const currentPath = window.location.pathname;
233
+
234
+ // Detectar loops infinitos: si ya visitamos esta ruta en la cadena de redirecciones
235
+ if (_redirectChain.includes(path)) {
236
+ slice.logger.logError('Router', `Guard redirection loop detected: ${_redirectChain.join(' → ')} → ${path}`);
237
+ return;
238
+ }
239
+
240
+ // Límite de seguridad: máximo 10 redirecciones
241
+ if (_redirectChain.length >= 10) {
242
+ slice.logger.logError('Router', `Too many redirections: ${_redirectChain.join(' → ')} → ${path}`);
243
+ return;
244
+ }
245
+
246
+ // Obtener información de ruta actual
247
+ const { route: fromRoute, params: fromParams } = this.matchRoute(currentPath);
248
+ const from = this._createRouteInfo(fromRoute, fromParams, currentPath);
249
+
250
+ // Obtener información de ruta destino
251
+ const { route: toRoute, params: toParams } = this.matchRoute(path);
252
+ const to = this._createRouteInfo(toRoute, toParams, path);
253
+
254
+ // EJECUTAR BEFORE EACH GUARD
255
+ const guardResult = await this._executeBeforeEachGuard(to, from);
256
+
257
+ // Si el guard redirige
258
+ if (guardResult && guardResult.path) {
259
+ const newChain = [..._redirectChain, path];
260
+ return this.navigate(guardResult.path, newChain, guardResult.options);
261
+ }
262
+
263
+ // Si el guard cancela la navegación (next(false))
264
+ if (guardResult && guardResult.path === false) {
265
+ slice.logger.logInfo('Router', 'Navigation cancelled by guard');
266
+ return;
267
+ }
268
+
269
+ // No hay redirección - continuar con la navegación normal
270
+ // Usar replace o push según las opciones
271
+ if (_options.replace) {
272
+ window.history.replaceState({}, path, window.location.origin + path);
273
+ } else {
274
+ window.history.pushState({}, path, window.location.origin + path);
275
+ }
276
+
277
+ await this._performNavigation(to, from);
278
+ }
279
+
280
+ /**
281
+ * Método interno para ejecutar la navegación después de pasar los guards
282
+ * @param {Object} to - Información de ruta destino
283
+ * @param {Object} from - Información de ruta origen
284
+ */
285
+ async _performNavigation(to, from) {
286
+ // Renderizar la nueva ruta
287
+ await this.onRouteChange();
288
+
289
+ // EJECUTAR AFTER EACH GUARD
290
+ this._executeAfterEachGuard(to, from);
291
+
292
+ // Emitir evento de cambio de ruta
293
+ this._emitRouteChange(to, from);
294
+ }
295
+
296
+ async onRouteChange() {
297
+ // Cancelar el timeout anterior si existe
298
+ if (this.routeChangeTimeout) {
299
+ clearTimeout(this.routeChangeTimeout);
300
+ }
301
+
302
+ // Debounce de 10ms para evitar múltiples llamadas seguidas
303
+ this.routeChangeTimeout = setTimeout(async () => {
304
+ const path = window.location.pathname;
305
+ const routeContainersFlag = await this.renderRoutesComponentsInPage();
306
+
307
+ if (routeContainersFlag) {
308
+ return;
309
+ }
310
+
311
+ const { route, params } = this.matchRoute(path);
312
+ if (route) {
313
+ await this.handleRoute(route, params);
314
+ }
315
+ }, 10);
316
+ }
317
+
318
+ async handleRoute(route, params) {
319
+ const targetElement = document.querySelector('#app');
320
+
321
+ const componentName = route.parentRoute ? route.parentRoute.component : route.component;
322
+ const sliceId = `route-${componentName}`;
323
+
324
+ const existingComponent = slice.controller.getComponent(sliceId);
325
+
326
+ if (slice.loading) {
327
+ slice.loading.start();
328
+ }
329
+
330
+ if (existingComponent) {
331
+ targetElement.innerHTML = '';
332
+ if (existingComponent.update) {
333
+ existingComponent.props = { ...existingComponent.props, ...params };
334
+ await existingComponent.update();
335
+ }
336
+ targetElement.appendChild(existingComponent);
337
+ await this.renderRoutesInComponent(existingComponent);
338
+ } else {
339
+ const component = await slice.build(componentName, {
340
+ params,
341
+ sliceId: sliceId,
342
+ });
343
+
344
+ targetElement.innerHTML = '';
345
+ targetElement.appendChild(component);
346
+
347
+ await this.renderRoutesInComponent(component);
348
+ }
349
+
350
+ // Invalidar caché después de cambios importantes en el DOM
351
+ this.invalidateCache();
352
+
353
+ if (slice.loading) {
354
+ slice.loading.stop();
355
+ }
356
+
357
+ slice.router.activeRoute = route;
358
+ }
359
+
360
+ async loadInitialRoute() {
361
+ const path = window.location.pathname;
362
+ const { route, params } = this.matchRoute(path);
363
+
364
+ // Para la carga inicial, también ejecutar guards
365
+ const from = this._createRouteInfo(null, {}, null);
366
+ const to = this._createRouteInfo(route, params, path);
367
+
368
+ // EJECUTAR BEFORE EACH GUARD en carga inicial
369
+ const guardResult = await this._executeBeforeEachGuard(to, from);
370
+
371
+ if (guardResult && guardResult.path) {
372
+ return this.navigate(guardResult.path, [], guardResult.options);
373
+ }
374
+
375
+ // Si el guard cancela la navegación inicial (caso raro pero posible)
376
+ if (guardResult && guardResult.path === false) {
377
+ slice.logger.logWarning('Router', 'Initial route navigation cancelled by guard');
378
+ return;
379
+ }
380
+
381
+ await this.handleRoute(route, params);
382
+
383
+ // EJECUTAR AFTER EACH GUARD en carga inicial
384
+ this._executeAfterEachGuard(to, from);
385
+
386
+ // Emitir evento de cambio de ruta
387
+ this._emitRouteChange(to, from);
388
+ }
389
+
390
+ /**
391
+ * Emitir evento de cambio de ruta
392
+ * @param {Object} to
393
+ * @param {Object} from
394
+ */
395
+ _emitRouteChange(to, from) {
396
+ const payload = { to, from };
397
+
398
+ if (slice.eventsConfig?.enabled && slice.events && typeof slice.events.emit === 'function') {
399
+ slice.events.emit('router:change', payload);
400
+ return;
401
+ }
402
+
403
+ window.dispatchEvent(new CustomEvent('router:change', { detail: payload }));
404
+ }
405
+
406
+ // ============================================
407
+ // MÉTODOS EXISTENTES (SIN CAMBIOS)
408
+ // ============================================
409
+
410
+ setupMutationObserver() {
411
+ if (typeof MutationObserver !== 'undefined') {
412
+ this.observer = new MutationObserver((mutations) => {
413
+ let shouldInvalidateCache = false;
414
+
415
+ mutations.forEach((mutation) => {
416
+ if (mutation.type === 'childList') {
417
+ const addedNodes = Array.from(mutation.addedNodes);
418
+ const removedNodes = Array.from(mutation.removedNodes);
419
+
420
+ const hasRouteNodes = [...addedNodes, ...removedNodes].some(
421
+ (node) =>
422
+ node.nodeType === Node.ELEMENT_NODE &&
423
+ (node.tagName === 'SLICE-ROUTE' ||
424
+ node.tagName === 'SLICE-MULTI-ROUTE' ||
425
+ node.querySelector?.('slice-route, slice-multi-route'))
426
+ );
427
+
428
+ if (hasRouteNodes) {
429
+ shouldInvalidateCache = true;
430
+ }
431
+ }
432
+ });
433
+
434
+ if (shouldInvalidateCache) {
435
+ this.invalidateCache();
436
+ }
437
+ });
438
+
439
+ this.observer.observe(document.body, {
440
+ childList: true,
441
+ subtree: true,
442
+ });
443
+ }
444
+ }
445
+
446
+ invalidateCache() {
447
+ this.routeContainersCache.clear();
448
+ this.lastCacheUpdate = 0;
449
+ }
450
+
451
+ createPathToRouteMap(routes, basePath = '', parentRoute = null) {
452
+ const pathToRouteMap = new Map();
453
+
454
+ for (const route of routes) {
455
+ const fullPath = `${basePath}${route.path}`.replace(/\/+/g, '/');
456
+
457
+ const routeWithParent = {
458
+ ...route,
459
+ fullPath,
460
+ parentPath: parentRoute ? parentRoute.fullPath : null,
461
+ parentRoute: parentRoute,
462
+ };
463
+
464
+ pathToRouteMap.set(fullPath, routeWithParent);
465
+
466
+ if (route.children) {
467
+ const childPathToRouteMap = this.createPathToRouteMap(route.children, fullPath, routeWithParent);
468
+
469
+ for (const [childPath, childRoute] of childPathToRouteMap.entries()) {
470
+ pathToRouteMap.set(childPath, childRoute);
471
+ }
472
+ }
473
+ }
474
+
475
+ return pathToRouteMap;
476
+ }
477
+
478
+ async renderRoutesComponentsInPage(searchContainer = document) {
479
+ let routerContainersFlag = false;
480
+ const routeContainers = this.getCachedRouteContainers(searchContainer);
481
+
482
+ for (const routeContainer of routeContainers) {
483
+ try {
484
+ if (!routeContainer.isConnected) {
485
+ this.invalidateCache();
486
+ continue;
487
+ }
488
+
489
+ let response = await routeContainer.renderIfCurrentRoute();
490
+ if (response) {
491
+ this.activeRoute = routeContainer.props;
492
+ routerContainersFlag = true;
493
+ }
494
+ } catch (error) {
495
+ slice.logger.logError('Router', `Error rendering route container`, error);
496
+ }
497
+ }
498
+
499
+ return routerContainersFlag;
500
+ }
501
+
502
+ getCachedRouteContainers(container) {
503
+ const containerKey = container === document ? 'document' : container.sliceId || 'anonymous';
504
+ const now = Date.now();
505
+
506
+ if (this.routeContainersCache.has(containerKey) && now - this.lastCacheUpdate < this.CACHE_DURATION) {
507
+ return this.routeContainersCache.get(containerKey);
508
+ }
509
+
510
+ const routeContainers = this.findAllRouteContainersOptimized(container);
511
+ this.routeContainersCache.set(containerKey, routeContainers);
512
+ this.lastCacheUpdate = now;
513
+
514
+ return routeContainers;
515
+ }
516
+
517
+ findAllRouteContainersOptimized(container) {
518
+ const routeContainers = [];
519
+
520
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
521
+ acceptNode: (node) => {
522
+ if (node.tagName === 'SLICE-ROUTE' || node.tagName === 'SLICE-MULTI-ROUTE') {
523
+ return NodeFilter.FILTER_ACCEPT;
524
+ }
525
+ return NodeFilter.FILTER_SKIP;
526
+ },
527
+ });
528
+
529
+ let node;
530
+ while ((node = walker.nextNode())) {
531
+ routeContainers.push(node);
532
+ }
533
+
534
+ return routeContainers;
535
+ }
536
+
537
+ async renderRoutesInComponent(component) {
538
+ if (!component) {
539
+ slice.logger.logWarning('Router', 'No component provided for route rendering');
540
+ return false;
541
+ }
542
+
543
+ return await this.renderRoutesComponentsInPage(component);
544
+ }
545
+
546
+ matchRoute(path) {
547
+ const exactMatch = this.pathToRouteMap.get(path);
548
+ if (exactMatch) {
549
+ if (exactMatch.parentRoute) {
550
+ return {
551
+ route: exactMatch.parentRoute,
552
+ params: {},
553
+ childRoute: exactMatch,
554
+ };
555
+ }
556
+ return { route: exactMatch, params: {} };
557
+ }
558
+
559
+ for (const [routePattern, route] of this.pathToRouteMap.entries()) {
560
+ if (routePattern.includes('${')) {
561
+ const { regex, paramNames } = this.compilePathPattern(routePattern);
562
+ const match = path.match(regex);
563
+ if (match) {
564
+ const params = {};
565
+ paramNames.forEach((name, i) => {
566
+ params[name] = match[i + 1];
567
+ });
568
+
569
+ if (route.parentRoute) {
570
+ return {
571
+ route: route.parentRoute,
572
+ params: params,
573
+ childRoute: route,
574
+ };
575
+ }
576
+
577
+ return { route, params };
578
+ }
579
+ }
580
+ }
581
+
582
+ const notFoundRoute = this.pathToRouteMap.get('/404');
583
+ return { route: notFoundRoute, params: {} };
584
+ }
585
+
586
+ compilePathPattern(pattern) {
587
+ const paramNames = [];
588
+ const regexPattern =
589
+ '^' +
590
+ pattern.replace(/\$\{([^}]+)\}/g, (_, paramName) => {
591
+ paramNames.push(paramName);
592
+ return '([^/]+)';
593
+ }) +
594
+ '$';
595
+
596
+ return { regex: new RegExp(regexPattern), paramNames };
597
+ }
598
+ }